Compare commits

...

182 Commits
3.0.9 ... 3.3.2

Author SHA1 Message Date
57366e972a Updates changelog and bumps up release version to 3.3.2 2017-12-19 11:40:11 -05:00
884f476598 Merge pull request #1228 from mailpoet/change-update-header
updated the header string [MAILPOET-1195]
2017-12-19 09:01:13 -05:00
55296d52ec updated the header string 2017-12-19 12:47:57 +00:00
8fb4d2e78f Merge pull request #1227 from mailpoet/status_page_update
Validates ping response when displaying cron URL status [MAILPOET-1253]
2017-12-19 14:29:47 +03:00
840473aae5 Merge pull request #1224 from mailpoet/send_with_language_update
Removes unused string in "send with" tab [MAILPOET-1252]
2017-12-19 09:49:06 +01:00
0e2a67c203 Validates ping response when displaying cron URL status 2017-12-18 20:28:09 -05:00
aaed5add52 Removes unused string 2017-12-17 10:51:39 -05:00
f616ef4d6c Merge pull request #1218 from mailpoet/widget_update
Prevents loading plugin assets when form widget is not activated [MAILPOET-1241]
2017-12-15 23:15:31 +02:00
083a6fd3a3 Merge pull request #1223 from mailpoet/new-surveys
What's New: 2 new surveys, less call-to-actions [MAILPOET-1243]
2017-12-14 23:28:00 +03:00
9a1ec60750 releasing 3.3.1 2017-12-14 18:09:17 +00:00
bd67f5fd36 New poll
[MAILPOET-1243]
2017-12-14 14:46:18 +00:00
ed2a2a6fa8 Remove additional CTAs
[PREMIUM-1243]
2017-12-14 13:12:53 +00:00
6a28fdcac2 Merge pull request #1222 from mailpoet/number_formatting_fix
Formats number according to user's locale [MAILPOET-1246]
2017-12-14 15:38:37 +03:00
fda1828637 Adds composer.lock security check
[MAILPOET-1226]
2017-12-14 08:47:47 +00:00
ebcc094b4d Formats number according to user's locale 2017-12-13 21:53:03 -05:00
51cde55217 Tests statistics column 2017-12-13 09:39:03 +00:00
23dff7403d Updates variable names to match those in views/newsletters.html 2017-12-13 09:39:03 +00:00
de164f918c releasing 3.3.0 2017-12-12 17:03:21 +00:00
84840c5261 releasing 3.2.5 2017-12-12 16:30:44 +00:00
16b9e0d467 Merge pull request #1219 from mailpoet/new_poll
New poll [MAILPOET-1225]
2017-12-11 18:56:16 +03:00
3c75e7b0bc Merge pull request #1211 from mailpoet/email-segments
Email segments [PREMIUM-44]
2017-12-11 09:29:49 -05:00
c41d5894d0 New poll
[MAILPOET-1225]
2017-12-11 09:39:20 +00:00
1f76f0d98f Allow form fields styling
[PREMIUM-44]
2017-12-11 09:05:29 +00:00
e859c090c8 Allow values for selections to be passed
[PREMIUM-44]
2017-12-11 08:54:43 +00:00
42528ab309 Allow select2 for single selects
[PREMIUM-44]
2017-12-11 08:49:36 +00:00
d761867a7d Enable dynamic segment edits
[PREMIUM-44]
2017-12-11 08:49:36 +00:00
6996615999 Improves tests that would fail due to array_flip and 1 item in array 2017-12-10 19:31:35 -05:00
3450a6fc97 Adds wait condition before assertion; corrects prefix 2017-12-10 18:41:54 -05:00
554dbb8dda Initializes dependencies only when widget is active 2017-12-09 19:53:12 -05:00
d13aa67a07 Centralizes widget logic in one place
Cleans up code
2017-12-09 19:45:19 -05:00
1f9c956637 Removes unused code 2017-12-09 19:27:21 -05:00
59d09866a1 Merge pull request #1217 from mailpoet/eslint-es6-3
Eslint es6 3 [MAILPOET-1139]
2017-12-07 18:49:21 +01:00
f8ade27a6c Improve code
[MAILPOET-1139]
2017-12-07 17:31:15 +00:00
313bcddba1 Split line into multiple lines
[MAILPOET-1139]
2017-12-07 16:39:02 +00:00
fd64702eaa Changes WP DB prefix 2017-12-07 11:08:52 +00:00
e2884ac625 Fix eqeqeq eslint rule in ES6 files
[MAILPOET-1139]
2017-12-07 09:44:58 +00:00
9ce9cb929f Fix camelcase eslint rule in ES6 files
[MAILPOET-1139]
2017-12-07 09:39:05 +00:00
52d33d7fe8 Merge pull request #1215 from mailpoet/fix-template-duplication
Use website language when running populator [MAILPOET-1233]
2017-12-06 20:05:53 -05:00
2ee33b2fc2 Bumps up minimum required WP version 2017-12-06 19:52:12 -05:00
b107be3241 Use website language when running populator
[MAILPOET-1233]
2017-12-06 19:51:09 -05:00
76148d0a85 Fix import/no-extraneous-dependendencies eslint rule in ES6 files
[MAILPOET-1139]
2017-12-06 16:37:45 +00:00
61a7f99e42 Make import/extensions eslint rule an exception in ES6 files
[MAILPOET-1139]
2017-12-06 16:19:48 +00:00
62e3ca8a80 Fix consistent-return eslint rule in ES6 files
[MAILPOET-1139]
2017-12-06 16:05:21 +00:00
d431efb288 Fix array-callback-return eslint rule in ES6 files
[MAILPOET-1139]
2017-12-06 15:55:15 +00:00
adf6485b7d Fix default-case eslint rule in ES6 files
[MAILPOET-1139]
2017-12-06 14:30:54 +00:00
170fd7f051 Fix prefer-template eslint rule in ES6 files
[MAILPOET-1139]
2017-12-06 14:18:24 +00:00
e7ddfc3d29 Fix arrow-body-style eslint rule in ES6 files
[MAILPOET-1139]
2017-12-06 14:16:41 +00:00
b08b53e9c0 Fix no-bitwise eslint rule in ES6 files
[MAILPOET-1139]
2017-12-06 13:58:56 +00:00
570529a8d8 Release 3.2.4 2017-12-05 18:58:18 +02:00
5505ffc783 Merge pull request #1206 from mailpoet/manage_subscription_shortcode_update
Allows using manage_subscription shortcode outside of newsletters [MAILPOET-1122]
2017-12-05 19:38:37 +03:00
6e5fc6ad6b Fix array_key_exists() error if no data is passed to subscription management page [MAILPOET-1122] 2017-12-05 19:22:28 +03:00
e731b261ab Conditionally initializes shortcodes/filters 2017-12-05 10:34:03 -05:00
955f24e71a Load mb_strtoupper() polyfill for MP2 migration
* Load mb_strtoupper() polyfill for MP2 migration
* Add "symfony/polyfill-mbstring" as explicit dependency
* Fixate AspectMock at 2.0.1
* Generate composer.lock with PHP 5.6 to include required dependencies

[MAILPOET-1234]
2017-12-05 11:15:31 +00:00
89ebc6051a Merge pull request #1210 from mailpoet/help_status_tab
Adds system status tab to the Help menu
2017-12-05 10:38:19 +02:00
79fff7b65d Merge pull request #1212 from mailpoet/new-poll
New poll [MAILPOET-1213]
2017-12-04 20:09:15 -05:00
7864e08900 Enables [mailpoet_manage] shortcode
Updates code
2017-12-04 19:58:30 -05:00
878cdf46e6 Update vulnerable Handlebars to newest version [MAILPOET-1235] 2017-12-04 16:12:30 +00:00
131d341744 New poll
[MAILPOET-1213]
2017-12-04 11:13:34 +00:00
513062b15d Cleans up code 2017-12-03 11:38:45 -05:00
4926267e36 Adds system status tab to the Help menu 2017-12-03 11:38:45 -05:00
aa4d78b1c9 Adds method to ping Bridge 2017-12-03 11:38:44 -05:00
8d7289e8ba Merge pull request #1208 from mailpoet/eslint-rules2
Eslint rules2 [MAILPOET-1144]
2017-11-30 19:24:26 -05:00
eb768ff1ee Merge pull request #1209 from mailpoet/mailer_error_code
Make rendering errors during sending more descriptive [MAILPOET-1232]
2017-11-30 12:09:40 -05:00
8afe7f5d97 Make rendering error during sending more descriptive [MAILPOET-1232] 2017-11-30 19:45:53 +03:00
97fb5cf66f Makes string translateable 2017-11-30 09:44:34 +00:00
8ea4a219e2 Fix padded-blocks eslint rule in ES5 files
[MAILPOET-1144]
2017-11-30 09:12:19 +00:00
0e08e58288 Fix camelcase eslint rule in ES5 files
[MAILPOET-1144]
2017-11-30 09:10:08 +00:00
dc52ba0d93 Bump up release version to 3.2.3 2017-11-29 18:35:43 +03:00
dc569672a9 Fix no-use-before-define eslint rule in ES5 files
[MAILPOET-1144]
2017-11-29 15:17:30 +00:00
159e946093 Fix no-else-return eslint rule in ES5 files
[MAILPOET-1144]
2017-11-29 15:04:38 +00:00
3d2a433df1 Moved no-underscore-dangel eslint rule to exceptions
this._appView cannot be renamed

[MAILPOET-1144]
2017-11-29 15:01:16 +00:00
fe0476e1c0 Fix no-shadow eslint rule in ES5 files
[MAILPOET-1144]
2017-11-29 14:49:19 +00:00
ea552508b4 Fix consistent-return eslint rule in ES5 files
[MAILPOET-1144]
2017-11-29 13:43:35 +00:00
dee2ff810c Fixes undefined index error 2017-11-29 10:19:30 +00:00
63ed835d64 Allows using manage_subscription shortcode outside of newsletters 2017-11-28 22:02:29 -05:00
fbf58f23fc Bump up release version to 3.2.2 2017-11-28 20:18:18 +03:00
b6f62bd9bc Catches exceptions during sending 2017-11-28 16:42:22 +00:00
88ef454844 Merge pull request #1203 from mailpoet/nov28_survey
Updates weekly survey [MAILPOET-1224]
2017-11-28 16:07:22 +03:00
3572df6c0c Fixes "other" method test email sending when MSS is enabled
[MAILPOET-1218]
2017-11-28 09:28:56 +00:00
714e6e013f Updates weekly survey 2017-11-27 19:51:40 -05:00
91568cbeb6 Merge pull request #1202 from mailpoet/post_notification_creation_acceptance_test
Tests post notification creation [MAILPOET-1219]
2017-11-27 12:59:48 +02:00
393c89b21f Tests that post notifications can be created 2017-11-23 21:02:42 -05:00
0b491b7943 Adds method to get current URL 2017-11-23 21:02:41 -05:00
e5d7f66561 Adds new id to create standard/notification newsletters 2017-11-23 21:02:36 -05:00
b7a7f40cde Merge pull request #1199 from mailpoet/image_url_space_fix
Encodes URLs in inlined images [MAILPOET-1220]
2017-11-23 16:03:57 +03:00
a267c7524c Close the a tag to make valid HTML [MAILPOET-1220] 2017-11-23 16:02:40 +03:00
20b59d11a7 Merge pull request #1201 from mailpoet/user_profile_language_fix
Changes plugin language based on user's locale [MAILPOET-1211]
2017-11-23 13:40:59 +02:00
94c7e2a5c0 Changes plugin language based on user's locale 2017-11-22 20:13:56 -05:00
d603d99a04 Merge pull request #1190 from mailpoet/multisite_unit_tests
Adds new CI multisite test environment
2017-11-22 15:16:20 +02:00
e7ffe4d694 Replaces spaces in image URLs 2017-11-21 21:02:33 -05:00
018d7bce77 Corrects test to work on multisite environment 2017-11-21 12:26:02 -05:00
64c40d5a1c Adds test for multisite environment 2017-11-21 12:25:54 -05:00
7e49328d5e Corrects test to work on multisite environment 2017-11-21 12:24:19 -05:00
b50b636371 Adds multisite test environment to CI
Adds Robo command to run multisite tests locally and on CI
2017-11-21 12:24:18 -05:00
ec3bb5b95c Release 3.2.1 2017-11-21 15:18:40 +00:00
6b2503fb36 Merge pull request #1196 from mailpoet/minimum_wp_version_notice
Shows notice on WP version < 4.6 and deactivates plugin [MAILPOET-1215]
2017-11-21 16:26:03 +02:00
e71d47f983 Adds link to minimum requirements page for WP version 2017-11-21 08:57:28 -05:00
085d4f566a Merge pull request #1198 from mailpoet/hide-honeypot
Hide honeypot field to prevent Safari autocomplete [MAILPOET-1180]
2017-11-21 16:08:45 +03:00
29b249de6e Remove an attribute we don't need anymore
[MAILPOET-1180]
2017-11-21 12:54:00 +00:00
883ae5b0e4 Hide honeypot field to prevent Safari autocomplete
[MAILPOET-1180]
2017-11-21 11:47:46 +00:00
d78990cda3 Shows notice on WP version < 4.6 and deactivates plugin 2017-11-20 14:26:31 -05:00
e5b5a8df37 Merge pull request #1197 from mailpoet/poll_nov20
Update the poll [MAILPOET-1212]
2017-11-20 11:30:29 -05:00
2ea2db66ec Update the poll [MAILPOET-1212] 2017-11-20 19:03:47 +03:00
7b76ddefd5 Merge pull request #1194 from mailpoet/double_opt-in_test
Add an acceptance test for subscription confirmation [MAILPOET-1205]
2017-11-20 14:36:55 +02:00
6c56ac8509 Clean up the acceptance dump from unused pages [MAILPOET-1206] 2017-11-20 10:42:17 +00:00
dc074bcb14 Add acceptances tests for admin listings [MAILPOET-1206] [MAILPOET-1207] [MAILPOET-1208] [MAILPOET-1209] 2017-11-20 10:42:17 +00:00
0193644b18 Merge pull request #1195 from mailpoet/test_email_send_fix
Fixes "mailer not defined" JS error [MAILPOET-1217]
2017-11-20 13:21:16 +03:00
66de11ecd4 Fixes "mailer not defined" JS error 2017-11-18 19:51:53 -05:00
1238b1711d Add an acceptance test for subscription confirmation [MAILPOET-1205] 2017-11-16 14:57:56 +03:00
0061a9daf9 Move key checks from constructor to init() in Menu class [MAILPOET-1204] 2017-11-15 11:23:39 +00:00
52a55d3bd1 Updates changelog and bumps up release version to 3.2.0 2017-11-14 14:13:49 -05:00
7d686eb1d1 Merge pull request #1191 from mailpoet/post_excerpt_shortcode_fix
Fixes shortcodes not being properly stripped in excerpts [MAILPOET-1210]
2017-11-14 20:51:21 +02:00
962188bdb8 Adds unit test 2017-11-14 13:39:56 -05:00
72c3f763ec Removes shortcodes from full post content 2017-11-14 13:17:47 -05:00
44c5d8490f Fixes shortcodes not being properly stripped on excerpts 2017-11-14 12:35:08 -05:00
fb85623b86 Merge pull request #1185 from mailpoet/mp_api_unsubscribe_from_lists
Adds API method to unsubscribe from lists [MAILPOET-1202]
2017-11-14 17:38:53 +02:00
d7bf6addf1 Fixes error due to throwing exception inside array_map 2017-11-14 09:58:01 -05:00
adea1e9be1 Fixes condition that allowed unsubscribing from WP segments 2017-11-14 09:56:53 -05:00
4f77ca31a3 Rebased master 2017-11-13 12:41:46 -05:00
087f2ebcdd Uses a built-in method to handle plural exception cases
Implements check for empty segments
Implements check for WP segment when unsubscribing
2017-11-13 10:12:50 -05:00
3d2a63f319 Adds methods to unsubscribe from list(s) 2017-11-13 10:12:50 -05:00
54dd3b621a Improves error messages, cleans up code 2017-11-13 10:11:07 -05:00
7fea134109 Merge pull request #1183 from mailpoet/dynamic-segments-listings
Dynamic segments subscribers listings [PREMIUM-38] [PREMIUM-43]
2017-11-13 16:52:48 +03:00
fcf4bd12d9 Merge pull request #1186 from mailpoet/mp_api_get_subscriber
Adds API method to get subscriber [MAILPOET-1203]
2017-11-13 15:38:47 +02:00
6694555592 Adds method to get subscriber 2017-11-10 18:47:53 -05:00
e6eb3d691e Fix quality problems
[PREMIUM-38]
2017-11-09 12:14:01 +00:00
00f2d418cc Load subscribers from dynamic segments
[PREMIUM-38]
2017-11-09 10:11:47 +00:00
c63f218edd Enable to add segments to subscribers listing filter
[PREMIUM-38]
2017-11-09 10:11:47 +00:00
fae849bbfc Merge pull request #1182 from mailpoet/es6_rules_1
Fix ES6 rules 1 [MAILPOET-1137]
2017-11-08 22:32:43 -05:00
2a253ccb8d Move ES6 no-script-url rule to exceptions [MAILPOET-1137] 2017-11-08 21:42:10 -05:00
ff55e55ad2 Fix indentation [MAILPOET-1137] 2017-11-08 21:42:10 -05:00
d798ec446d Fix ES6 no-alert eslint rule (replace confirm() with a custom UI component) [MAILPOET-1137] 2017-11-08 21:41:17 -05:00
56b4038073 Fix padded-blocks after rebasing [MAILPOET-1137] 2017-11-08 21:41:16 -05:00
076a55f1fb Fix ES6 no-underscore-dangle eslint rule [MAILPOET-1137] 2017-11-08 21:41:16 -05:00
10eca98dc3 Fix ES6 padded-blocks eslint rule [MAILPOET-1137] 2017-11-08 21:41:16 -05:00
37aec3ee4f Fix ES6 wrap-iife eslint rule [MAILPOET-1137] 2017-11-08 21:41:16 -05:00
dc7c629e3b Fix ES6 no-shadow eslint rule [MAILPOET-1137] 2017-11-08 21:41:16 -05:00
e4f16eda49 Fix ES6 dot-notation eslint rule [MAILPOET-1137] 2017-11-08 21:41:16 -05:00
bf15bda84f Merge pull request #1181 from mailpoet/norwegian
Update translations in readme [MAILPOET-1196]
2017-11-08 09:19:20 -05:00
8472a837e8 Fix an 'Undefined index: SERVER_NAME' error in ConflictResolverTest [MAILPOET-1187] 2017-11-08 14:18:58 +00:00
fc326131ae Rebased master 2017-11-08 14:04:45 +00:00
a19753205f Increases timeout value when tests are run on slow hosts 2017-11-08 14:04:45 +00:00
ee404e3b84 Uses CLI to truncate table 2017-11-08 14:04:45 +00:00
f1918ac953 Removes timeouts and uses data-automation-id as element selector 2017-11-08 14:04:45 +00:00
d399ddf6b6 Fixed acceptance tests for me 2017-11-08 14:04:45 +00:00
3f06448f37 Uses data-automation-id/iframe id and bypasses throttling for
consecutive signups
2017-11-08 14:04:45 +00:00
83d84e67d6 Adds test for iframe form subscription 2017-11-08 14:04:45 +00:00
46c42c1bb4 Re-adds vendor folder installation (this is needed for Alex's setup) 2017-11-08 14:04:45 +00:00
944bf67190 Updates docker-compose 2017-11-08 14:04:45 +00:00
7cac061a73 Renames test 2017-11-08 14:04:45 +00:00
17764b708f Adds check for JS errors 2017-11-08 14:04:45 +00:00
2e0f4dfb19 Removes the need to reinstall vendor folder 2017-11-08 14:04:45 +00:00
7f5bc8681c Adds acceptance tests for unsubscribe/manage subscription links 2017-11-08 14:04:45 +00:00
bfcb85f744 Updates existing tests format 2017-11-08 14:04:45 +00:00
cb9aaf120e Cleans up DB dump 2017-11-08 14:04:45 +00:00
97a9465db3 Adds MailHog for SMTP testing 2017-11-08 14:04:45 +00:00
8dfaf9ba32 Release 3.1.0 2017-11-07 15:20:00 +02:00
89d0da93d3 Merge pull request #1180 from mailpoet/wp_sync_db_error_fix
Fixes "column cannot be null" error during WP sync [MAILPOET-1191]
2017-11-07 12:23:39 +02:00
c9cbfb4317 Update translations in readme [MAILPOET-1196] 2017-11-07 12:04:56 +03:00
a8ccfec5c6 Merge pull request #1178 from mailpoet/superadmin_access
Enable permissions for superadmin users [MAILPOET-1200]
2017-11-07 11:15:33 +03:00
d8609c9e84 Merge pull request #1179 from mailpoet/survey_nov7
Updates weekly survey [MAILPOET-1199]
2017-11-07 10:41:07 +03:00
5461c975d4 Fixes "column cannot be null" error during WP sync 2017-11-06 22:29:59 -05:00
11298bc101 Updates weekly survey 2017-11-06 22:25:35 -05:00
c42cf2f622 Switch to using current_user_can function to check capabilities 2017-11-06 18:09:38 +02:00
c9f1d38baa Merge pull request #1177 from mailpoet/mpapi_create_list
Add a method to create a new list to public API [MAILPOET-1197]
2017-11-02 18:54:06 -04:00
3d78d6bbe9 Add a method to create a new list to public API [MAILPOET-1197] 2017-11-02 22:13:50 +03:00
2b4288f301 Merge pull request #1176 from mailpoet/throttling_test_fix
Fix an off-by-one error in some timezones [MAILPOET-1186]
2017-11-02 14:34:03 +02:00
09a2dd231a Fix an off-by-one error in some timezones [MAILPOET-1186] 2017-11-02 14:56:44 +03:00
3fd4ef9985 Cleanup after WP sync test [MAILPOET-1185] 2017-11-01 15:43:31 +00:00
05979965ba Merge pull request #1174 from mailpoet/es6
ESLint: ES6 rules 2 [MAILPOET-1138]
2017-11-01 11:32:34 -04:00
e625a7602a Merge pull request #1171 from mailpoet/mixpanel
Report more system environment data via MixPanel [MAILPOET-1189]
2017-11-01 11:21:29 -04:00
a0c41ad7ab Merge pull request #1168 from mailpoet/jquery
plugin scripts should be loaded with a dependency on jquery [MAILPOET-1149]
2017-11-01 15:01:38 +02:00
7163747eb9 no-extra-boolean-cast 2017-11-01 10:20:13 +00:00
c30e2b6cf3 no-sequences 2017-10-31 17:16:24 +00:00
e9eae92ba9 no-useless-concat 2017-10-31 17:15:09 +00:00
0fd6fa8879 max-len 2017-10-31 17:12:59 +00:00
52ca6eac18 no-else-return 2017-10-31 17:10:10 +00:00
28776b8558 no-case-declarations 2017-10-31 16:52:00 +00:00
588ad3eab7 class-methods-use-this 2017-10-31 16:46:19 +00:00
d791538086 no-lonely-if 2017-10-31 16:40:08 +00:00
f39a2c8dda added env data to weekly report 2017-10-31 11:17:34 +00:00
cd145c51f7 public.js now depends on jquery 2017-10-30 14:25:05 +00:00
166 changed files with 11889 additions and 4425 deletions

View File

@ -1,6 +1,4 @@
Listen 8080 <VirtualHost *:80>
<VirtualHost *:8080>
UseCanonicalName Off UseCanonicalName Off
ServerName mailpoet.loc ServerName mailpoet.loc
DocumentRoot /home/circleci/mailpoet/wordpress DocumentRoot /home/circleci/mailpoet/wordpress
@ -8,6 +6,9 @@ Listen 8080
LogLevel notice LogLevel notice
<Directory /home/circleci/mailpoet/wordpress> <Directory /home/circleci/mailpoet/wordpress>
Options Indexes FollowSymLinks
AllowOverride All
RewriteEngine On
Require all granted Require all granted
</Directory> </Directory>
</VirtualHost> </VirtualHost>

View File

@ -1,6 +1,6 @@
version: 2 version: 2
jobs: jobs:
qa_js_php5: qa_js_security_php5:
working_directory: /home/circleci/mailpoet working_directory: /home/circleci/mailpoet
docker: docker:
- image: circleci/php:5.6.30-apache-browsers - image: circleci/php:5.6.30-apache-browsers
@ -38,6 +38,10 @@ jobs:
command: | command: |
mkdir test-results/mocha mkdir test-results/mocha
./do t:j test-results/mocha/junit.xml ./do t:j test-results/mocha/junit.xml
- run:
name: "Composer security check"
command: |
./do s:composer
- run: - run:
name: "PHP Unit tests" name: "PHP Unit tests"
command: | command: |
@ -120,7 +124,38 @@ jobs:
- run: - run:
name: "PHP Unit tests" name: "PHP Unit tests"
command: | command: |
WP_TEST_PATH="/home/circleci/mailpoet/wordpress" ./do t:u --xml ./do t:u --xml
- store_test_results:
path: tests/_output
- store_artifacts:
path: tests/_output
destination: codeception
- store_artifacts:
path: /tmp/fake-mailer/
destination: fake-mailer
php7_multisite:
working_directory: /home/circleci/mailpoet
docker:
- image: circleci/php:7.1-apache-browsers
- image: circleci/mysql:5.7
environment:
TZ: /usr/share/zoneinfo/Etc/UTC
steps:
- checkout
- run:
name: "Set up virtual host"
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- restore_cache:
key: composer-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
- restore_cache:
key: npm-{{ checksum "package.json" }}
- run:
name: "Set up test environment"
command: source ./.circleci/setup.bash && setup php7_multisite
- run:
name: "PHP Unit tests"
command: |
./do t:multisite-unit --xml
- store_test_results: - store_test_results:
path: tests/_output path: tests/_output
- store_artifacts: - store_artifacts:
@ -133,6 +168,7 @@ workflows:
version: 2 version: 2
build_and_test: build_and_test:
jobs: jobs:
- qa_js_php5 - qa_js_security_php5
- php7 - php7
- php7_multisite
- acceptance_tests - acceptance_tests

View File

@ -2,8 +2,11 @@
function setup { function setup {
local version=$1 local version=$1
local wp_cli_wordpress_path="--path=wordpress"
local wp_cli_allow_root="--allow-root"
# install PHP dependencies for WordPress # install PHP dependencies for WordPress
if [[ $version == "php7" ]]; then if [[ $version == "php7" ]] || [[ $version == "php7_multisite" ]]; then
echo "deb http://packages.dotdeb.org jessie all" | sudo tee -a /etc/apt/sources.list.d/dotdeb.list echo "deb http://packages.dotdeb.org jessie all" | sudo tee -a /etc/apt/sources.list.d/dotdeb.list
echo "deb-src http://packages.dotdeb.org jessie all" | sudo tee -a /etc/apt/sources.list.d/dotdeb.list echo "deb-src http://packages.dotdeb.org jessie all" | sudo tee -a /etc/apt/sources.list.d/dotdeb.list
wget -qO - http://www.dotdeb.org/dotdeb.gpg | sudo apt-key add - wget -qO - http://www.dotdeb.org/dotdeb.gpg | sudo apt-key add -
@ -15,35 +18,66 @@ function setup {
sudo apt-get install mysql-client php5-mysql zlib1g-dev sudo apt-get install mysql-client php5-mysql zlib1g-dev
sudo docker-php-ext-install mysql mysqli pdo pdo_mysql zip sudo docker-php-ext-install mysql mysqli pdo pdo_mysql zip
fi fi
# Add a fake sendmail mailer # Add a fake sendmail mailer
sudo cp ./.circleci/fake-sendmail.php /usr/local/bin/ sudo cp ./.circleci/fake-sendmail.php /usr/local/bin/
# configure Apache # configure Apache
sudo cp ./.circleci/mailpoet_php.ini /usr/local/etc/php/conf.d/ sudo cp ./.circleci/mailpoet_php.ini /usr/local/etc/php/conf.d/
sudo cp ./.circleci/apache/mailpoet.loc.conf /etc/apache2/sites-available sudo cp ./.circleci/apache/mailpoet.loc.conf /etc/apache2/sites-available
sudo a2dissite 000-default.conf
sudo a2ensite mailpoet.loc sudo a2ensite mailpoet.loc
sudo a2enmod rewrite sudo a2enmod rewrite
sudo service apache2 restart sudo service apache2 restart
# Install NodeJS+NPM # Install NodeJS+NPM
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install nodejs build-essential sudo apt-get install nodejs build-essential
# install plugin dependencies # install plugin dependencies
curl -sS https://getcomposer.org/installer | php curl -sS https://getcomposer.org/installer | php
./composer.phar install ./composer.phar install
./do install ./do install
# Set up Wordpress
# Set up WordPress
mysql -h 127.0.0.1 -u root -e "create database wordpress" mysql -h 127.0.0.1 -u root -e "create database wordpress"
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar chmod +x wp-cli.phar
./wp-cli.phar core download --allow-root --path=wordpress sudo mv wp-cli.phar /usr/local/bin/wp
wp core download $wp_cli_wordpress_path $wp_cli_allow_root
# Generate `wp-config.php` file with debugging enabled # Generate `wp-config.php` file with debugging enabled
echo "define(\"WP_DEBUG\", true);" | ./wp-cli.phar core config --allow-root --dbname=wordpress --dbuser=root --dbhost=127.0.0.1 --path=wordpress --extra-php echo "define(\"WP_DEBUG\", true);" | wp core config --dbname=wordpress --dbuser=root --dbhost=127.0.0.1 --extra-php $wp_cli_wordpress_path $wp_cli_allow_root
# Change default table prefix
sed -i "s/\$table_prefix = 'wp_';/\$table_prefix = 'mp_';/" ./wordpress/wp-config.php sed -i "s/\$table_prefix = 'wp_';/\$table_prefix = 'mp_';/" ./wordpress/wp-config.php
# Install WordPress # Install WordPress
./wp-cli.phar core install --allow-root --admin_name=admin --admin_password=admin --admin_email=admin@mailpoet.loc --url=http://mailpoet.loc:8080 --title=WordPress --path=wordpress if [[ $version == "php7_multisite" ]]; then
# Configure multisite environment
wp core multisite-install --admin_name=admin --admin_password=admin --admin_email=admin@mailpoet.loc --url=http://mailpoet.loc --title="WordPress MultiSite" $wp_cli_wordpress_path $wp_cli_allow_root
cp ./.circleci/wordpress/.htaccess ./wordpress/
# Add a second blog
wp site create --slug=php7_multisite $wp_cli_wordpress_path $wp_cli_allow_root
echo "WP_TEST_MULTISITE_SLUG=php7_multisite" >> .env
echo "WP_TEST_PATH_MULTISITE=/home/circleci/mailpoet/wordpress" >> .env
echo "HTTP_HOST=mailpoet.loc" >> .env
# Add a third dummy blog
wp site create --slug=dummy_multisite $wp_cli_wordpress_path $wp_cli_allow_root
else
wp core install --admin_name=admin --admin_password=admin --admin_email=admin@mailpoet.loc --url=http://mailpoet.loc --title="WordPress Single" $wp_cli_wordpress_path $wp_cli_allow_root
echo "WP_TEST_PATH=/home/circleci/mailpoet/wordpress" >> .env
fi
# Softlink plugin to plugin path # Softlink plugin to plugin path
ln -s ../../.. wordpress/wp-content/plugins/mailpoet ln -s ../../.. wordpress/wp-content/plugins/mailpoet
./wp-cli.phar plugin activate mailpoet --path=wordpress
# Create .env file with correct path to WP installation # Activate plugin
# TODO: Remove this line after PR gets merged and CircleCI env variables change if [[ $version == "php7_multisite" ]]; then
echo "WP_TEST_PATH=\"/home/circleci/mailpoet/wordpress\"" > .env wp plugin activate mailpoet --url=http://mailpoet.loc/php7_multisite/ $wp_cli_wordpress_path $wp_cli_allow_root
else
wp plugin activate mailpoet $wp_cli_wordpress_path $wp_cli_allow_root
fi
} }

View File

@ -0,0 +1,12 @@
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
RewriteRule . index.php [L]

View File

@ -1,4 +1,6 @@
WP_TEST_PATH="/var/www/wordpress" WP_TEST_PATH="/var/www/wordpress"
WP_TEST_PATH_MULTISITE="/var/www/wordpress"
WP_TEST_MULTISITE_SLUG=""
WP_TEST_ENABLE_NETWORK_TESTS="true" WP_TEST_ENABLE_NETWORK_TESTS="true"
WP_TEST_IMPORT_MAILCHIMP_API="" WP_TEST_IMPORT_MAILCHIMP_API=""
WP_TEST_IMPORT_MAILCHIMP_LISTS="" // (separated with comma) WP_TEST_IMPORT_MAILCHIMP_LISTS="" // (separated with comma)
@ -17,3 +19,4 @@ WP_TEST_MAILER_SMTP_PASSWORD=""
WP_SVN_USERNAME="" WP_SVN_USERNAME=""
WP_SVN_PASSWORD="" WP_SVN_PASSWORD=""
WP_TRANSIFEX_API_TOKEN="" WP_TRANSIFEX_API_TOKEN=""
HTTP_HOST="" // URL of your site (used for multisite env and equals to DOMAIN_CURRENT_SITE from wp-config.php)

View File

@ -8,6 +8,9 @@
"ecmaVersion": 5 "ecmaVersion": 5
}, },
"rules": { "rules": {
// Exceptions
"no-underscore-dangle": 0, // Backbone uses underscores, we cannot remove them
// Temporary
"import/no-amd": 0, "import/no-amd": 0,
"prefer-arrow-callback": 0, "prefer-arrow-callback": 0,
"radix": 0, "radix": 0,
@ -37,14 +40,7 @@
"global-require": 0, "global-require": 0,
"no-throw-literal": 0, "no-throw-literal": 0,
"no-extra-bind": 0, "no-extra-bind": 0,
"consistent-return": 0,
"no-shadow": 0,
"no-underscore-dangle": 0,
"brace-style": 0, "brace-style": 0,
"no-else-return": 0,
"no-use-before-define": 0,
"camelcase": 0,
"padded-blocks": 0,
"strict": 0, "strict": 0,
"space-infix-ops": 0, "space-infix-ops": 0,
"object-shorthand": 0, "object-shorthand": 0,

View File

@ -14,7 +14,11 @@
"import/resolver": "webpack" "import/resolver": "webpack"
}, },
"rules": { "rules": {
// Exceptions
"comma-dangle": ["error", "always-multiline"], "comma-dangle": ["error", "always-multiline"],
"no-script-url": 0,
"import/extensions": 0, // we wouldn't be able to import jQuery without this line
// Temporary
"import/no-amd": 0, "import/no-amd": 0,
"react/no-multi-comp": 0, "react/no-multi-comp": 0,
"react/sort-comp": 0, "react/sort-comp": 0,
@ -37,34 +41,8 @@
"jsx-a11y/alt-text": 0, "jsx-a11y/alt-text": 0,
"func-names": 0, "func-names": 0,
"object-shorthand": 0, "object-shorthand": 0,
"no-bitwise": 0,
"arrow-body-style": 0,
"prefer-template": 0,
"default-case": 0,
"array-callback-return": 0,
"consistent-return": 0,
"import/extensions": 0,
"import/no-extraneous-dependencies": 0,
"camelcase": 0,
"eqeqeq": 0,
"no-lonely-if": 0,
"space-unary-ops": 0, "space-unary-ops": 0,
"no-extra-bind": 0,
"class-methods-use-this": 0,
"no-case-declarations": 0,
"no-else-return": 0,
"max-len": 0,
"no-useless-concat": 0,
"no-sequences": 0,
"no-extra-boolean-cast": 0,
"dot-notation": 0,
"no-shadow": 0,
"no-alert": 0,
"no-script-url": 0,
"wrap-iife": 0,
"space-infix-ops": 0, "space-infix-ops": 0,
"no-irregular-whitespace": 0, "no-irregular-whitespace": 0
"padded-blocks": 0,
"no-underscore-dangle": 0
} }
} }

View File

@ -153,17 +153,30 @@ class RoboFile extends \Robo\Tasks {
return $this->_exec('./tasks/transifex_init.sh'); return $this->_exec('./tasks/transifex_init.sh');
} }
function testUnit($opts=['file' => null, 'xml' => false]) { function testUnit($opts=['file' => null, 'xml' => false, 'multisite' => false]) {
$this->loadEnv(); $this->loadEnv();
$command = 'vendor/bin/codecept run unit -c codeception.unit.yml -f '.(($opts['file']) ? $opts['file'] : ''); $command = 'vendor/bin/codecept run unit -c codeception.unit.yml';
if($opts['multisite']) {
$command = 'MULTISITE=true ' . $command;
}
if($opts['file']) {
$command .= ' -f ' . $opts['file'];
}
if($opts['xml']) { if($opts['xml']) {
$command .= ' --xml'; $command .= ' --xml';
} }
return $this->_exec($command); return $this->_exec($command);
} }
function testMultisiteUnit($opts=['file' => null, 'xml' => false, 'multisite' => true]) {
return $this->testUnit($opts);
}
function testCoverage($opts=['file' => null, 'xml' => false]) { function testCoverage($opts=['file' => null, 'xml' => false]) {
$this->loadEnv(); $this->loadEnv();
$command = join(' ', array( $command = join(' ', array(
@ -198,6 +211,10 @@ class RoboFile extends \Robo\Tasks {
return $this->_exec($command); return $this->_exec($command);
} }
function securityComposer() {
return $this->_exec('vendor/bin/security-checker security:check --format=simple');
}
function testDebug($opts=['file' => null, 'xml' => false]) { function testDebug($opts=['file' => null, 'xml' => false]) {
$this->loadEnv(); $this->loadEnv();
$this->_exec('vendor/bin/codecept build -c codeception.unit.yml'); $this->_exec('vendor/bin/codecept build -c codeception.unit.yml');

View File

@ -27,3 +27,5 @@
@require 'pages_custom' @require 'pages_custom'
@require 'mp2migrator' @require 'mp2migrator'
@require '../../../node_modules/react-confirm-alert/src/react-confirm-alert.css'

View File

@ -77,7 +77,7 @@ define('date',
var escapeToken; var escapeToken;
var index; var index;
var token; var token;
var format_mappings = { var formatMappings = {
date: { date: {
d: 'DD', d: 'DD',
D: 'ddd', D: 'ddd',
@ -146,7 +146,7 @@ define('date',
if (!format || format.length <= 0) return format; if (!format || format.length <= 0) return format;
replacements = format_mappings['date']; replacements = formatMappings['date'];
convertedFormat = []; convertedFormat = [];
escapeToken = false; escapeToken = false;

View File

@ -16,25 +16,23 @@ define([
// isChecked will be true only if the value is "1" // isChecked will be true only if the value is "1"
// it will be false in case value is "0" or empty // it will be false in case value is "0" or empty
const isChecked = !!(~~(this.props.item[this.props.field.name])); const isChecked = !!(Number(this.props.item[this.props.field.name]));
const options = Object.keys(this.props.field.values).map( const options = Object.keys(this.props.field.values).map(
(value, index) => { (value, index) => (
return ( <p key={`checkbox-${index}`}>
<p key={'checkbox-' + index}> <label>
<label> <input
<input ref="checkbox"
ref="checkbox" type="checkbox"
type="checkbox" value="1"
value="1" checked={isChecked}
checked={isChecked} onChange={this.onValueChange}
onChange={this.onValueChange} name={this.props.field.name}
name={this.props.field.name}
/> />
{ this.props.field.values[value] } { this.props.field.values[value] }
</label> </label>
</p> </p>
); )
}
); );
return ( return (

View File

@ -167,6 +167,11 @@ define([
year: this.state.year, year: this.state.year,
}; };
break; break;
default:
value = {
value: 'invalid type',
};
break;
} }
return value; return value;
@ -181,7 +186,7 @@ define([
field = matches[1]; field = matches[1];
property = matches[2]; property = matches[2];
const value = ~~(e.target.value); const value = Number(e.target.value);
this.setState({ this.setState({
[`${property}`]: value, [`${property}`]: value,
@ -233,6 +238,9 @@ define([
day={this.state.day} day={this.state.day}
placeholder={this.props.field.day_placeholder} placeholder={this.props.field.day_placeholder}
/>); />);
default:
return <div>Invalid date type</div>;
} }
}); });

View File

@ -32,7 +32,7 @@ define([
let field = false; let field = false;
let dataField = data.field; let dataField = data.field;
if (data.field['field'] !== undefined) { if (data.field.field !== undefined) {
dataField = jQuery.merge(dataField, data.field.field); dataField = jQuery.merge(dataField, data.field.field);
} }
@ -68,36 +68,37 @@ define([
case 'reactComponent': case 'reactComponent':
field = (<data.field.component {...data} />); field = (<data.field.component {...data} />);
break; break;
default:
field = 'invalid';
break;
} }
if (inline === true) { if (inline === true) {
return ( return (
<span key={'field-' + (data.index || 0)}> <span key={`field-${data.index || 0}`}>
{ field } { field }
{ description } { description }
</span> </span>
); );
} else {
return (
<div key={'field-' + (data.index || 0)}>
{ field }
{ description }
</div>
);
} }
return (
<div key={`field-${data.index || 0}`}>
{ field }
{ description }
</div>
);
}, },
render: function () { render: function () {
let field = false; let field = false;
if (this.props.field['fields'] !== undefined) { if (this.props.field.fields !== undefined) {
field = this.props.field.fields.map((subfield, index) => { field = this.props.field.fields.map((subfield, index) => this.renderField({
return this.renderField({ index: index,
index: index, field: subfield,
field: subfield, item: this.props.item,
item: this.props.item, onValueChange: this.props.onValueChange || false,
onValueChange: this.props.onValueChange || false, }));
});
});
} else { } else {
field = this.renderField(this.props); field = this.renderField(this.props);
} }
@ -110,10 +111,10 @@ define([
} }
return ( return (
<tr> <tr className={`form-field-row-${this.props.field.name}`}>
<th scope="row"> <th scope="row">
<label <label
htmlFor={'field_' + this.props.field.name} htmlFor={`field_${this.props.field.name}`}
> >
{ this.props.field.label } { this.props.field.label }
{ tip } { tip }

View File

@ -10,23 +10,21 @@ define([
return false; return false;
} }
const selected_value = this.props.item[this.props.field.name]; const selectedValue = this.props.item[this.props.field.name];
const options = Object.keys(this.props.field.values).map( const options = Object.keys(this.props.field.values).map(
(value, index) => { (value, index) => (
return ( <p key={`radio-${index}`}>
<p key={'radio-' + index}> <label>
<label> <input
<input type="radio"
type="radio" checked={selectedValue === value}
checked={selected_value === value} value={value}
value={value} onChange={this.props.onValueChange}
onChange={this.props.onValueChange} name={this.props.field.name} />
name={this.props.field.name} /> { this.props.field.values[value] }
{ this.props.field.values[value] } </label>
</label> </p>
</p> )
);
}
); );
return ( return (

View File

@ -17,7 +17,7 @@ const FormFieldSelect = React.createClass({
); );
} }
if (this.props.field['filter'] !== undefined) { if (this.props.field.filter !== undefined) {
filter = this.props.field.filter; filter = this.props.field.filter;
} }
@ -41,27 +41,25 @@ const FormFieldSelect = React.createClass({
keys = Object.keys(this.props.field.values); keys = Object.keys(this.props.field.values);
} }
const options = keys.map( const options = keys
(value, index) => { .filter((value) => {
if (filter === false) return true;
if (filter !== false && filter(this.props.item, value) === false) { return filter(this.props.item, value);
return; })
} .map(
(value, index) => (
return (
<option <option
key={'option-' + index} key={`option-${index}`}
value={value}> value={value}>
{ this.props.field.values[value] } { this.props.field.values[value] }
</option> </option>
); )
} );
);
return ( return (
<select <select
name={this.props.field.name} name={this.props.field.name}
id={'field_' + this.props.field.name} id={`field_${this.props.field.name}`}
value={this.props.item[this.props.field.name]} value={this.props.item[this.props.field.name]}
onChange={this.props.onValueChange} onChange={this.props.onValueChange}
{...this.props.field.validation} {...this.props.field.validation}

View File

@ -26,7 +26,7 @@ define([
return (this.state.select2 === true); return (this.state.select2 === true);
}, },
componentDidMount: function () { componentDidMount: function () {
if (this.allowMultipleValues()) { if (this.allowMultipleValues() || this.props.field.forceSelect2) {
this.setupSelect2(); this.setupSelect2();
} }
}, },
@ -35,19 +35,19 @@ define([
(this.props.item !== undefined && prevProps.item !== undefined) (this.props.item !== undefined && prevProps.item !== undefined)
&& (this.props.item.id !== prevProps.item.id) && (this.props.item.id !== prevProps.item.id)
) { ) {
jQuery('#' + this.refs.select.id) jQuery(`#${this.refs.select.id}`)
.val(this.getSelectedValues()) .val(this.getSelectedValues())
.trigger('change'); .trigger('change');
} }
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
if (this.allowMultipleValues()) { if (this.allowMultipleValues() || this.props.field.forceSelect2) {
this.destroySelect2(); this.destroySelect2();
} }
}, },
destroySelect2: function () { destroySelect2: function () {
if (this.isSelect2Initialized()) { if (this.isSelect2Initialized()) {
jQuery('#' + this.refs.select.id).select2('destroy'); jQuery(`#${this.refs.select.id}`).select2('destroy');
} }
}, },
setupSelect2: function () { setupSelect2: function () {
@ -55,18 +55,15 @@ define([
return; return;
} }
const select2 = jQuery('#' + this.refs.select.id).select2({ const select2 = jQuery(`#${this.refs.select.id}`).select2({
width: (this.props.width || ''), width: (this.props.width || ''),
templateResult: function (item) { templateResult: function (item) {
if (item.element && item.element.selected) { if (item.element && item.element.selected) {
return null; return null;
} else { } else if (item.title) {
if (item.title) { return item.title;
return item.title;
} else {
return item.text;
}
} }
return item.text;
}, },
}); });
@ -86,14 +83,12 @@ define([
this.setState({ select2: true }); this.setState({ select2: true });
}, },
getSelectedValues: function () { getSelectedValues: function () {
if (this.props.field['selected'] !== undefined) { if (this.props.field.selected !== undefined) {
return this.props.field['selected'](this.props.item); return this.props.field.selected(this.props.item);
} else if (this.props.item !== undefined && this.props.field.name !== undefined) { } else if (this.props.item !== undefined && this.props.field.name !== undefined) {
if (this.allowMultipleValues()) { if (this.allowMultipleValues()) {
if (Array.isArray(this.props.item[this.props.field.name])) { if (Array.isArray(this.props.item[this.props.field.name])) {
return this.props.item[this.props.field.name].map((item) => { return this.props.item[this.props.field.name].map(item => item.id);
return item.id;
});
} }
} else { } else {
return this.props.item[this.props.field.name]; return this.props.item[this.props.field.name];
@ -102,11 +97,15 @@ define([
return null; return null;
}, },
loadCachedItems: function () { loadCachedItems: function () {
if (typeof (window['mailpoet_' + this.props.field.endpoint]) !== 'undefined') { let items;
let items = window['mailpoet_' + this.props.field.endpoint]; if (typeof (window[`mailpoet_${this.props.field.endpoint}`]) !== 'undefined') {
items = window[`mailpoet_${this.props.field.endpoint}`];
} else if (this.props.field.values !== undefined) {
items = this.props.field.values;
}
if (Array.isArray(items)) {
if (this.props.field['filter'] !== undefined) { if (this.props.field.filter !== undefined) {
items = items.filter(this.props.field.filter); items = items.filter(this.props.field.filter);
} }
@ -119,7 +118,7 @@ define([
let value; let value;
if (this.props.onValueChange !== undefined) { if (this.props.onValueChange !== undefined) {
if (this.props.field.multiple) { if (this.props.field.multiple) {
value = jQuery('#' + this.refs.select.id).val(); value = jQuery(`#${this.refs.select.id}`).val();
} else { } else {
value = e.target.value; value = e.target.value;
} }
@ -133,19 +132,19 @@ define([
} }
}, },
getLabel: function (item) { getLabel: function (item) {
if (this.props.field['getLabel'] !== undefined) { if (this.props.field.getLabel !== undefined) {
return this.props.field.getLabel(item, this.props.item); return this.props.field.getLabel(item, this.props.item);
} }
return item.name; return item.name;
}, },
getSearchLabel: function (item) { getSearchLabel: function (item) {
if (this.props.field['getSearchLabel'] !== undefined) { if (this.props.field.getSearchLabel !== undefined) {
return this.props.field.getSearchLabel(item, this.props.item); return this.props.field.getSearchLabel(item, this.props.item);
} }
return null; return null;
}, },
getValue: function (item) { getValue: function (item) {
if (this.props.field['getValue'] !== undefined) { if (this.props.field.getValue !== undefined) {
return this.props.field.getValue(item, this.props.item); return this.props.field.getValue(item, this.props.item);
} }
return item.id; return item.id;
@ -154,11 +153,18 @@ define([
// this function may be used to transform the placeholder value into // this function may be used to transform the placeholder value into
// desired value. // desired value.
transformChangedValue: function (value) { transformChangedValue: function (value) {
if (typeof this.props.field['transformChangedValue'] === 'function') { if (typeof this.props.field.transformChangedValue === 'function') {
return this.props.field.transformChangedValue.call(this, value); return this.props.field.transformChangedValue.call(this, value);
} else {
return value;
} }
return value;
},
insertEmptyOption: function () {
// https://select2.org/placeholders
// For single selects only, in order for the placeholder value to appear,
// we must have a blank <option> as the first option in the <select> control.
if (this.allowMultipleValues()) return undefined;
if (this.props.field.placeholder) return (<option />);
return undefined;
}, },
render: function () { render: function () {
const options = this.state.items.map((item, index) => { const options = this.state.items.map((item, index) => {
@ -168,7 +174,7 @@ define([
return ( return (
<option <option
key={'option-' + index} key={`option-${index}`}
value={value} value={value}
title={searchLabel} title={searchLabel}
> >
@ -186,7 +192,10 @@ define([
multiple={this.props.field.multiple} multiple={this.props.field.multiple}
defaultValue={this.getSelectedValues()} defaultValue={this.getSelectedValues()}
{...this.props.field.validation} {...this.props.field.validation}
>{ options }</select> >
{ this.insertEmptyOption() }
{ options }
</select>
); );
}, },
}); });

View File

@ -11,7 +11,7 @@ const FormFieldText = React.createClass({
<input <input
type="text" type="text"
disabled={ disabled={
(this.props.field['disabled'] !== undefined) (this.props.field.disabled !== undefined)
? this.props.field.disabled(this.props.item) ? this.props.field.disabled(this.props.item)
: false : false
} }
@ -22,7 +22,7 @@ const FormFieldText = React.createClass({
: false : false
} }
name={this.props.field.name} name={this.props.field.name}
id={'field_' + this.props.field.name} id={`field_${this.props.field.name}`}
value={value} value={value}
placeholder={this.props.field.placeholder} placeholder={this.props.field.placeholder}
onChange={this.props.onValueChange} onChange={this.props.onValueChange}

View File

@ -11,7 +11,7 @@ define([
type="text" type="text"
className="regular-text" className="regular-text"
name={this.props.field.name} name={this.props.field.name}
id={'field_' + this.props.field.name} id={`field_${this.props.field.name}`}
value={this.props.item[this.props.field.name]} value={this.props.item[this.props.field.name]}
placeholder={this.props.field.placeholder} placeholder={this.props.field.placeholder}
defaultValue={this.props.field.defaultValue} defaultValue={this.props.field.defaultValue}

View File

@ -15,7 +15,6 @@ define(
FormField, FormField,
jQuery jQuery
) => { ) => {
const Form = React.createClass({ const Form = React.createClass({
contextTypes: { contextTypes: {
router: React.PropTypes.object.isRequired, router: React.PropTypes.object.isRequired,
@ -58,8 +57,6 @@ define(
if (props.item === undefined) { if (props.item === undefined) {
this.refs.form.reset(); this.refs.form.reset();
} }
} else {
this.loadItem(props.params.id);
} }
}, },
loadItem: function (id) { loadItem: function (id) {
@ -77,6 +74,9 @@ define(
loading: false, loading: false,
item: response.data, item: response.data,
}); });
if (typeof this.props.onItemLoad === 'function') {
this.props.onItemLoad(response.data);
}
}).fail(() => { }).fail(() => {
this.setState({ this.setState({
loading: false, loading: false,
@ -100,9 +100,9 @@ define(
// only get values from displayed fields // only get values from displayed fields
const item = {}; const item = {};
this.props.fields.map((field) => { this.props.fields.forEach((field) => {
if (field['fields'] !== undefined) { if (field.fields !== undefined) {
field.fields.map((subfield) => { field.fields.forEach((subfield) => {
item[subfield.name] = this.state.item[subfield.name]; item[subfield.name] = this.state.item[subfield.name];
}); });
} else { } else {
@ -142,28 +142,25 @@ define(
handleValueChange: function (e) { handleValueChange: function (e) {
if (this.props.onChange) { if (this.props.onChange) {
return this.props.onChange(e); return this.props.onChange(e);
} else {
const item = this.state.item;
const field = e.target.name;
item[field] = e.target.value;
this.setState({
item: item,
});
return true;
} }
const item = this.state.item;
const field = e.target.name;
item[field] = e.target.value;
this.setState({
item: item,
});
return true;
}, },
render: function () { render: function () {
let errors; let errors;
if (this.getErrors() !== undefined) { if (this.getErrors() !== undefined) {
errors = this.getErrors().map((error, index) => { errors = this.getErrors().map((error, index) => (
return ( <p key={`error-${index}`} className="mailpoet_error">
<p key={'error-' + index} className="mailpoet_error"> { error.message }
{ error.message } </p>
</p> ));
);
});
} }
const formClasses = classNames( const formClasses = classNames(
@ -197,7 +194,7 @@ define(
field={field} field={field}
item={this.getValues()} item={this.getValues()}
onValueChange={onValueChange} onValueChange={onValueChange}
key={'field-' + i} /> key={`field-${i}`} />
); );
}); });

View File

@ -11,6 +11,27 @@ var Observable;
var WysijaHistory; var WysijaHistory;
var WysijaForm; var WysijaForm;
/* LOGGING */
function info(value) {
if (WysijaForm.options.debug === false) return;
if (!(window.console && console.log)) {
(function () {
var noop = function () {};
var methods = ['assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'markTimeline', 'table', 'time', 'timeEnd', 'timeStamp', 'trace', 'warn'];
var length = methods.length;
var console = {};
window.console = {};
while (length--) {
console[methods[length]] = noop;
}
}());
}
try {
console.log('[DEBUG] ' + value);
} catch (e) {}
}
Event.cacheDelegated = {}; Event.cacheDelegated = {};
Object.extend(document, (function () { Object.extend(document, (function () {
var cache = Event.cacheDelegated; var cache = Event.cacheDelegated;
@ -66,18 +87,18 @@ Object.extend(document, (function () {
var wrapper; var wrapper;
switch (length) { switch (length) {
case 2: case 2:
getWrappersForSelector(selector, eventName).each(function (wrapper) { getWrappersForSelector(selector, eventName).each(function (selectorWrapper) {
document.stopDelegating(selector, eventName, wrapper.handler); document.stopDelegating(selector, eventName, selectorWrapper.handler);
}); });
break; break;
case 1: case 1:
Object.keys(getCacheForSelector(selector)).each(function (eventName) { Object.keys(getCacheForSelector(selector)).each(function (event) {
document.stopDelegating(selector, eventName); document.stopDelegating(selector, event);
}); });
break; break;
case 0: case 0:
Object.keys(cache).each(function (selector) { Object.keys(cache).each(function (cacheSelector) {
document.stopDelegating(selector); document.stopDelegating(cacheSelector);
}); });
break; break;
default: default:
@ -96,6 +117,12 @@ Observable = (function () {
return name.underscore().split('_').join(':'); return name.underscore().split('_').join(':');
} }
function getWrapper(handler, klass) {
return function (event) {
return handler.call(new klass(this), event, event.memo);
};
}
function getHandlers(klass) { function getHandlers(klass) {
var proto = klass.prototype; var proto = klass.prototype;
var namespace = proto.namespace; var namespace = proto.namespace;
@ -106,12 +133,6 @@ Observable = (function () {
}); });
} }
function getWrapper(handler, klass) {
return function (event) {
return handler.call(new klass(this), event, event.memo);
};
}
function onDomLoad(selector, klass) { function onDomLoad(selector, klass) {
window.$$(selector).each(function (element) { window.$$(selector).each(function (element) {
new klass(element).onDomLoaded(); new klass(element).onDomLoaded();
@ -157,8 +178,8 @@ Object.extend(window.Droppables, {
var drop; var drop;
var affected = []; var affected = [];
if (!this.drops.length) return; if (!this.drops.length) return;
this.drops.each(function (drop) { this.drops.each(function (dropsDrop) {
if (window.Droppables.isAffected(point, element, drop)) affected.push(drop); if (window.Droppables.isAffected(point, element, dropsDrop)) affected.push(dropsDrop);
}); });
if (affected.length > 0) drop = window.Droppables.findDeepestChild(affected); if (affected.length > 0) drop = window.Droppables.findDeepestChild(affected);
if (this.last_active && this.last_active !== drop) this.deactivate(this.last_active, element); if (this.last_active && this.last_active !== drop) this.deactivate(this.last_active, element);
@ -285,8 +306,8 @@ WysijaForm = {
return str.replace(/&amp;/g, '&').replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&quot;/g, '"'); return str.replace(/&amp;/g, '&').replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&quot;/g, '"');
// ": fix for FileMerge because the previous line fucks up its syntax coloring // ": fix for FileMerge because the previous line fucks up its syntax coloring
}, },
loading: function (is_loading) { loading: function (isLoading) {
if (is_loading) { if (isLoading) {
window.$(WysijaForm.options.editor).addClassName('loading'); window.$(WysijaForm.options.editor).addClassName('loading');
window.$(WysijaForm.options.toolbar).addClassName('loading'); window.$(WysijaForm.options.toolbar).addClassName('loading');
} else { } else {
@ -301,7 +322,7 @@ WysijaForm = {
}); });
}, },
load: function (data) { load: function (data) {
var settings_elements; var settingsElements;
if (data === undefined) return; if (data === undefined) return;
// load body // load body
@ -312,8 +333,8 @@ WysijaForm = {
}); });
// load settings // load settings
settings_elements = window.$('mailpoet_form_settings').getElements(); settingsElements = window.$('mailpoet_form_settings').getElements();
settings_elements.each(function (setting) { settingsElements.each(function (setting) {
// skip lists // skip lists
if (setting.name === 'segments') { if (setting.name === 'segments') {
return true; return true;
@ -330,6 +351,7 @@ WysijaForm = {
setting.setValue(data.settings[setting.name]); setting.setValue(data.settings[setting.name]);
} }
} }
return true;
}); });
} }
}, },
@ -343,17 +365,17 @@ WysijaForm = {
}; };
// body // body
WysijaForm.getBlocks().each(function (b) { WysijaForm.getBlocks().each(function (b) {
var block_data = (typeof (b.block['save']) === 'function') ? b.block.save() : null; var blockData = (typeof (b.block['save']) === 'function') ? b.block.save() : null;
if (block_data !== null) { if (blockData !== null) {
// set block position // set block position
block_data['position'] = position; blockData['position'] = position;
// increment position // increment position
position++; position++;
// add block data to body // add block data to body
data['body'].push(block_data); data['body'].push(blockData);
} }
}); });
@ -436,10 +458,10 @@ WysijaForm = {
return (window.$$('#' + WysijaForm.options.editor + ' [wysija_id="segments"]').length > 0); return (window.$$('#' + WysijaForm.options.editor + ' [wysija_id="segments"]').length > 0);
}, },
isSegmentSelectionValid: function () { isSegmentSelectionValid: function () {
var segment_selection = window.$$('#' + WysijaForm.options.editor + ' [wysija_id="segments"]')[0]; var segmentSelection = window.$$('#' + WysijaForm.options.editor + ' [wysija_id="segments"]')[0];
var block; var block;
if (segment_selection !== undefined) { if (segmentSelection !== undefined) {
block = WysijaForm.get(segment_selection).block.getData(); block = WysijaForm.get(segmentSelection).block.getData();
return ( return (
(block.params.values !== undefined) (block.params.values !== undefined)
&& &&
@ -450,8 +472,8 @@ WysijaForm = {
}, },
setBlockPositions: function (event, target) { setBlockPositions: function (event, target) {
var index = 1; var index = 1;
var block_placeholder; var blockPlaceholder;
var previous_placeholder; var previousPlaceholder;
// release dragging lock // release dragging lock
WysijaForm.locks.dragging = false; WysijaForm.locks.dragging = false;
@ -467,19 +489,19 @@ WysijaForm = {
if (target !== undefined) { if (target !== undefined) {
// get placeholders (previous placeholder matches the placeholder linked to the next block) // get placeholders (previous placeholder matches the placeholder linked to the next block)
block_placeholder = window.$(target.element.readAttribute('wysija_placeholder')); blockPlaceholder = window.$(target.element.readAttribute('wysija_placeholder'));
previous_placeholder = target.element.previous('.block_placeholder'); previousPlaceholder = target.element.previous('.block_placeholder');
if (block_placeholder !== null) { if (blockPlaceholder !== null) {
// put block placeholder before the current block // put block placeholder before the current block
target.element.insert({ target.element.insert({
before: block_placeholder before: blockPlaceholder
}); });
// if the next block is a wysija_block, insert previous placeholder // if the next block is a wysija_block, insert previous placeholder
if (target.element.next() !== undefined && target.element.next().hasClassName('mailpoet_form_block') && previous_placeholder !== undefined) { if (target.element.next() !== undefined && target.element.next().hasClassName('mailpoet_form_block') && previousPlaceholder !== undefined) {
target.element.insert({ target.element.insert({
after: previous_placeholder after: previousPlaceholder
}); });
} }
} }
@ -499,11 +521,11 @@ WysijaForm = {
// get parent dimensions and position // get parent dimensions and position
var parentDim = element.up('.mailpoet_form_block').getDimensions(); var parentDim = element.up('.mailpoet_form_block').getDimensions();
var parentPos = element.up('.mailpoet_form_block').cumulativeOffset(); var parentPos = element.up('.mailpoet_form_block').cumulativeOffset();
var is_visible = (parentPos.top <= (WysijaForm.scroll.top + viewportHeight)); var isVisible = (parentPos.top <= (WysijaForm.scroll.top + viewportHeight));
var buttonMargin = 5; var buttonMargin = 5;
var relativeTop = buttonMargin; var relativeTop = buttonMargin;
if (is_visible) { if (isVisible) {
// always center // always center
relativeTop = parseInt((parentDim.height / 2) - (element.getHeight() / 2), 10); relativeTop = parseInt((parentDim.height / 2) - (element.getHeight() / 2), 10);
} }
@ -524,7 +546,6 @@ WysijaForm = {
if (WysijaForm.toolbar.left === null) WysijaForm.toolbar.left = parseInt(window.$(WysijaForm.options.container).positionedOffset().left); if (WysijaForm.toolbar.left === null) WysijaForm.toolbar.left = parseInt(window.$(WysijaForm.options.container).positionedOffset().left);
} }
if (WysijaForm.toolbar.x === null) WysijaForm.toolbar.x = parseInt(WysijaForm.toolbar.left + window.$(WysijaForm.options.container).getDimensions().width + 15); if (WysijaForm.toolbar.x === null) WysijaForm.toolbar.x = parseInt(WysijaForm.toolbar.left + window.$(WysijaForm.options.container).getDimensions().width + 15);
}, },
setToolbarPosition: function () { setToolbarPosition: function () {
var position; var position;
@ -582,10 +603,8 @@ WysijaForm = {
}, },
hideControls: function () { hideControls: function () {
try { try {
return WysijaForm.getBlocks().invoke('hideControls'); WysijaForm.getBlocks().invoke('hideControls');
} catch (e) { } catch (e) {}
return;
}
}, },
hideTools: function () { hideTools: function () {
window.$$('.wysija_tools').invoke('hide'); window.$$('.wysija_tools').invoke('hide');
@ -657,6 +676,7 @@ WysijaForm = {
// this is a url, so do not encode the protocol // this is a url, so do not encode the protocol
return encodeURI(str).replace(/[!'()*]/g, escape); return encodeURI(str).replace(/[!'()*]/g, escape);
} }
return str;
}, },
updateBlock: function (field) { updateBlock: function (field) {
var hasUpdated = false; var hasUpdated = false;
@ -778,29 +798,28 @@ WysijaForm.Block = window.Class.create({
} }
}, },
makeBlockDroppable: function () { makeBlockDroppable: function () {
var block_placeholder; var blockPlaceholder;
if (this.isBlockDroppableEnabled() === false) { if (this.isBlockDroppableEnabled() === false) {
block_placeholder = this.getBlockDroppable(); blockPlaceholder = this.getBlockDroppable();
window.Droppables.add(block_placeholder.identify(), WysijaForm.blockDropOptions); window.Droppables.add(blockPlaceholder.identify(), WysijaForm.blockDropOptions);
block_placeholder.addClassName('enabled'); blockPlaceholder.addClassName('enabled');
} }
}, },
removeBlockDroppable: function () { removeBlockDroppable: function () {
var block_placeholder; var blockPlaceholder;
if (this.isBlockDroppableEnabled()) { if (this.isBlockDroppableEnabled()) {
block_placeholder = this.getBlockDroppable(); blockPlaceholder = this.getBlockDroppable();
window.Droppables.remove(block_placeholder.identify()); window.Droppables.remove(blockPlaceholder.identify());
block_placeholder.removeClassName('enabled'); blockPlaceholder.removeClassName('enabled');
} }
}, },
isBlockDroppableEnabled: function () { isBlockDroppableEnabled: function () {
// if the block_placeholder does not exist, create it // if the block_placeholder does not exist, create it
var block_placeholder = this.getBlockDroppable(); var blockPlaceholder = this.getBlockDroppable();
if (block_placeholder === null) { if (blockPlaceholder === null) {
return this.createBlockDroppable().hasClassName('enabled'); return this.createBlockDroppable().hasClassName('enabled');
} else {
return block_placeholder.hasClassName('enabled');
} }
return blockPlaceholder.hasClassName('enabled');
}, },
createBlockDroppable: function () { createBlockDroppable: function () {
info('block -> createBlockDroppable'); info('block -> createBlockDroppable');
@ -812,9 +831,8 @@ WysijaForm.Block = window.Class.create({
getBlockDroppable: function () { getBlockDroppable: function () {
if (this.element.previous() === undefined || this.element.previous().hasClassName('block_placeholder') === false) { if (this.element.previous() === undefined || this.element.previous().hasClassName('block_placeholder') === false) {
return null; return null;
} else {
return this.element.previous();
} }
return this.element.previous();
}, },
getControls: function () { getControls: function () {
return this.element.down('.wysija_controls'); return this.element.down('.wysija_controls');
@ -919,25 +937,25 @@ WysijaForm.Block = window.Class.create({
WysijaForm.Block.create = function (createBlock, target) { WysijaForm.Block.create = function (createBlock, target) {
var block = createBlock; var block = createBlock;
var body; var body;
var block_template; var blockTemplate;
var template; var template;
var output; var output;
var settings_segments; var settingsSegments;
if (window.$('form_template_' + block.type) === null) { if (window.$('form_template_' + block.type) === null) {
return false; return false;
} }
body = window.$(WysijaForm.options.body); body = window.$(WysijaForm.options.body);
block_template = window.Handlebars.compile(window.$('form_template_block').innerHTML); blockTemplate = window.Handlebars.compile(window.$('form_template_block').innerHTML);
template = window.Handlebars.compile(window.$('form_template_' + block.type).innerHTML); template = window.Handlebars.compile(window.$('form_template_' + block.type).innerHTML);
output = ''; output = '';
if (block.type === 'segment') { if (block.type === 'segment') {
if (block.params.values === undefined) { if (block.params.values === undefined) {
settings_segments = window.jQuery('#mailpoet_form_segments').val(); settingsSegments = window.jQuery('#mailpoet_form_segments').val();
if (settings_segments !== null && settings_segments.length > 0) { if (settingsSegments !== null && settingsSegments.length > 0) {
block.params.values = window.mailpoet_segments.filter(function (segment) { block.params.values = window.mailpoet_segments.filter(function (segment) {
return (settings_segments.indexOf(segment.id) !== -1); return (settingsSegments.indexOf(segment.id) !== -1);
}); });
} }
} }
@ -945,7 +963,7 @@ WysijaForm.Block.create = function (createBlock, target) {
// set block template (depending on the block type) // set block template (depending on the block type)
block.template = template(block); block.template = template(block);
output = block_template(block); output = blockTemplate(block);
// check if the new block is unique and if there's already an instance // check if the new block is unique and if there's already an instance
// of it in the history. If so, remove its former instance from the history // of it in the history. If so, remove its former instance from the history
@ -973,6 +991,7 @@ WysijaForm.Block.create = function (createBlock, target) {
// position settings // position settings
WysijaForm.setSettingsPosition(); WysijaForm.setSettingsPosition();
return true;
}; };
document.observe('wjfe:item:drop', function (event) { document.observe('wjfe:item:drop', function (event) {
@ -1011,11 +1030,11 @@ WysijaForm.Widget = window.Class.create(WysijaForm.Block, {
return data; return data;
}, },
setData: function (data) { setData: function (data) {
var current_data = this.getData(); var currentData = this.getData();
var params = window.$H(current_data.params).merge(data.params).toObject(); var params = window.$H(currentData.params).merge(data.params).toObject();
// update type if it changed // update type if it changed
if (data.type !== undefined && data.type !== current_data.type) { if (data.type !== undefined && data.type !== currentData.type) {
this.element.writeAttribute('wysija_type', data.type); this.element.writeAttribute('wysija_type', data.type);
} }
@ -1038,19 +1057,19 @@ WysijaForm.Widget = window.Class.create(WysijaForm.Block, {
}, },
redraw: function (data) { redraw: function (data) {
var options; var options;
var block_template; var blockTemplate;
var template; var template;
var params; var params;
// set parameters // set parameters
this.setData(data); this.setData(data);
options = this.getData(); options = this.getData();
// redraw block // redraw block
block_template = window.Handlebars.compile(window.$('form_template_block').innerHTML); blockTemplate = window.Handlebars.compile(window.$('form_template_block').innerHTML);
template = window.Handlebars.compile(window.$('form_template_' + options.type).innerHTML); template = window.Handlebars.compile(window.$('form_template_' + options.type).innerHTML);
params = window.$H(options).merge({ params = window.$H(options).merge({
template: template(options) template: template(options)
}).toObject(); }).toObject();
this.element.replace(block_template(params)); this.element.replace(blockTemplate(params));
WysijaForm.init(); WysijaForm.init();
}, },
@ -1073,25 +1092,4 @@ WysijaForm.Widget = window.Class.create(WysijaForm.Block, {
/* When dom is loaded, initialize WysijaForm */ /* When dom is loaded, initialize WysijaForm */
document.observe('dom:loaded', WysijaForm.init); document.observe('dom:loaded', WysijaForm.init);
/* LOGGING */
function info(value) {
if (WysijaForm.options.debug === false) return;
if (!(window.console && console.log)) {
(function () {
var noop = function () {};
var methods = ['assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'markTimeline', 'table', 'time', 'timeEnd', 'timeStamp', 'trace', 'warn'];
var length = methods.length;
var console = {};
window.console = {};
while (length--) {
console[methods[length]] = noop;
}
}());
}
try {
console.log('[DEBUG] ' + value);
} catch (e) {}
}
module.exports = WysijaForm; module.exports = WysijaForm;

View File

@ -27,7 +27,7 @@ const columns = [
const messages = { const messages = {
onTrash: (response) => { onTrash: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -42,7 +42,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onDelete: (response) => { onDelete: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -57,7 +57,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onRestore: (response) => { onRestore: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -73,7 +73,7 @@ const messages = {
}, },
}; };
const bulk_actions = [ const bulkActions = [
{ {
name: 'trash', name: 'trash',
label: MailPoet.I18n.t('moveToTrash'), label: MailPoet.I18n.t('moveToTrash'),
@ -81,7 +81,7 @@ const bulk_actions = [
}, },
]; ];
const item_actions = [ const itemActions = [
{ {
name: 'edit', name: 'edit',
label: MailPoet.I18n.t('edit'), label: MailPoet.I18n.t('edit'),
@ -110,7 +110,7 @@ const item_actions = [
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -133,32 +133,31 @@ const FormList = React.createClass({
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
}); });
}, },
renderItem(form, actions) { renderItem(form, actions) {
const row_classes = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
'column-primary', 'column-primary',
'has-row-actions' 'has-row-actions'
); );
let segments = window.mailpoet_segments.filter((segment) => { let segments = window.mailpoet_segments
return (jQuery.inArray(segment.id, form.segments) !== -1); .filter(segment => (jQuery.inArray(segment.id, form.segments) !== -1))
}).map((segment) => { .map(segment => segment.name)
return segment.name; .join(', ');
}).join(', ');
if (form.settings.segments_selected_by === 'user') { if (form.settings.segments_selected_by === 'user') {
segments = MailPoet.I18n.t('userChoice') + ' ' + segments; segments = `${MailPoet.I18n.t('userChoice')} ${segments}`;
} }
return ( return (
<div> <div>
<td className={row_classes}> <td className={rowClasses}>
<strong> <strong>
<a <a
className="row-title" className="row-title"
@ -199,8 +198,8 @@ const FormList = React.createClass({
endpoint="forms" endpoint="forms"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
item_actions={item_actions} item_actions={itemActions}
/> />
</div> </div>
); );

View File

@ -17,7 +17,7 @@ define('handlebars_helpers', ['handlebars'], function (Handlebars) {
var f; var f;
if (window.moment) { if (window.moment) {
if (timestamp === undefined || isNaN(timestamp) || timestamp <= 0) { if (timestamp === undefined || isNaN(timestamp) || timestamp <= 0) {
return; return undefined;
} }
// set date format // set date format
@ -25,12 +25,10 @@ define('handlebars_helpers', ['handlebars'], function (Handlebars) {
// check if we passed a timestamp // check if we passed a timestamp
if (parseInt(timestamp, 10) == timestamp) { if (parseInt(timestamp, 10) == timestamp) {
return window.moment.unix(timestamp).format(f); return window.moment.unix(timestamp).format(f);
} else {
return window.moment.utc(timestamp).format(f);
} }
} else { return window.moment.utc(timestamp).format(f);
return timestamp;
} }
return timestamp;
}); });
Handlebars.registerHelper('cycle', function (value, block) { Handlebars.registerHelper('cycle', function (value, block) {
@ -87,9 +85,8 @@ define('handlebars_helpers', ['handlebars'], function (Handlebars) {
var mailtoMatchingRegex = /^mailto\:/i; var mailtoMatchingRegex = /^mailto\:/i;
if (typeof value === 'string' && value.match(mailtoMatchingRegex)) { if (typeof value === 'string' && value.match(mailtoMatchingRegex)) {
return value.replace(mailtoMatchingRegex, ''); return value.replace(mailtoMatchingRegex, '');
} else {
return value;
} }
return value;
}); });
Handlebars.registerHelper('lookup', function (obj, field) { Handlebars.registerHelper('lookup', function (obj, field) {
return obj && obj[field]; return obj && obj[field];
@ -134,9 +131,8 @@ define('handlebars_helpers', ['handlebars'], function (Handlebars) {
} }
if (sanitized.length > limit) { if (sanitized.length > limit) {
return sanitized.substr(0, limit - strAppend.length) + strAppend; return sanitized.substr(0, limit - strAppend.length) + strAppend;
} else {
return sanitized;
} }
return sanitized;
}); });
Handlebars.registerHelper('getNumber', function (string) { Handlebars.registerHelper('getNumber', function (string) {

View File

@ -6,7 +6,6 @@ define('helpTooltip', ['mailpoet', 'react', 'react-dom', 'help-tooltip.jsx'],
MailPoet.helpTooltip = { MailPoet.helpTooltip = {
show: function (domContainerNode, opts) { show: function (domContainerNode, opts) {
ReactDOM.render(React.createElement( ReactDOM.render(React.createElement(
TooltipComponent, { TooltipComponent, {
tooltip: opts.tooltip, tooltip: opts.tooltip,
@ -16,7 +15,6 @@ define('helpTooltip', ['mailpoet', 'react', 'react-dom', 'help-tooltip.jsx'],
), domContainerNode); ), domContainerNode);
} }
}; };
} }
); );

View File

@ -3,8 +3,9 @@ import ReactDOM from 'react-dom';
import { Router, Route, IndexRedirect, useRouterHistory } from 'react-router'; import { Router, Route, IndexRedirect, useRouterHistory } from 'react-router';
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import KnowledgeBase from 'help/knowledge_base.jsx'; import SystemStatus from 'help/system_status.jsx';
import SystemInfo from 'help/system_info.jsx'; import SystemInfo from 'help/system_info.jsx';
import KnowledgeBase from 'help/knowledge_base.jsx';
const history = useRouterHistory(createHashHistory)({ queryKey: false }); const history = useRouterHistory(createHashHistory)({ queryKey: false });
@ -17,16 +18,15 @@ const App = React.createClass({
const container = document.getElementById('help_container'); const container = document.getElementById('help_container');
if (container) { if (container) {
ReactDOM.render(( ReactDOM.render((
<Router history={history}> <Router history={history}>
<Route path="/" component={App}> <Route path="/" component={App}>
<IndexRedirect to="knowledgeBase" /> <IndexRedirect to="systemStatus" />
{/* Pages */} {/* Pages */}
<Route path="knowledgeBase(/)**" params={{ tab: 'knowledgeBase' }} component={KnowledgeBase} /> <Route path="systemStatus(/)**" params={{ tab: 'systemStatus' }} component={SystemStatus} />
<Route path="systemInfo(/)**" params={{ tab: 'systemInfo' }} component={SystemInfo} /> <Route path="systemInfo(/)**" params={{ tab: 'systemInfo' }} component={SystemInfo} />
<Route path="knowledgeBase(/)**" params={{ tab: 'knowledgeBase' }} component={KnowledgeBase} />
</Route> </Route>
</Router> </Router>
), container); ), container);
} }

View File

@ -4,7 +4,6 @@ import MailPoet from 'mailpoet';
import Tabs from './tabs.jsx'; import Tabs from './tabs.jsx';
function KnowledgeBase() { function KnowledgeBase() {
return ( return (
<div> <div>

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import _ from 'underscore'; import _ from 'underscore';
import Tabs from './tabs.jsx'; import Tabs from './tabs.jsx';
function handleFocus(event) { function handleFocus(event) {
@ -10,9 +9,7 @@ function handleFocus(event) {
function printData(data) { function printData(data) {
if (_.isObject(data)) { if (_.isObject(data)) {
const printableData = Object.keys(data).map((key) => { const printableData = Object.keys(data).map(key => `${key}: ${data[key]}`);
return `${key}: ${data[key]}`;
});
return (<textarea return (<textarea
readOnly={true} readOnly={true}
@ -23,25 +20,24 @@ function printData(data) {
height: '400px', height: '400px',
}} }}
/>); />);
} else {
return (<p>{MailPoet.I18n.t('systemInfoDataError')}</p>);
} }
return (<p>{MailPoet.I18n.t('systemInfoDataError')}</p>);
} }
function KnowledgeBase() { function SystemInfo() {
const data = window.help_scout_data; const systemInfoData = window.systemInfoData;
return ( return (
<div> <div>
<Tabs tab="systemInfo" /> <Tabs tab="systemInfo" />
<div className="mailpoet_notice notice inline notice-success" style={{ marginTop: '1em' }}> <div className="mailpoet_notice notice inline" style={{ marginTop: '1em' }}>
<p>{MailPoet.I18n.t('systemInfoIntro')}</p> <p>{MailPoet.I18n.t('systemInfoIntro')}</p>
</div> </div>
{printData(data)} {printData(systemInfoData)}
</div> </div>
); );
} }
module.exports = KnowledgeBase; module.exports = SystemInfo;

View File

@ -0,0 +1,71 @@
import MailPoet from 'mailpoet';
import React from 'react';
import ReactStringReplace from 'react-string-replace';
import Tabs from './tabs.jsx';
function renderStatusMessage(status, error, link) {
const noticeType = (status) ? 'success' : 'error';
let noticeMessage = (status) ?
MailPoet.I18n.t('systemStatusConnectionSuccessful') :
`${MailPoet.I18n.t('systemStatusConnectionUnsuccessful')} ${error}`;
if (link) {
noticeMessage = ReactStringReplace(
noticeMessage,
/\[link\](.*?)\[\/link\]/g,
match => (
<a href={`${link}`} key="kb-link">{ match }</a>
)
);
}
return (
<div className={`mailpoet_notice notice inline notice-${noticeType}`} style={{ marginTop: '1em' }}>
<p>{noticeMessage}</p>
</div>
);
}
function renderCronSection(data) {
const status = data.cron.isReachable;
const url = data.cron.url;
return (
<div>
<h2>{MailPoet.I18n.t('systemStatusCronTitle')}</h2>
<p>
<a href={url} target="_blank">{url}</a>
</p>
{renderStatusMessage(status, MailPoet.I18n.t('systemStatusCronConnectionUnsuccessfulInfo'), '//beta.docs.mailpoet.com/article/231-sending-does-not-work')}
</div>
);
}
function renderMSSSection(data) {
if (!data.mss.enabled) return undefined;
const status = data.mss.enabled.isReachable;
return (
<div>
<h2>{MailPoet.I18n.t('systemStatusMSSTitle')}</h2>
{renderStatusMessage(status, MailPoet.I18n.t('systemStatusMSSConnectionUnsuccessfulInfo'), false)}
</div>
);
}
function SystemStatus() {
const systemStatusData = window.systemStatusData;
return (
<div>
<Tabs tab="systemStatus" />
<div className="mailpoet_notice notice inline" style={{ marginTop: '1em' }}>
<p>{systemStatusData.mss.enabled ? MailPoet.I18n.t('systemStatusIntroCronMSS') : MailPoet.I18n.t('systemStatusIntroCron')}</p>
</div>
{renderCronSection(systemStatusData)}
{renderMSSSection(systemStatusData)}
</div>
);
}
module.exports = SystemStatus;

View File

@ -5,19 +5,23 @@ import MailPoet from 'mailpoet';
const tabs = [ const tabs = [
{ {
name: 'knowledgeBase', name: 'systemStatus',
label: MailPoet.I18n.t('tabKnowledgeBaseTitle'), label: MailPoet.I18n.t('tabSystemStatusTitle'),
link: '/knowledgeBase', link: '/systemStatus',
}, },
{ {
name: 'systemInfo', name: 'systemInfo',
label: MailPoet.I18n.t('tabSystemInfoTitle'), label: MailPoet.I18n.t('tabSystemInfoTitle'),
link: '/systemInfo', link: '/systemInfo',
}, },
{
name: 'knowledgeBase',
label: MailPoet.I18n.t('tabKnowledgeBaseTitle'),
link: '/knowledgeBase',
},
]; ];
function Tabs(props) { function Tabs(props) {
const tabLinks = tabs.map((tab, index) => { const tabLinks = tabs.map((tab, index) => {
const tabClasses = classNames( const tabClasses = classNames(
'nav-tab', 'nav-tab',
@ -26,7 +30,7 @@ function Tabs(props) {
return ( return (
<Link <Link
key={'tab-' + index} key={`tab-${index}`}
className={tabClasses} className={tabClasses}
to={tab.link} to={tab.link}
>{ tab.label }</Link> >{ tab.label }</Link>
@ -41,6 +45,6 @@ function Tabs(props) {
} }
Tabs.propTypes = { tab: React.PropTypes.string }; Tabs.propTypes = { tab: React.PropTypes.string };
Tabs.defaultProps = { tab: 'knowledgeBase' }; Tabs.defaultProps = { tab: 'systemStatus' };
module.exports = Tabs; module.exports = Tabs;

View File

@ -21,5 +21,4 @@ define('i18n',
return translations; return translations;
} }
}; };
}); });

View File

@ -25,7 +25,7 @@ define(
*/ */
$.fn.mailpoetSerializeObject = function (coerce) { $.fn.mailpoetSerializeObject = function (coerce) {
var obj = {}; var obj = {};
var coerce_types = { true: !0, false: !1, null: null }; var coerceTypes = { true: !0, false: !1, null: null };
// Iterate over all name=value pairs. // Iterate over all name=value pairs.
$.each(this.serializeArray(), function (j, v) { $.each(this.serializeArray(), function (j, v) {
@ -37,33 +37,33 @@ define(
// If key is more complex than 'foo', like 'a[]' or 'a[b][c]', split it // If key is more complex than 'foo', like 'a[]' or 'a[b][c]', split it
// into its component parts. // into its component parts.
var keys = key.split(']['); var keys = key.split('][');
var keys_last = keys.length - 1; var keysLast = keys.length - 1;
// If the first keys part contains [ and the last ends with ], then [] // If the first keys part contains [ and the last ends with ], then []
// are correctly balanced. // are correctly balanced.
if (/\[/.test(keys[0]) && /\]$/.test(keys[keys_last])) { if (/\[/.test(keys[0]) && /\]$/.test(keys[keysLast])) {
// Remove the trailing ] from the last keys part. // Remove the trailing ] from the last keys part.
keys[keys_last] = keys[keys_last].replace(/\]$/, ''); keys[keysLast] = keys[keysLast].replace(/\]$/, '');
// Split first keys part into two parts on the [ and add them back onto // Split first keys part into two parts on the [ and add them back onto
// the beginning of the keys array. // the beginning of the keys array.
keys = keys.shift().split('[').concat(keys); keys = keys.shift().split('[').concat(keys);
keys_last = keys.length - 1; keysLast = keys.length - 1;
} else { } else {
// Basic 'foo' style key. // Basic 'foo' style key.
keys_last = 0; keysLast = 0;
} }
// Coerce values. // Coerce values.
if (coerce) { if (coerce) {
val = val && !isNaN(val) ? +val // number val = val && !isNaN(val) ? +val // number
: val === 'undefined' ? undefined // undefined : val === 'undefined' ? undefined // undefined
: coerce_types[val] !== undefined ? coerce_types[val] // true, false, null : coerceTypes[val] !== undefined ? coerceTypes[val] // true, false, null
: val; // string : val; // string
} }
if (keys_last) { if (keysLast) {
// Complex key, build deep object structure based on a few rules: // Complex key, build deep object structure based on a few rules:
// * The 'cur' pointer starts at the object top-level. // * The 'cur' pointer starts at the object top-level.
// * [] = array push (n is set to array length), [n] = array if n is // * [] = array push (n is set to array length), [n] = array if n is
@ -73,14 +73,13 @@ define(
// object or array based on the type of the next keys part. // object or array based on the type of the next keys part.
// * Move the 'cur' pointer to the next level. // * Move the 'cur' pointer to the next level.
// * Rinse & repeat. // * Rinse & repeat.
for (; i <= keys_last; i++) { for (; i <= keysLast; i++) {
key = keys[i] === '' ? cur.length : keys[i]; key = keys[i] === '' ? cur.length : keys[i];
cur[key] = i < keys_last cur[key] = i < keysLast
? cur[key] || (keys[i + 1] && isNaN(keys[i + 1]) ? {} : []) ? cur[key] || (keys[i + 1] && isNaN(keys[i + 1]) ? {} : [])
: val; : val;
cur = cur[key]; cur = cur[key];
} }
} else { } else {
// Simple key, even simpler rules, since only scalars and shallow // Simple key, even simpler rules, since only scalars and shallow
// arrays are allowed. // arrays are allowed.
@ -88,12 +87,10 @@ define(
if ($.isArray(obj[key])) { if ($.isArray(obj[key])) {
// val is already an array, so push on the next value. // val is already an array, so push on the next value.
obj[key].push(val); obj[key].push(val);
} else if (obj[key] !== undefined) { } else if (obj[key] !== undefined) {
// val isn't an array, but since a second value has been specified, // val isn't an array, but since a second value has been specified,
// convert val into an array. // convert val into an array.
obj[key] = [obj[key], val]; obj[key] = [obj[key], val];
} else { } else {
// val is a scalar. // val is a scalar.
obj[key] = val; obj[key] = val;

View File

@ -21,7 +21,7 @@ define([
const action = this.getSelectedAction(); const action = this.getSelectedAction();
// action on select callback // action on select callback
if (action !== null && action['onSelect'] !== undefined) { if (action !== null && action.onSelect !== undefined) {
this.setState({ this.setState({
extra: action.onSelect(e), extra: action.onSelect(e),
}); });
@ -37,23 +37,23 @@ define([
return; return;
} }
const selected_ids = (this.props.selection !== 'all') const selectedIds = (this.props.selection !== 'all')
? this.props.selected_ids ? this.props.selected_ids
: []; : [];
const data = (action['getData'] !== undefined) const data = (action.getData !== undefined)
? action.getData() ? action.getData()
: {}; : {};
data.action = this.state.action; data.action = this.state.action;
let onSuccess = function () {}; let onSuccess = function () {};
if (action['onSuccess'] !== undefined) { if (action.onSuccess !== undefined) {
onSuccess = action.onSuccess; onSuccess = action.onSuccess;
} }
if (data.action) { if (data.action) {
const promise = this.props.onBulkAction(selected_ids, data); const promise = this.props.onBulkAction(selectedIds, data);
if (promise !== false) { if (promise !== false) {
promise.then(onSuccess); promise.then(onSuccess);
} }
@ -65,11 +65,9 @@ define([
}); });
}, },
getSelectedAction: function () { getSelectedAction: function () {
const selected_action = this.refs.action.value; const selectedAction = this.refs.action.value;
if (selected_action.length > 0) { if (selectedAction.length > 0) {
const action = this.props.bulk_actions.filter((action) => { const action = this.props.bulk_actions.filter(act => (act.name === selectedAction));
return (action.name === selected_action);
});
if (action.length > 0) { if (action.length > 0) {
return action[0]; return action[0];
@ -97,14 +95,12 @@ define([
onChange={this.handleChangeAction} onChange={this.handleChangeAction}
> >
<option value="">{MailPoet.I18n.t('bulkActions')}</option> <option value="">{MailPoet.I18n.t('bulkActions')}</option>
{ this.props.bulk_actions.map((action, index) => { { this.props.bulk_actions.map((action, index) => (
return ( <option
<option value={action.name}
value={action.name} key={`action-${index}`}
key={'action-' + index}
>{ action.label }</option> >{ action.label }</option>
); )) }
}) }
</select> </select>
<input <input
onClick={this.handleApplyAction} onClick={this.handleApplyAction}

View File

@ -11,8 +11,8 @@ define([
const ListingFilters = React.createClass({ const ListingFilters = React.createClass({
handleFilterAction: function () { handleFilterAction: function () {
const filters = {}; const filters = {};
this.getAvailableFilters().map((filter, i) => { this.getAvailableFilters().forEach((filter, i) => {
filters[this.refs['filter-' + i].name] = this.refs['filter-' + i].value; filters[this.refs[`filter-${i}`].name] = this.refs[`filter-${i}`].value;
}); });
if (this.props.onBeforeSelectFilter) { if (this.props.onBeforeSelectFilter) {
this.props.onBeforeSelectFilter(filters); this.props.onBeforeSelectFilter(filters);
@ -24,23 +24,21 @@ define([
}, },
getAvailableFilters: function () { getAvailableFilters: function () {
const filters = this.props.filters; const filters = this.props.filters;
return Object.keys(filters).filter((filter) => { return Object.keys(filters).filter(filter => !(
return !(
filters[filter].length === 0 filters[filter].length === 0
|| ( || (
filters[filter].length === 1 filters[filter].length === 1
&& !filters[filter][0].value && !filters[filter][0].value
) )
); ));
});
}, },
componentDidUpdate: function () { componentDidUpdate: function () {
const selected_filters = this.props.filter; const selectedFilters = this.props.filter;
this.getAvailableFilters().map( this.getAvailableFilters().forEach(
(filter, i) => { (filter, i) => {
if (selected_filters[filter] !== undefined && selected_filters[filter]) { if (selectedFilters[filter] !== undefined && selectedFilters[filter]) {
jQuery(this.refs['filter-' + i]) jQuery(this.refs[`filter-${i}`])
.val(selected_filters[filter]) .val(selectedFilters[filter])
.trigger('change'); .trigger('change');
} }
} }
@ -48,29 +46,25 @@ define([
}, },
render: function () { render: function () {
const filters = this.props.filters; const filters = this.props.filters;
const available_filters = this.getAvailableFilters() const availableFilters = this.getAvailableFilters()
.map((filter, i) => { .map((filter, i) => (
return ( <select
<select ref={`filter-${i}`}
ref={`filter-${i}`} key={`filter-${i}`}
key={`filter-${i}`} name={filter}
name={filter}
> >
{ filters[filter].map((option, j) => { { filters[filter].map((option, j) => (
return ( <option
<option value={option.value}
value={option.value} key={`filter-option-${j}`}
key={'filter-option-' + j}
>{ option.label }</option> >{ option.label }</option>
); )) }
}) } </select>
</select> ));
);
});
let button; let button;
if (available_filters.length > 0) { if (availableFilters.length > 0) {
button = ( button = (
<input <input
id="post-query-submit" id="post-query-submit"
@ -81,9 +75,9 @@ define([
); );
} }
let empty_trash; let emptyTrash;
if (this.props.group === 'trash') { if (this.props.group === 'trash') {
empty_trash = ( emptyTrash = (
<input <input
onClick={this.handleEmptyTrash} onClick={this.handleEmptyTrash}
type="submit" type="submit"
@ -95,9 +89,9 @@ define([
return ( return (
<div className="alignleft actions actions"> <div className="alignleft actions actions">
{ available_filters } { availableFilters }
{ button } { button }
{ empty_trash } { emptyTrash }
</div> </div>
); );
}, },

View File

@ -1,5 +1,4 @@
define(['react', 'classnames'], (React, classNames) => { define(['react', 'classnames'], (React, classNames) => {
const ListingGroups = React.createClass({ const ListingGroups = React.createClass({
handleSelect: function (group) { handleSelect: function (group) {
return this.props.onSelectGroup(group); return this.props.onSelectGroup(group);
@ -21,7 +20,8 @@ define(['react', 'classnames'], (React, classNames) => {
href="javascript:;" href="javascript:;"
className={classes} className={classes}
onClick={this.handleSelect.bind(this, group.name)} > onClick={this.handleSelect.bind(this, group.name)} >
{group.label} <span className="count">({ group.count.toLocaleString() })</span> {group.label}
<span className="count">({ parseInt(group.count, 10).toLocaleString() })</span>
</a> </a>
</li> </li>
); );

View File

@ -19,7 +19,7 @@ const ListingHeader = React.createClass({
<ListingColumn <ListingColumn
onSort={this.props.onSort} onSort={this.props.onSort}
sort_by={this.props.sort_by} sort_by={this.props.sort_by}
key={'column-' + index} key={`column-${index}`}
column={renderColumn} /> column={renderColumn} />
); );
}); });
@ -54,9 +54,9 @@ const ListingHeader = React.createClass({
const ListingColumn = React.createClass({ const ListingColumn = React.createClass({
handleSort: function () { handleSort: function () {
const sort_by = this.props.column.name; const sortBy = this.props.column.name;
const sort_order = (this.props.column.sorted === 'asc') ? 'desc' : 'asc'; const sortOrder = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
this.props.onSort(sort_by, sort_order); this.props.onSort(sortBy, sortOrder);
}, },
render: function () { render: function () {
const classes = classNames( const classes = classNames(

View File

@ -44,7 +44,7 @@ const ListingItem = React.createClass({
checkbox = ( checkbox = (
<th className="check-column" scope="row"> <th className="check-column" scope="row">
<label className="screen-reader-text">{ <label className="screen-reader-text">{
'Select ' + this.props.item[this.props.columns[0].name] `Select ${this.props.item[this.props.columns[0].name]}`
}</label> }</label>
<input <input
type="checkbox" type="checkbox"
@ -58,24 +58,20 @@ const ListingItem = React.createClass({
); );
} }
const custom_actions = this.props.item_actions; const customActions = this.props.item_actions;
let item_actions = false; let itemActions = false;
if (custom_actions.length > 0) { if (customActions.length > 0) {
let is_first = true; let isFirst = true;
item_actions = custom_actions.map((action, index) => { itemActions = customActions
if (action.display !== undefined) { .filter(action => action.display === undefined || action.display(this.props.item))
if (action.display(this.props.item) === false) { .map((action, index) => {
return; let customAction = null;
}
}
let custom_action = null;
if (action.name === 'trash') { if (action.name === 'trash') {
custom_action = ( customAction = (
<span key={'action-' + index} className="trash"> <span key={`action-${index}`} className="trash">
{(!is_first) ? ' | ' : ''} {(!isFirst) ? ' | ' : ''}
<a <a
href="javascript:;" href="javascript:;"
onClick={this.handleTrashItem.bind( onClick={this.handleTrashItem.bind(
@ -87,27 +83,27 @@ const ListingItem = React.createClass({
</span> </span>
); );
} else if (action.refresh) { } else if (action.refresh) {
custom_action = ( customAction = (
<span <span
onClick={this.props.onRefreshItems} onClick={this.props.onRefreshItems}
key={'action-' + index} className={action.name}> key={`action-${index}`} className={action.name}>
{(!is_first) ? ' | ' : ''} {(!isFirst) ? ' | ' : ''}
{ action.link(this.props.item) } { action.link(this.props.item) }
</span> </span>
); );
} else if (action.link) { } else if (action.link) {
custom_action = ( customAction = (
<span <span
key={'action-' + index} className={action.name}> key={`action-${index}`} className={action.name}>
{(!is_first) ? ' | ' : ''} {(!isFirst) ? ' | ' : ''}
{ action.link(this.props.item) } { action.link(this.props.item) }
</span> </span>
); );
} else { } else {
custom_action = ( customAction = (
<span <span
key={'action-' + index} className={action.name}> key={`action-${index}`} className={action.name}>
{(!is_first) ? ' | ' : ''} {(!isFirst) ? ' | ' : ''}
<a href="javascript:;" onClick={ <a href="javascript:;" onClick={
(action.onClick !== undefined) (action.onClick !== undefined)
? action.onClick.bind(null, ? action.onClick.bind(null,
@ -120,14 +116,14 @@ const ListingItem = React.createClass({
); );
} }
if (custom_action !== null && is_first === true) { if (customAction !== null && isFirst === true) {
is_first = false; isFirst = false;
} }
return custom_action; return customAction;
}); });
} else { } else {
item_actions = ( itemActions = (
<span className="edit"> <span className="edit">
<Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link> <Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link>
</span> </span>
@ -172,7 +168,7 @@ const ListingItem = React.createClass({
actions = ( actions = (
<div> <div>
<div className="row-actions"> <div className="row-actions">
{ item_actions } { itemActions }
</div> </div>
<button <button
onClick={this.handleToggleItem.bind(null, this.props.item.id)} onClick={this.handleToggleItem.bind(null, this.props.item.id)}
@ -183,10 +179,10 @@ const ListingItem = React.createClass({
); );
} }
const row_classes = classNames({ 'is-expanded': this.state.expanded }); const rowClasses = classNames({ 'is-expanded': this.state.expanded });
return ( return (
<tr className={row_classes}> <tr className={rowClasses} data-automation-id={`listing_item_${this.props.item.id}`}>
{ checkbox } { checkbox }
{ this.props.onRenderItem(this.props.item, actions) } { this.props.onRenderItem(this.props.item, actions) }
</tr> </tr>
@ -223,24 +219,24 @@ const ListingItems = React.createClass({
</tr> </tr>
</tbody> </tbody>
); );
} else { }
const select_all_classes = classNames( const selectAllClasses = classNames(
'mailpoet_select_all', 'mailpoet_select_all',
{ mailpoet_hidden: ( { mailpoet_hidden: (
this.props.selection === false this.props.selection === false
|| (this.props.count <= this.props.limit) || (this.props.count <= this.props.limit)
), ),
} }
); );
return ( return (
<tbody> <tbody>
<tr className={select_all_classes}> <tr className={selectAllClasses}>
<td colSpan={ <td colSpan={
this.props.columns.length this.props.columns.length
+ (this.props.is_selectable ? 1 : 0) + (this.props.is_selectable ? 1 : 0)
}> }>
{ {
(this.props.selection !== 'all') (this.props.selection !== 'all')
? MailPoet.I18n.t('selectAllLabel') ? MailPoet.I18n.t('selectAllLabel')
: MailPoet.I18n.t('selectedAllLabel').replace( : MailPoet.I18n.t('selectedAllLabel').replace(
@ -249,41 +245,40 @@ const ListingItems = React.createClass({
) )
} }
&nbsp; &nbsp;
<a <a
onClick={this.props.onSelectAll} onClick={this.props.onSelectAll}
href="javascript:;">{ href="javascript:;">{
(this.props.selection !== 'all') (this.props.selection !== 'all')
? MailPoet.I18n.t('selectAllLink') ? MailPoet.I18n.t('selectAllLink')
: MailPoet.I18n.t('clearSelection') : MailPoet.I18n.t('clearSelection')
}</a> }</a>
</td> </td>
</tr> </tr>
{this.props.items.map((item, index) => { {this.props.items.map((item, index) => {
const renderItem = item; const renderItem = item;
renderItem.id = parseInt(item.id, 10); renderItem.id = parseInt(item.id, 10);
renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1); renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1);
return ( return (
<ListingItem <ListingItem
columns={this.props.columns} columns={this.props.columns}
onSelectItem={this.props.onSelectItem} onSelectItem={this.props.onSelectItem}
onRenderItem={this.props.onRenderItem} onRenderItem={this.props.onRenderItem}
onDeleteItem={this.props.onDeleteItem} onDeleteItem={this.props.onDeleteItem}
onRestoreItem={this.props.onRestoreItem} onRestoreItem={this.props.onRestoreItem}
onTrashItem={this.props.onTrashItem} onTrashItem={this.props.onTrashItem}
onRefreshItems={this.props.onRefreshItems} onRefreshItems={this.props.onRefreshItems}
selection={this.props.selection} selection={this.props.selection}
is_selectable={this.props.is_selectable} is_selectable={this.props.is_selectable}
item_actions={this.props.item_actions} item_actions={this.props.item_actions}
group={this.props.group} group={this.props.group}
key={`item-${renderItem.id}-${index}`} key={`item-${renderItem.id}-${index}`}
item={renderItem} /> item={renderItem} />
); );
})} })}
</tbody> </tbody>
); );
}
}, },
}); });
@ -319,16 +314,15 @@ const Listing = React.createClass({
const state = this.getInitialState(); const state = this.getInitialState();
// check for url params // check for url params
if (params.splat) { if (params.splat) {
params.splat.split('/').map((param) => { params.splat.split('/').forEach((param) => {
const [key, value] = this.getParam(param); const [key, value] = this.getParam(param);
const filters = {};
switch (key) { switch (key) {
case 'filter': case 'filter':
const filters = {}; value.split('&').forEach((pair) => {
value.split('&').map((pair) => {
const [k, v] = pair.split('='); const [k, v] = pair.split('=');
filters[k] = v; filters[k] = v;
} });
);
state.filter = filters; state.filter = filters;
break; break;
@ -340,7 +334,7 @@ const Listing = React.createClass({
// limit per page // limit per page
if (this.props.limit !== undefined) { if (this.props.limit !== undefined) {
state.limit = Math.abs(~~this.props.limit); state.limit = Math.abs(Number(this.props.limit));
} }
// sort by // sort by
@ -371,8 +365,7 @@ const Listing = React.createClass({
setParams: function () { setParams: function () {
if (this.props.location) { if (this.props.location) {
const params = Object.keys(this.state) const params = Object.keys(this.state)
.filter((key) => { .filter(key => (
return (
[ [
'group', 'group',
'filter', 'filter',
@ -381,8 +374,7 @@ const Listing = React.createClass({
'sort_by', 'sort_by',
'sort_order', 'sort_order',
].indexOf(key) !== -1 ].indexOf(key) !== -1
); ))
})
.map((key) => { .map((key) => {
let value = this.state[key]; let value = this.state[key];
if (value === Object(value)) { if (value === Object(value)) {
@ -390,12 +382,13 @@ const Listing = React.createClass({
} else if (value === Boolean(value)) { } else if (value === Boolean(value)) {
value = value.toString(); value = value.toString();
} }
return {
if (value !== '' && value !== null) { key,
return `${key}[${value}]`; value,
} };
}) })
.filter((key) => { return (key !== undefined); }) .filter(({ value }) => value !== '' && value !== null)
.map(({ key, value }) => `${key}[${value}]`)
.join('/'); .join('/');
// set url // set url
@ -407,24 +400,23 @@ const Listing = React.createClass({
} }
}, },
getUrlWithParams: function (params) { getUrlWithParams: function (params) {
let base_url = (this.props.base_url !== undefined) let baseUrl = (this.props.base_url !== undefined)
? this.props.base_url ? this.props.base_url
: null; : null;
if (base_url !== null) { if (baseUrl !== null) {
base_url = this.setBaseUrlParams(base_url); baseUrl = this.setBaseUrlParams(baseUrl);
return `/${base_url}/${params}`; return `/${baseUrl}/${params}`;
} else {
return `/${params}`;
} }
return `/${params}`;
}, },
setBaseUrlParams: function (base_url) { setBaseUrlParams: function (baseUrl) {
let ret = base_url; let ret = baseUrl;
if (ret.indexOf(':') !== -1) { if (ret.indexOf(':') !== -1) {
const params = this.getParams(); const params = this.getParams();
Object.keys(params).map((key) => { Object.keys(params).forEach((key) => {
if (ret.indexOf(':' + key) !== -1) { if (ret.indexOf(`:${key}`) !== -1) {
ret = ret.replace(':' + key, params[key]); ret = ret.replace(`:${key}`, params[key]);
} }
}); });
} }
@ -491,7 +483,7 @@ const Listing = React.createClass({
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -514,14 +506,14 @@ const Listing = React.createClass({
}).done((response) => { }).done((response) => {
if ( if (
this.props.messages !== undefined this.props.messages !== undefined
&& this.props.messages['onRestore'] !== undefined && this.props.messages.onRestore !== undefined
) { ) {
this.props.messages.onRestore(response); this.props.messages.onRestore(response);
} }
this.getItems(); this.getItems();
}).fail((response) => { }).fail((response) => {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
}); });
@ -542,14 +534,14 @@ const Listing = React.createClass({
}).done((response) => { }).done((response) => {
if ( if (
this.props.messages !== undefined this.props.messages !== undefined
&& this.props.messages['onTrash'] !== undefined && this.props.messages.onTrash !== undefined
) { ) {
this.props.messages.onTrash(response); this.props.messages.onTrash(response);
} }
this.getItems(); this.getItems();
}).fail((response) => { }).fail((response) => {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
}); });
@ -570,14 +562,14 @@ const Listing = React.createClass({
}).done((response) => { }).done((response) => {
if ( if (
this.props.messages !== undefined this.props.messages !== undefined
&& this.props.messages['onDelete'] !== undefined && this.props.messages.onDelete !== undefined
) { ) {
this.props.messages.onDelete(response); this.props.messages.onDelete(response);
} }
this.getItems(); this.getItems();
}).fail((response) => { }).fail((response) => {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
}); });
@ -594,16 +586,16 @@ const Listing = React.createClass({
this.handleGroup('all'); this.handleGroup('all');
}).fail((response) => { }).fail((response) => {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
}); });
}, },
handleBulkAction: function (selected_ids, params) { handleBulkAction: function (selectedIds, params) {
if ( if (
this.state.selection === false this.state.selection === false
&& this.state.selected_ids.length === 0 && this.state.selected_ids.length === 0
&& selected_ids !== 'all' && selectedIds !== 'all'
) { ) {
return false; return false;
} }
@ -619,8 +611,8 @@ const Listing = React.createClass({
group: this.state.group, group: this.state.group,
search: this.state.search, search: this.state.search,
}; };
if (selected_ids !== 'all') { if (selectedIds !== 'all') {
data.listing.selection = selected_ids; data.listing.selection = selectedIds;
} }
return MailPoet.Ajax.post({ return MailPoet.Ajax.post({
@ -633,7 +625,7 @@ const Listing = React.createClass({
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -649,20 +641,20 @@ const Listing = React.createClass({
this.setParams(); this.setParams();
}); });
}, },
handleSort: function (sort_by, sort_order = 'asc') { handleSort: function (sortBy, sortOrder = 'asc') {
this.setState({ this.setState({
sort_by: sort_by, sort_by: sortBy,
sort_order: (sort_order === 'asc') ? 'asc' : 'desc', sort_order: (sortOrder === 'asc') ? 'asc' : 'desc',
}, () => { }, () => {
this.setParams(); this.setParams();
}); });
}, },
handleSelectItem: function (id, is_checked) { handleSelectItem: function (id, isChecked) {
let selected_ids = this.state.selected_ids; let selectedIds = this.state.selected_ids;
let selection = false; let selection = false;
if (is_checked) { if (isChecked) {
selected_ids = jQuery.merge(selected_ids, [id]); selectedIds = jQuery.merge(selectedIds, [id]);
// check whether all items on the page are selected // check whether all items on the page are selected
if ( if (
jQuery('tbody .check-column :checkbox:not(:checked)').length === 0 jQuery('tbody .check-column :checkbox:not(:checked)').length === 0
@ -670,24 +662,22 @@ const Listing = React.createClass({
selection = 'page'; selection = 'page';
} }
} else { } else {
selected_ids.splice(selected_ids.indexOf(id), 1); selectedIds.splice(selectedIds.indexOf(id), 1);
} }
this.setState({ this.setState({
selection: selection, selection: selection,
selected_ids: selected_ids, selected_ids: selectedIds,
}); });
}, },
handleSelectItems: function (is_checked) { handleSelectItems: function (isChecked) {
if (is_checked === false) { if (isChecked === false) {
this.clearSelection(); this.clearSelection();
} else { } else {
const selected_ids = this.state.items.map((item) => { const selectedIds = this.state.items.map(item => Number(item.id));
return ~~item.id;
});
this.setState({ this.setState({
selected_ids: selected_ids, selected_ids: selectedIds,
selection: 'page', selection: 'page',
}); });
} }
@ -747,20 +737,20 @@ const Listing = React.createClass({
}, },
render: function () { render: function () {
const items = this.state.items; const items = this.state.items;
const sort_by = this.state.sort_by; const sortBy = this.state.sort_by;
const sort_order = this.state.sort_order; const sortOrder = this.state.sort_order;
// columns // columns
let columns = this.props.columns || []; let columns = this.props.columns || [];
columns = columns.filter((column) => { columns = columns.filter(
return (column.display === undefined || !!(column.display) === true); column => (column.display === undefined || !!(column.display) === true)
}); );
// bulk actions // bulk actions
let bulk_actions = this.props.bulk_actions || []; let bulkActions = this.props.bulk_actions || [];
if (this.state.group === 'trash' && bulk_actions.length > 0) { if (this.state.group === 'trash' && bulkActions.length > 0) {
bulk_actions = [ bulkActions = [
{ {
name: 'restore', name: 'restore',
label: MailPoet.I18n.t('restore'), label: MailPoet.I18n.t('restore'),
@ -775,9 +765,9 @@ const Listing = React.createClass({
} }
// item actions // item actions
const item_actions = this.props.item_actions || []; const itemActions = this.props.item_actions || [];
const table_classes = classNames( const tableClasses = classNames(
'mailpoet_listing_table', 'mailpoet_listing_table',
'wp-list-table', 'wp-list-table',
'widefat', 'widefat',
@ -822,7 +812,7 @@ const Listing = React.createClass({
<div className="tablenav top clearfix"> <div className="tablenav top clearfix">
<ListingBulkActions <ListingBulkActions
count={this.state.count} count={this.state.count}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
selection={this.state.selection} selection={this.state.selection}
selected_ids={this.state.selected_ids} selected_ids={this.state.selected_ids}
onBulkAction={this.handleBulkAction} /> onBulkAction={this.handleBulkAction} />
@ -840,16 +830,16 @@ const Listing = React.createClass({
limit={this.state.limit} limit={this.state.limit}
onSetPage={this.handleSetPage} /> onSetPage={this.handleSetPage} />
</div> </div>
<table className={table_classes}> <table className={tableClasses}>
<thead> <thead>
<ListingHeader <ListingHeader
onSort={this.handleSort} onSort={this.handleSort}
onSelectItems={this.handleSelectItems} onSelectItems={this.handleSelectItems}
selection={this.state.selection} selection={this.state.selection}
sort_by={sort_by} sort_by={sortBy}
sort_order={sort_order} sort_order={sortOrder}
columns={columns} columns={columns}
is_selectable={bulk_actions.length > 0} /> is_selectable={bulkActions.length > 0} />
</thead> </thead>
<ListingItems <ListingItems
@ -859,7 +849,7 @@ const Listing = React.createClass({
onTrashItem={this.handleTrashItem} onTrashItem={this.handleTrashItem}
onRefreshItems={this.handleRefreshItems} onRefreshItems={this.handleRefreshItems}
columns={columns} columns={columns}
is_selectable={bulk_actions.length > 0} is_selectable={bulkActions.length > 0}
onSelectItem={this.handleSelectItem} onSelectItem={this.handleSelectItem}
onSelectAll={this.handleSelectAll} onSelectAll={this.handleSelectAll}
selection={this.state.selection} selection={this.state.selection}
@ -868,7 +858,7 @@ const Listing = React.createClass({
group={this.state.group} group={this.state.group}
count={this.state.count} count={this.state.count}
limit={this.state.limit} limit={this.state.limit}
item_actions={item_actions} item_actions={itemActions}
messages={messages} messages={messages}
items={items} /> items={items} />
@ -877,17 +867,17 @@ const Listing = React.createClass({
onSort={this.handleSort} onSort={this.handleSort}
onSelectItems={this.handleSelectItems} onSelectItems={this.handleSelectItems}
selection={this.state.selection} selection={this.state.selection}
sort_by={sort_by} sort_by={sortBy}
sort_order={sort_order} sort_order={sortOrder}
columns={columns} columns={columns}
is_selectable={bulk_actions.length > 0} /> is_selectable={bulkActions.length > 0} />
</tfoot> </tfoot>
</table> </table>
<div className="tablenav bottom"> <div className="tablenav bottom">
<ListingBulkActions <ListingBulkActions
count={this.state.count} count={this.state.count}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
selection={this.state.selection} selection={this.state.selection}
selected_ids={this.state.selected_ids} selected_ids={this.state.selected_ids}
onBulkAction={this.handleBulkAction} /> onBulkAction={this.handleBulkAction} />

View File

@ -7,7 +7,6 @@ define([
classNames, classNames,
MailPoet MailPoet
) => { ) => {
const ListingPages = React.createClass({ const ListingPages = React.createClass({
getInitialState: function () { getInitialState: function () {
return { return {
@ -38,7 +37,7 @@ define([
); );
}, },
constrainPage: function (page) { constrainPage: function (page) {
return Math.min(Math.max(1, Math.abs(~~page)), this.getLastPage()); return Math.min(Math.max(1, Math.abs(Number(page))), this.getLastPage());
}, },
handleSetManualPage: function (e) { handleSetManualPage: function (e) {
if (e.which === 13) { if (e.which === 13) {
@ -59,125 +58,125 @@ define([
render: function () { render: function () {
if (this.props.count === 0) { if (this.props.count === 0) {
return false; return false;
} else { }
let pagination = false; let pagination = false;
let firstPage = ( let firstPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">«</span> <span aria-hidden="true" className="tablenav-pages-navspan">«</span>
); );
let previousPage = ( let previousPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span> <span aria-hidden="true" className="tablenav-pages-navspan"></span>
); );
let nextPage = ( let nextPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span> <span aria-hidden="true" className="tablenav-pages-navspan"></span>
); );
let lastPage = ( let lastPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">»</span> <span aria-hidden="true" className="tablenav-pages-navspan">»</span>
); );
if (this.props.limit > 0 && this.props.count > this.props.limit) { if (this.props.limit > 0 && this.props.count > this.props.limit) {
if (this.props.page > 1) { if (this.props.page > 1) {
previousPage = ( previousPage = (
<a href="javascript:;" <a href="javascript:;"
onClick={this.setPreviousPage} onClick={this.setPreviousPage}
className="prev-page"> className="prev-page">
<span className="screen-reader-text">{MailPoet.I18n.t('previousPage')}</span> <span className="screen-reader-text">{MailPoet.I18n.t('previousPage')}</span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
</a> </a>
); );
}
if (this.props.page > 2) {
firstPage = (
<a href="javascript:;"
onClick={this.setFirstPage}
className="first-page">
<span className="screen-reader-text">{MailPoet.I18n.t('firstPage')}</span>
<span aria-hidden="true">«</span>
</a>
);
}
if (this.props.page < this.getLastPage()) {
nextPage = (
<a href="javascript:;"
onClick={this.setNextPage}
className="next-page">
<span className="screen-reader-text">{MailPoet.I18n.t('nextPage')}</span>
<span aria-hidden="true"></span>
</a>
);
}
if (this.props.page < this.getLastPage() - 1) {
lastPage = (
<a href="javascript:;"
onClick={this.setLastPage}
className="last-page">
<span className="screen-reader-text">{MailPoet.I18n.t('lastPage')}</span>
<span aria-hidden="true">»</span>
</a>
);
}
let pageValue = this.props.page;
if (this.state.page !== null) {
pageValue = this.state.page;
}
pagination = (
<span className="pagination-links">
{firstPage}
&nbsp;
{previousPage}
&nbsp;
<span className="paging-input">
<label
className="screen-reader-text"
htmlFor="current-page-selector">{MailPoet.I18n.t('currentPage')}</label>
<input
type="text"
onChange={this.handleChangeManualPage}
onKeyUp={this.handleSetManualPage}
onBlur={this.handleBlurManualPage}
aria-describedby="table-paging"
size="2"
ref="page"
value={pageValue}
name="paged"
id="current-page-selector"
className="current-page" />
&nbsp;{MailPoet.I18n.t('pageOutOf')}&nbsp;
<span className="total-pages">
{Math.ceil(this.props.count / this.props.limit).toLocaleString()}
</span>
</span>
&nbsp;
{nextPage}
&nbsp;
{lastPage}
</span>
);
} }
const classes = classNames( if (this.props.page > 2) {
firstPage = (
<a href="javascript:;"
onClick={this.setFirstPage}
className="first-page">
<span className="screen-reader-text">{MailPoet.I18n.t('firstPage')}</span>
<span aria-hidden="true">«</span>
</a>
);
}
if (this.props.page < this.getLastPage()) {
nextPage = (
<a href="javascript:;"
onClick={this.setNextPage}
className="next-page">
<span className="screen-reader-text">{MailPoet.I18n.t('nextPage')}</span>
<span aria-hidden="true"></span>
</a>
);
}
if (this.props.page < this.getLastPage() - 1) {
lastPage = (
<a href="javascript:;"
onClick={this.setLastPage}
className="last-page">
<span className="screen-reader-text">{MailPoet.I18n.t('lastPage')}</span>
<span aria-hidden="true">»</span>
</a>
);
}
let pageValue = this.props.page;
if (this.state.page !== null) {
pageValue = this.state.page;
}
pagination = (
<span className="pagination-links">
{firstPage}
&nbsp;
{previousPage}
&nbsp;
<span className="paging-input">
<label
className="screen-reader-text"
htmlFor="current-page-selector">{MailPoet.I18n.t('currentPage')}</label>
<input
type="text"
onChange={this.handleChangeManualPage}
onKeyUp={this.handleSetManualPage}
onBlur={this.handleBlurManualPage}
aria-describedby="table-paging"
size="2"
ref="page"
value={pageValue}
name="paged"
id="current-page-selector"
className="current-page" />
{MailPoet.I18n.t('pageOutOf')}&nbsp;
<span className="total-pages">
{Math.ceil(this.props.count / this.props.limit).toLocaleString()}
</span>
</span>
&nbsp;
{nextPage}
&nbsp;
{lastPage}
</span>
);
}
const classes = classNames(
'tablenav-pages', 'tablenav-pages',
{ 'one-page': (this.props.count <= this.props.limit) } { 'one-page': (this.props.count <= this.props.limit) }
); );
let numberOfItemsLabel; let numberOfItemsLabel;
if (this.props.count == 1) { if (Number(this.props.count) === 1) {
numberOfItemsLabel = MailPoet.I18n.t('numberOfItemsSingular'); numberOfItemsLabel = MailPoet.I18n.t('numberOfItemsSingular');
} else { } else {
numberOfItemsLabel = MailPoet.I18n.t('numberOfItemsMultiple') numberOfItemsLabel = MailPoet.I18n.t('numberOfItemsMultiple')
.replace('%$1d', this.props.count.toLocaleString()); .replace('%$1d', parseInt(this.props.count, 10).toLocaleString());
}
return (
<div className={classes}>
<span className="displaying-num">{ numberOfItemsLabel }</span>
{ pagination }
</div>
);
} }
return (
<div className={classes}>
<span className="displaying-num">{ numberOfItemsLabel }</span>
{ pagination }
</div>
);
}, },
}); });

View File

@ -5,7 +5,6 @@ define([
MailPoet, MailPoet,
React React
) => { ) => {
const ListingSearch = React.createClass({ const ListingSearch = React.createClass({
handleSearch: function (e) { handleSearch: function (e) {
e.preventDefault(); e.preventDefault();
@ -19,27 +18,26 @@ define([
render: function () { render: function () {
if (this.props.search === false) { if (this.props.search === false) {
return false; return false;
} else {
return (
<form name="search" onSubmit={this.handleSearch}>
<p className="search-box">
<label htmlFor="search_input" className="screen-reader-text">
{MailPoet.I18n.t('searchLabel')}
</label>
<input
type="search"
id="search_input"
ref="search"
name="s"
defaultValue={this.props.search} />
<input
type="submit"
defaultValue={MailPoet.I18n.t('searchLabel')}
className="button" />
</p>
</form>
);
} }
return (
<form name="search" onSubmit={this.handleSearch}>
<p className="search-box">
<label htmlFor="search_input" className="screen-reader-text">
{MailPoet.I18n.t('searchLabel')}
</label>
<input
type="search"
id="search_input"
ref="search"
name="s"
defaultValue={this.props.search} />
<input
type="submit"
defaultValue={MailPoet.I18n.t('searchLabel')}
className="button" />
</p>
</form>
);
}, },
}); });

View File

@ -111,9 +111,8 @@ define('modal', ['mailpoet', 'jquery'],
compileTemplate: function (template) { compileTemplate: function (template) {
if (this.renderer === 'html') { if (this.renderer === 'html') {
return function () { return template; }; return function () { return template; };
} else {
return window.Handlebars.compile(template);
} }
return window.Handlebars.compile(template);
}, },
init: function (options) { init: function (options) {
var modal; var modal;
@ -215,6 +214,7 @@ define('modal', ['mailpoet', 'jquery'],
jQuery(document).on('keyup.mailpoet_modal', function (e) { jQuery(document).on('keyup.mailpoet_modal', function (e) {
if (this.opened === false) { return false; } if (this.opened === false) { return false; }
if (e.keyCode === 27) { this.cancel(); } if (e.keyCode === 27) { this.cancel(); }
return true;
}.bind(this)); }.bind(this));
// make sure the popup is repositioned when the window is resized // make sure the popup is repositioned when the window is resized

View File

@ -41,7 +41,6 @@ define('mp2migrator', ['mailpoet', 'jquery'], function (mp, jQuery) {
jQuery('#upgrade-completed').show(); jQuery('#upgrade-completed').show();
} }
jQuery('#logger').append(row + '<br />\n'); jQuery('#logger').append(row + '<br />\n');
}); });
jQuery('#logger').append('<span class="error_msg">' + MailPoet.MP2Migrator.fatal_error + '</span>' + '<br />\n'); jQuery('#logger').append('<span class="error_msg">' + MailPoet.MP2Migrator.fatal_error + '</span>' + '<br />\n');
}).always(function () { }).always(function () {
@ -206,5 +205,4 @@ define('mp2migrator', ['mailpoet', 'jquery'], function (mp, jQuery) {
// Update the display // Update the display
MailPoet.MP2Migrator.updateDisplay(); MailPoet.MP2Migrator.updateDisplay();
}); });
}); });

View File

@ -36,5 +36,4 @@ define([
window.EditorApplication = app; window.EditorApplication = app;
return app; return app;
}); });

View File

@ -281,7 +281,7 @@ define([
// 2. Remove visual markings of drop position visualization // 2. Remove visual markings of drop position visualization
this.view.$('.mailpoet_drop_marker').remove(); this.view.$('.mailpoet_drop_marker').remove();
}, },
getDropPosition: function (eventX, eventY, is_unsafe) { getDropPosition: function (eventX, eventY, isUnsafe) {
var SPECIAL_AREA_INSERTION_WIDTH = 0.00; // Disable special insertion. Default: 0.3 var SPECIAL_AREA_INSERTION_WIDTH = 0.00; // Disable special insertion. Default: 0.3
var element = this.view.$el; var element = this.view.$el;
@ -307,7 +307,7 @@ define([
var position; var position;
var indexAndPosition; var indexAndPosition;
var unsafe = !!is_unsafe; var unsafe = !!isUnsafe;
if (this.getCollection().length === 0) { if (this.getCollection().length === 0) {
return { return {
@ -358,7 +358,7 @@ define([
if (orientation === 'horizontal' && insertionType === 'special') { if (orientation === 'horizontal' && insertionType === 'special') {
// Disable special insertion for horizontal containers // Disable special insertion for horizontal containers
return; return undefined;
} }
return { return {
@ -398,13 +398,12 @@ define([
index: index, index: index,
position: 'before' position: 'before'
}; };
} else {
// Second half of the element
return {
index: index,
position: 'after'
};
} }
// Second half of the element
return {
index: index,
position: 'after'
};
}, },
_computeSpecialIndex: function (eventX, eventY) { _computeSpecialIndex: function (eventX, eventY) {
return this._computeCellIndex(eventX, eventY); return this._computeCellIndex(eventX, eventY);

View File

@ -84,7 +84,6 @@ define([
that.view.$el.addClass('mailpoet_hidden'); that.view.$el.addClass('mailpoet_hidden');
} }
} }
}, },
// call this function on every dragmove event // call this function on every dragmove event
onmove: function (event) { onmove: function (event) {

View File

@ -27,7 +27,6 @@ define([
_, _,
jQuery jQuery
) { ) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -380,21 +379,21 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('automatedLatestContent', { BeforeStartApp.registerBlockType('automatedLatestContent', {
blockModel: Module.AutomatedLatestContentBlockModel, blockModel: Module.AutomatedLatestContentBlockModel,
blockView: Module.AutomatedLatestContentBlockView blockView: Module.AutomatedLatestContentBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'automatedLatestContent', name: 'automatedLatestContent',
widgetView: Module.AutomatedLatestContentWidgetView, widgetView: Module.AutomatedLatestContentWidgetView,
priority: 97 priority: 97
}); });
}); });
App.on('start', function (App) { App.on('start', function (StartApp) {
var Application = App; var Application = StartApp;
Application._ALCSupervisor = new Module.ALCSupervisor(); Application._ALCSupervisor = new Module.ALCSupervisor();
Application._ALCSupervisor.refresh(); Application._ALCSupervisor.refresh();
}); });

View File

@ -13,7 +13,6 @@ define([
'mailpoet', 'mailpoet',
'modal' 'modal'
], function (App, Marionette, SuperModel, _, jQuery, MailPoet) { ], function (App, Marionette, SuperModel, _, jQuery, MailPoet) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -79,6 +78,7 @@ define([
WidgetView.destroy(); WidgetView.destroy();
return node; return node;
} }
return undefined;
} }
}, },
HighlightEditingBehavior: {} HighlightEditingBehavior: {}

View File

@ -8,7 +8,6 @@ define([
'underscore', 'underscore',
'jquery' 'jquery'
], function (App, BaseBlock, MailPoet, _, jQuery) { ], function (App, BaseBlock, MailPoet, _, jQuery) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -132,13 +131,13 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('button', { BeforeStartApp.registerBlockType('button', {
blockModel: Module.ButtonBlockModel, blockModel: Module.ButtonBlockModel,
blockView: Module.ButtonBlockView blockView: Module.ButtonBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'button', name: 'button',
widgetView: Module.ButtonWidgetView, widgetView: Module.ButtonWidgetView,
priority: 92 priority: 92

View File

@ -11,7 +11,6 @@ define([
'newsletter_editor/App', 'newsletter_editor/App',
'newsletter_editor/blocks/base' 'newsletter_editor/blocks/base'
], function (Backbone, Marionette, _, jQuery, App, BaseBlock) { ], function (Backbone, Marionette, _, jQuery, App, BaseBlock) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -54,6 +53,7 @@ define([
if (invalidBlock) { if (invalidBlock) {
return invalidBlock.validationError; return invalidBlock.validationError;
} }
return undefined;
}, },
parse: function (response) { parse: function (response) {
// If container has any blocks - add them to a collection // If container has any blocks - add them to a collection
@ -132,6 +132,7 @@ define([
WidgetView.destroy(); WidgetView.destroy();
return node; return node;
} }
return undefined;
}, },
testAttachToInstance: function (model, view) { testAttachToInstance: function (model, view) {
// Attach Draggable only to layout containers and disable it // Attach Draggable only to layout containers and disable it
@ -149,7 +150,6 @@ define([
if (this.model.get('blocks').length === 2) return Module.TwoColumnContainerWidgetView; if (this.model.get('blocks').length === 2) return Module.TwoColumnContainerWidgetView;
} }
return Module.OneColumnContainerWidgetView; return Module.OneColumnContainerWidgetView;
}, },
initialize: function (options) { initialize: function (options) {
base.BlockView.prototype.initialize.apply(this, arguments); base.BlockView.prototype.initialize.apply(this, arguments);
@ -194,13 +194,6 @@ define([
var $toggleButton = this.$('> .mailpoet_tools .mailpoet_newsletter_layer_selector'); var $toggleButton = this.$('> .mailpoet_tools .mailpoet_newsletter_layer_selector');
var $overlay = jQuery('.mailpoet_layer_overlay'); var $overlay = jQuery('.mailpoet_layer_overlay');
var $container = this.$('> .mailpoet_container'); var $container = this.$('> .mailpoet_container');
var enableContainerLayer = function () {
that.$el.addClass('mailpoet_container_layer_active');
$toggleButton.addClass('mailpoet_container_layer_active');
$container.addClass('mailpoet_layer_highlight');
$overlay.click(disableContainerLayer);
$overlay.show();
};
var disableContainerLayer = function () { var disableContainerLayer = function () {
that.$el.removeClass('mailpoet_container_layer_active'); that.$el.removeClass('mailpoet_container_layer_active');
$toggleButton.removeClass('mailpoet_container_layer_active'); $toggleButton.removeClass('mailpoet_container_layer_active');
@ -208,6 +201,13 @@ define([
$overlay.hide(); $overlay.hide();
$overlay.off('click'); $overlay.off('click');
}; };
var enableContainerLayer = function () {
that.$el.addClass('mailpoet_container_layer_active');
$toggleButton.addClass('mailpoet_container_layer_active');
$container.addClass('mailpoet_layer_highlight');
$overlay.click(disableContainerLayer);
$overlay.show();
};
if ($toggleButton.hasClass('mailpoet_container_layer_active')) { if ($toggleButton.hasClass('mailpoet_container_layer_active')) {
disableContainerLayer(); disableContainerLayer();
} else { } else {
@ -336,25 +336,25 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('container', { BeforeStartApp.registerBlockType('container', {
blockModel: Module.ContainerBlockModel, blockModel: Module.ContainerBlockModel,
blockView: Module.ContainerBlockView blockView: Module.ContainerBlockView
}); });
App.registerLayoutWidget({ BeforeStartApp.registerLayoutWidget({
name: 'oneColumnLayout', name: 'oneColumnLayout',
priority: 100, priority: 100,
widgetView: Module.OneColumnContainerWidgetView widgetView: Module.OneColumnContainerWidgetView
}); });
App.registerLayoutWidget({ BeforeStartApp.registerLayoutWidget({
name: 'twoColumnLayout', name: 'twoColumnLayout',
priority: 100, priority: 100,
widgetView: Module.TwoColumnContainerWidgetView widgetView: Module.TwoColumnContainerWidgetView
}); });
App.registerLayoutWidget({ BeforeStartApp.registerLayoutWidget({
name: 'threeColumnLayout', name: 'threeColumnLayout',
priority: 100, priority: 100,
widgetView: Module.ThreeColumnContainerWidgetView widgetView: Module.ThreeColumnContainerWidgetView

View File

@ -7,7 +7,6 @@ define([
'underscore', 'underscore',
'jquery' 'jquery'
], function (App, BaseBlock, _, jQuery) { ], function (App, BaseBlock, _, jQuery) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -138,13 +137,13 @@ define([
} }
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('divider', { BeforeStartApp.registerBlockType('divider', {
blockModel: Module.DividerBlockModel, blockModel: Module.DividerBlockModel,
blockView: Module.DividerBlockView blockView: Module.DividerBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'divider', name: 'divider',
widgetView: Module.DividerWidgetView, widgetView: Module.DividerWidgetView,
priority: 93 priority: 93

View File

@ -7,7 +7,6 @@ define([
'underscore', 'underscore',
'mailpoet' 'mailpoet'
], function (App, BaseBlock, _, MailPoet) { ], function (App, BaseBlock, _, MailPoet) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -110,13 +109,13 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('footer', { BeforeStartApp.registerBlockType('footer', {
blockModel: Module.FooterBlockModel, blockModel: Module.FooterBlockModel,
blockView: Module.FooterBlockView blockView: Module.FooterBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'footer', name: 'footer',
widgetView: Module.FooterWidgetView, widgetView: Module.FooterWidgetView,
priority: 99 priority: 99

View File

@ -7,7 +7,6 @@ define([
'underscore', 'underscore',
'mailpoet' 'mailpoet'
], function (App, BaseBlock, _, MailPoet) { ], function (App, BaseBlock, _, MailPoet) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -110,13 +109,13 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('header', { BeforeStartApp.registerBlockType('header', {
blockModel: Module.HeaderBlockModel, blockModel: Module.HeaderBlockModel,
blockView: Module.HeaderBlockView blockView: Module.HeaderBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'header', name: 'header',
widgetView: Module.HeaderWidgetView, widgetView: Module.HeaderWidgetView,
priority: 98 priority: 98

View File

@ -8,7 +8,6 @@ define([
'mailpoet', 'mailpoet',
'jquery' 'jquery'
], function (App, BaseBlock, _, MailPoet, jQuery) { ], function (App, BaseBlock, _, MailPoet, jQuery) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -413,13 +412,13 @@ define([
}); });
Module.ImageWidgetView = ImageWidgetView; Module.ImageWidgetView = ImageWidgetView;
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('image', { BeforeStartApp.registerBlockType('image', {
blockModel: Module.ImageBlockModel, blockModel: Module.ImageBlockModel,
blockView: Module.ImageBlockView blockView: Module.ImageBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'image', name: 'image',
widgetView: Module.ImageWidgetView, widgetView: Module.ImageWidgetView,
priority: 91 priority: 91

View File

@ -36,7 +36,6 @@ define([
ButtonBlock, ButtonBlock,
DividerBlock DividerBlock
) { ) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -143,6 +142,7 @@ define([
}).always(function () { }).always(function () {
that.trigger('morePostsLoaded'); that.trigger('morePostsLoaded');
}); });
return true;
}, },
_refreshTransformedPosts: function () { _refreshTransformedPosts: function () {
var that = this; var that = this;
@ -582,13 +582,13 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('posts', { BeforeStartApp.registerBlockType('posts', {
blockModel: Module.PostsBlockModel, blockModel: Module.PostsBlockModel,
blockView: Module.PostsBlockView blockView: Module.PostsBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'posts', name: 'posts',
widgetView: Module.PostsWidgetView, widgetView: Module.PostsWidgetView,
priority: 96 priority: 96

View File

@ -10,7 +10,6 @@ define([
'underscore', 'underscore',
'jquery' 'jquery'
], function (App, BaseBlock, Backbone, Marionette, SuperModel, _, jQuery) { ], function (App, BaseBlock, Backbone, Marionette, SuperModel, _, jQuery) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -195,6 +194,7 @@ define([
} else { } else {
return this.changeField('link', event); return this.changeField('link', event);
} }
return undefined;
}, },
changeField: function (field, event) { changeField: function (field, event) {
this.model.set(field, jQuery(event.target).val()); this.model.set(field, jQuery(event.target).val());
@ -298,13 +298,13 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('social', { BeforeStartApp.registerBlockType('social', {
blockModel: Module.SocialBlockModel, blockModel: Module.SocialBlockModel,
blockView: Module.SocialBlockView blockView: Module.SocialBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'social', name: 'social',
widgetView: Module.SocialWidgetView, widgetView: Module.SocialWidgetView,
priority: 95 priority: 95

View File

@ -6,7 +6,6 @@ define([
'newsletter_editor/blocks/base', 'newsletter_editor/blocks/base',
'underscore' 'underscore'
], function (App, BaseBlock, _) { ], function (App, BaseBlock, _) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -87,13 +86,13 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('spacer', { BeforeStartApp.registerBlockType('spacer', {
blockModel: Module.SpacerBlockModel, blockModel: Module.SpacerBlockModel,
blockView: Module.SpacerBlockView blockView: Module.SpacerBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'spacer', name: 'spacer',
widgetView: Module.SpacerWidgetView, widgetView: Module.SpacerWidgetView,
priority: 94 priority: 94

View File

@ -7,7 +7,6 @@ define([
'underscore', 'underscore',
'mailpoet' 'mailpoet'
], function (App, BaseBlock, _, MailPoet) { ], function (App, BaseBlock, _, MailPoet) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -94,13 +93,13 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
App.registerBlockType('text', { BeforeStartApp.registerBlockType('text', {
blockModel: Module.TextBlockModel, blockModel: Module.TextBlockModel,
blockView: Module.TextBlockView blockView: Module.TextBlockView
}); });
App.registerWidget({ BeforeStartApp.registerWidget({
name: 'text', name: 'text',
widgetView: Module.TextWidgetView, widgetView: Module.TextWidgetView,
priority: 90 priority: 90

View File

@ -11,8 +11,8 @@
var Radio = require('backbone.radio'); var Radio = require('backbone.radio');
var _ = require('underscore'); var _ = require('underscore');
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
define(['backbone.marionette', 'backbone.radio', 'underscore'], function (Marionette, Radio, _) { define(['backbone.marionette', 'backbone.radio', 'underscore'], function (BackboneMarionette, BackboneRadio, underscore) {
return factory(Marionette, Radio, _); return factory(BackboneMarionette, BackboneRadio, underscore);
}); });
} }
else if (typeof exports !== 'undefined') { else if (typeof exports !== 'undefined') {

View File

@ -4,7 +4,6 @@ define([
'mailpoet', 'mailpoet',
'ajax' 'ajax'
], function (App, _, MailPoet) { ], function (App, _, MailPoet) {
var Module = {}; var Module = {};
Module._query = function (args) { Module._query = function (args) {

View File

@ -2,7 +2,6 @@ define([
'newsletter_editor/App', 'newsletter_editor/App',
'backbone.supermodel' 'backbone.supermodel'
], function (App, SuperModel) { ], function (App, SuperModel) {
var Module = {}; var Module = {};
Module.ConfigModel = SuperModel.extend({ Module.ConfigModel = SuperModel.extend({
@ -24,8 +23,8 @@ define([
return Module._config; return Module._config;
}; };
App.on('before:start', function (App, options) { App.on('before:start', function (BeforeStartApp, options) {
var Application = App; var Application = BeforeStartApp;
// Expose config methods globally // Expose config methods globally
Application.getConfig = Module.getConfig; Application.getConfig = Module.getConfig;
Application.setConfig = Module.setConfig; Application.setConfig = Module.setConfig;

View File

@ -33,16 +33,14 @@ define([
Module.getBlockTypeModel = function (type) { Module.getBlockTypeModel = function (type) {
if (type in Module._blockTypes) { if (type in Module._blockTypes) {
return Module._blockTypes[type].blockModel; return Module._blockTypes[type].blockModel;
} else {
throw 'Block type not supported: ' + type;
} }
throw 'Block type not supported: ' + type;
}; };
Module.getBlockTypeView = function (type) { Module.getBlockTypeView = function (type) {
if (type in Module._blockTypes) { if (type in Module._blockTypes) {
return Module._blockTypes[type].blockView; return Module._blockTypes[type].blockView;
} else {
throw 'Block type not supported: ' + type;
} }
throw 'Block type not supported: ' + type;
}; };
Module.getBody = function () { Module.getBody = function () {
@ -68,21 +66,21 @@ define([
}; };
App.on('before:start', function (Application, options) { App.on('before:start', function (Application, options) {
var App = Application; var BeforeStartApp = Application;
// Expose block methods globally // Expose block methods globally
App.registerBlockType = Module.registerBlockType; BeforeStartApp.registerBlockType = Module.registerBlockType;
App.getBlockTypeModel = Module.getBlockTypeModel; BeforeStartApp.getBlockTypeModel = Module.getBlockTypeModel;
App.getBlockTypeView = Module.getBlockTypeView; BeforeStartApp.getBlockTypeView = Module.getBlockTypeView;
App.toJSON = Module.toJSON; BeforeStartApp.toJSON = Module.toJSON;
App.getBody = Module.getBody; BeforeStartApp.getBody = Module.getBody;
App.getNewsletter = Module.getNewsletter; BeforeStartApp.getNewsletter = Module.getNewsletter;
App.findModels = Module.findModels; BeforeStartApp.findModels = Module.findModels;
Module.newsletter = new Module.NewsletterModel(_.omit(_.clone(options.newsletter), ['body'])); Module.newsletter = new Module.NewsletterModel(_.omit(_.clone(options.newsletter), ['body']));
}); });
App.on('start', function (Application, options) { App.on('start', function (Application, options) {
var App = Application; var StartApp = Application;
var body = options.newsletter.body; var body = options.newsletter.body;
var content = (_.has(body, 'content')) ? body.content : {}; var content = (_.has(body, 'content')) ? body.content : {};
@ -93,13 +91,13 @@ define([
); );
} }
App._contentContainer = new (App.getBlockTypeModel('container'))(content, { parse: true }); StartApp._contentContainer = new (StartApp.getBlockTypeModel('container'))(content, { parse: true });
App._contentContainerView = new (App.getBlockTypeView('container'))({ StartApp._contentContainerView = new (StartApp.getBlockTypeView('container'))({
model: App._contentContainer, model: StartApp._contentContainer,
renderOptions: { depth: 0 } renderOptions: { depth: 0 }
}); });
App._appView.showChildView('contentRegion', App._contentContainerView); StartApp._appView.showChildView('contentRegion', StartApp._contentContainerView);
}); });

View File

@ -6,7 +6,6 @@ define([
'jquery', 'jquery',
'mailpoet' 'mailpoet'
], function (App, Backbone, Marionette, _, jQuery, MailPoet) { ], function (App, Backbone, Marionette, _, jQuery, MailPoet) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -29,8 +28,8 @@ define([
} }
}); });
App.on('start', function (App) { App.on('start', function (StartApp) {
App._appView.showChildView('headingRegion', new Module.HeadingView({ model: App.getNewsletter() })); StartApp._appView.showChildView('headingRegion', new Module.HeadingView({ model: StartApp.getNewsletter() }));
MailPoet.helpTooltip.show(document.getElementById('tooltip-designer-subject-line'), { MailPoet.helpTooltip.show(document.getElementById('tooltip-designer-subject-line'), {
tooltipId: 'tooltip-designer-subject-line-ti', tooltipId: 'tooltip-designer-subject-line-ti',
tooltip: MailPoet.I18n.t('helpTooltipDesignerSubjectLine'), tooltip: MailPoet.I18n.t('helpTooltipDesignerSubjectLine'),

View File

@ -25,7 +25,6 @@ define([
_, _,
$ $
) { ) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -33,7 +32,6 @@ define([
// Save editor contents to server // Save editor contents to server
Module.save = function () { Module.save = function () {
var json = App.toJSON(); var json = App.toJSON();
// Stringify to enable transmission of primitive non-string value types // Stringify to enable transmission of primitive non-string value types
@ -235,7 +233,6 @@ define([
}); });
this.hideOptionContents(); this.hideOptionContents();
} }
}, },
toggleExportTemplate: function () { toggleExportTemplate: function () {
this.$('.mailpoet_export_template_container').toggleClass('mailpoet_hidden'); this.$('.mailpoet_export_template_container').toggleClass('mailpoet_hidden');
@ -350,10 +347,11 @@ define([
return message; return message;
} }
return undefined;
}; };
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
var Application = App; var Application = BeforeStartApp;
Application.save = Module.save; Application.save = Module.save;
Application.getChannel().on('autoSave', Module.autoSave); Application.getChannel().on('autoSave', Module.autoSave);
@ -362,9 +360,9 @@ define([
Application.getChannel().reply('save', Application.save); Application.getChannel().reply('save', Application.save);
}); });
App.on('start', function (App) { App.on('start', function (BeforeStartApp) {
var saveView = new Module.SaveView(); var saveView = new Module.SaveView();
App._appView.showChildView('bottomRegion', saveView); BeforeStartApp._appView.showChildView('bottomRegion', saveView);
}); });
return Module; return Module;

View File

@ -17,7 +17,6 @@ define([
_, _,
jQuery jQuery
) { ) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -111,12 +110,12 @@ define([
// position of the sidebar would be scrollable and not fixed // position of the sidebar would be scrollable and not fixed
// partially out of visible screen // partially out of visible screen
this.$el.parent().each(function () { this.$el.parent().each(function () {
var calculated_left; var calculatedLeft;
var self = jQuery(this); var self = jQuery(this);
if (self.css('position') === 'fixed') { if (self.css('position') === 'fixed') {
calculated_left = self.parent().offset().left - jQuery(window).scrollLeft(); calculatedLeft = self.parent().offset().left - jQuery(window).scrollLeft();
self.css('left', calculated_left + 'px'); self.css('left', calculatedLeft + 'px');
} else { } else {
self.css('left', ''); self.css('left', '');
} }
@ -339,6 +338,7 @@ define([
} }
}); });
}); });
return undefined;
} }
}); });
@ -360,18 +360,18 @@ define([
} }
}); });
App.on('before:start', function (App) { App.on('before:start', function (BeforeStartApp) {
var Application = App; var Application = BeforeStartApp;
Application.registerWidget = Module.registerWidget; Application.registerWidget = Module.registerWidget;
Application.getWidgets = Module.getWidgets; Application.getWidgets = Module.getWidgets;
Application.registerLayoutWidget = Module.registerLayoutWidget; Application.registerLayoutWidget = Module.registerLayoutWidget;
Application.getLayoutWidgets = Module.getLayoutWidgets; Application.getLayoutWidgets = Module.getLayoutWidgets;
}); });
App.on('start', function (App) { App.on('start', function (StartApp) {
var sidebarView = new SidebarView(); var sidebarView = new SidebarView();
App._appView.showChildView('sidebarRegion', sidebarView); StartApp._appView.showChildView('sidebarRegion', sidebarView);
MailPoet.helpTooltip.show(document.getElementById('tooltip-send-preview'), { MailPoet.helpTooltip.show(document.getElementById('tooltip-send-preview'), {
tooltipId: 'tooltip-editor-send-preview', tooltipId: 'tooltip-editor-send-preview',

View File

@ -4,7 +4,6 @@ define([
'backbone.supermodel', 'backbone.supermodel',
'underscore' 'underscore'
], function (App, Marionette, SuperModel, _) { ], function (App, Marionette, SuperModel, _) {
'use strict'; 'use strict';
var Module = {}; var Module = {};
@ -69,8 +68,8 @@ define([
return App.getConfig().get('availableStyles'); return App.getConfig().get('availableStyles');
}; };
App.on('before:start', function (App, options) { App.on('before:start', function (BeforeStartApp, options) {
var Application = App; var Application = BeforeStartApp;
var body; var body;
var globalStyles; var globalStyles;
// Expose style methods to global application // Expose style methods to global application
@ -83,9 +82,9 @@ define([
this.setGlobalStyles(globalStyles); this.setGlobalStyles(globalStyles);
}); });
App.on('start', function (App) { App.on('start', function (StartApp) {
var stylesView = new Module.StylesView({ model: App.getGlobalStyles() }); var stylesView = new Module.StylesView({ model: StartApp.getGlobalStyles() });
App._appView.showChildView('stylesRegion', stylesView); StartApp._appView.showChildView('stylesRegion', stylesView);
}); });
return Module; return Module;

View File

@ -49,7 +49,7 @@ const stats = {
}; };
class StatsBadge extends React.Component { class StatsBadge extends React.Component {
getBadgeType(stat, rate) { static getBadgeType(stat, rate) {
const len = stat.badgeRanges.length; const len = stat.badgeRanges.length;
for (let i = 0; i < len; i += 1) { for (let i = 0; i < len; i += 1) {
if (rate > stat.badgeRanges[i]) { if (rate > stat.badgeRanges[i]) {
@ -70,7 +70,7 @@ class StatsBadge extends React.Component {
return null; return null;
} }
const badgeType = this.getBadgeType(stat, rate); const badgeType = StatsBadge.getBadgeType(stat, rate);
const badge = badges[badgeType] || null; const badge = badges[badgeType] || null;
if (!badge) { if (!badge) {
return null; return null;

View File

@ -46,14 +46,14 @@ define(
let label = step.label; let label = step.label;
if (step['link'] !== undefined && this.props.step !== step.name) { if (step.link !== undefined && this.props.step !== step.name) {
label = ( label = (
<Link to={step.link}>{ step.label }</Link> <Link to={step.link}>{ step.label }</Link>
); );
} }
return ( return (
<span key={'step-' + index}> <span key={`step-${index}`}>
<span className={stepClasses}> <span className={stepClasses}>
{ label } { label }
</span> </span>

View File

@ -9,7 +9,7 @@ import jQuery from 'jquery';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
import StatsBadge from 'newsletters/badges/stats.jsx'; import StatsBadge from 'newsletters/badges/stats.jsx';
const _QueueMixin = { const QueueMixin = {
pauseSending: function (newsletter) { pauseSending: function (newsletter) {
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
@ -19,12 +19,12 @@ const _QueueMixin = {
newsletter_id: newsletter.id, newsletter_id: newsletter.id,
}, },
}).done(() => { }).done(() => {
jQuery('#resume_' + newsletter.id).show(); jQuery(`#resume_${newsletter.id}`).show();
jQuery('#pause_' + newsletter.id).hide(); jQuery(`#pause_${newsletter.id}`).hide();
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -39,107 +39,106 @@ const _QueueMixin = {
newsletter_id: newsletter.id, newsletter_id: newsletter.id,
}, },
}).done(() => { }).done(() => {
jQuery('#pause_' + newsletter.id).show(); jQuery(`#pause_${newsletter.id}`).show();
jQuery('#resume_' + newsletter.id).hide(); jQuery(`#resume_${newsletter.id}`).hide();
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
}); });
}, },
renderQueueStatus: function (newsletter, mailer_log) { renderQueueStatus: function (newsletter, mailerLog) {
if (!newsletter.queue) { if (!newsletter.queue) {
return ( return (
<span>{MailPoet.I18n.t('notSentYet')}</span> <span>{MailPoet.I18n.t('notSentYet')}</span>
); );
} else if (mailer_log.status === 'paused' && newsletter.queue.status !== 'completed') { } else if (mailerLog.status === 'paused' && newsletter.queue.status !== 'completed') {
return ( return (
<span>{MailPoet.I18n.t('paused')}</span> <span>{MailPoet.I18n.t('paused')}</span>
); );
} else { }
if (newsletter.queue.status === 'scheduled') { if (newsletter.queue.status === 'scheduled') {
return ( return (
<span> <span>
{ MailPoet.I18n.t('scheduledFor') } { MailPoet.Date.format(newsletter.queue.scheduled_at) } { MailPoet.I18n.t('scheduledFor') } { MailPoet.Date.format(newsletter.queue.scheduled_at) }
</span> </span>
); );
} }
const progressClasses = classNames( const progressClasses = classNames(
'mailpoet_progress', 'mailpoet_progress',
{ mailpoet_progress_complete: newsletter.queue.status === 'completed' } { mailpoet_progress_complete: newsletter.queue.status === 'completed' }
); );
// calculate percentage done // calculate percentage done
let percentage = Math.round( let percentage = Math.round(
(newsletter.queue.count_processed * 100) / (newsletter.queue.count_total) (newsletter.queue.count_processed * 100) / (newsletter.queue.count_total)
); );
let label; let label;
if (newsletter.queue.status === 'completed') { if (newsletter.queue.status === 'completed') {
label = ( label = (
<span> <span>
{ {
MailPoet.I18n.t('newsletterQueueCompleted') MailPoet.I18n.t('newsletterQueueCompleted')
.replace('%$1d', parseInt(newsletter.queue.count_processed, 10).toLocaleString()) .replace('%$1d', parseInt(newsletter.queue.count_processed, 10).toLocaleString())
.replace('%$2d', parseInt(newsletter.queue.count_total, 10).toLocaleString()) .replace('%$2d', parseInt(newsletter.queue.count_total, 10).toLocaleString())
} }
</span> </span>
); );
} else { } else {
label = ( label = (
<span> <span>
{ newsletter.queue.count_processed } / { newsletter.queue.count_total } { newsletter.queue.count_processed } / { newsletter.queue.count_total }
&nbsp;&nbsp; &nbsp;&nbsp;
<a <a
id={'resume_' + newsletter.id} id={`resume_${newsletter.id}`}
className="button" className="button"
style={{ display: (newsletter.queue.status === 'paused') style={{ display: (newsletter.queue.status === 'paused')
? 'inline-block' : 'none' }} ? 'inline-block' : 'none' }}
href="javascript:;" href="javascript:;"
onClick={this.resumeSending.bind(null, newsletter)} onClick={this.resumeSending.bind(null, newsletter)}
>{MailPoet.I18n.t('resume')}</a> >{MailPoet.I18n.t('resume')}</a>
<a <a
id={'pause_' + newsletter.id} id={`pause_${newsletter.id}`}
className="button mailpoet_pause" className="button mailpoet_pause"
style={{ display: (newsletter.queue.status === null) style={{ display: (newsletter.queue.status === null)
? 'inline-block' : 'none' }} ? 'inline-block' : 'none' }}
href="javascript:;" href="javascript:;"
onClick={this.pauseSending.bind(null, newsletter)} onClick={this.pauseSending.bind(null, newsletter)}
>{MailPoet.I18n.t('pause')}</a> >{MailPoet.I18n.t('pause')}</a>
</span> </span>
); );
}
let progress_bar_width = 0;
if (isNaN(percentage)) {
percentage = MailPoet.I18n.t('noSubscribers');
} else {
progress_bar_width = percentage;
percentage += '%';
}
return (
<div>
<div className={progressClasses}>
<span
className="mailpoet_progress_bar"
style={{ width: progress_bar_width + '%' }}
></span>
<span className="mailpoet_progress_label">
{ percentage }
</span>
</div>
<p style={{ textAlign: 'center' }}>
{ label }
</p>
</div>
);
} }
let progressBarWidth = 0;
if (isNaN(percentage)) {
percentage = MailPoet.I18n.t('noSubscribers');
} else {
progressBarWidth = percentage;
percentage += '%';
}
return (
<div>
<div className={progressClasses}>
<span
className="mailpoet_progress_bar"
style={{ width: `${progressBarWidth}%` }}
></span>
<span className="mailpoet_progress_label">
{ percentage }
</span>
</div>
<p style={{ textAlign: 'center' }}>
{ label }
</p>
</div>
);
}, },
}; };
@ -150,9 +149,9 @@ const trackStatsCTAClicked = function () {
); );
}; };
const _StatisticsMixin = { const StatisticsMixin = {
renderStatistics: function (newsletter, is_sent, current_time) { renderStatistics: function (newsletter, isSent, currentTime) {
let sent = is_sent; let sent = isSent;
if (sent === undefined) { if (sent === undefined) {
// condition for standard and post notification listings // condition for standard and post notification listings
sent = newsletter.statistics sent = newsletter.statistics
@ -170,74 +169,74 @@ const _StatisticsMixin = {
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter); params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
// welcome emails provide explicit total_sent value // welcome emails provide explicit total_sent value
const total_sent = ~~(newsletter.total_sent || newsletter.queue.count_processed); const totalSent = Number((newsletter.total_sent || newsletter.queue.count_processed));
let percentage_clicked = 0; let percentageClicked = 0;
let percentage_opened = 0; let percentageOpened = 0;
let percentage_unsubscribed = 0; let percentageUnsubscribed = 0;
if (total_sent > 0) { if (totalSent > 0) {
percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent; percentageClicked = (newsletter.statistics.clicked * 100) / totalSent;
percentage_opened = (newsletter.statistics.opened * 100) / total_sent; percentageOpened = (newsletter.statistics.opened * 100) / totalSent;
percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent; percentageUnsubscribed = (newsletter.statistics.unsubscribed * 100) / totalSent;
} }
// format to 1 decimal place // format to 1 decimal place
const percentage_clicked_display = MailPoet.Num.toLocaleFixed(percentage_clicked, 1); const percentageClickedDisplay = MailPoet.Num.toLocaleFixed(percentageClicked, 1);
const percentage_opened_display = MailPoet.Num.toLocaleFixed(percentage_opened, 1); const percentageOpenedDisplay = MailPoet.Num.toLocaleFixed(percentageOpened, 1);
const percentage_unsubscribed_display = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1); const percentageUnsubscribedDisplay = MailPoet.Num.toLocaleFixed(percentageUnsubscribed, 1);
let show_stats_timeout; let showStatsTimeout;
let newsletter_date; let newsletterDate;
let sent_hours_ago; let sentHoursAgo;
let too_early_for_stats; let tooEarlyForStats;
let show_kb_link; let showKbLink;
if (current_time !== undefined) { if (currentTime !== undefined) {
// standard emails and post notifications: // standard emails and post notifications:
// display green box for newsletters that were just sent // display green box for newsletters that were just sent
show_stats_timeout = 6; // in hours showStatsTimeout = 6; // in hours
newsletter_date = newsletter.queue.scheduled_at || newsletter.queue.created_at; newsletterDate = newsletter.queue.scheduled_at || newsletter.queue.created_at;
sent_hours_ago = moment(current_time).diff(moment(newsletter_date), 'hours'); sentHoursAgo = moment(currentTime).diff(moment(newsletterDate), 'hours');
too_early_for_stats = sent_hours_ago < show_stats_timeout; tooEarlyForStats = sentHoursAgo < showStatsTimeout;
show_kb_link = true; showKbLink = true;
} else { } else {
// welcome emails: no green box and KB link // welcome emails: no green box and KB link
too_early_for_stats = false; tooEarlyForStats = false;
show_kb_link = false; showKbLink = false;
} }
const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/191-how-to-improve-my-open-and-click-rates'; const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/191-how-to-improve-my-open-and-click-rates';
// thresholds to display badges // thresholds to display badges
const min_newsletters_sent = 20; const minNewslettersSent = 20;
const min_newsletter_opens = 5; const minNewsletterOpens = 5;
let content; let content;
if (total_sent >= min_newsletters_sent if (totalSent >= minNewslettersSent
&& newsletter.statistics.opened >= min_newsletter_opens && newsletter.statistics.opened >= minNewsletterOpens
&& !too_early_for_stats && !tooEarlyForStats
) { ) {
// display stats with badges // display stats with badges
content = ( content = (
<div className="mailpoet_stats_text"> <div className="mailpoet_stats_text">
<div> <div>
<span>{ percentage_opened_display }% </span> <span>{ percentageOpenedDisplay }% </span>
<StatsBadge <StatsBadge
stat="opened" stat="opened"
rate={percentage_opened} rate={percentageOpened}
tooltipId={`opened-${newsletter.id}`} tooltipId={`opened-${newsletter.id}`}
/> />
</div> </div>
<div> <div>
<span>{ percentage_clicked_display }% </span> <span>{ percentageClickedDisplay }% </span>
<StatsBadge <StatsBadge
stat="clicked" stat="clicked"
rate={percentage_clicked} rate={percentageClicked}
tooltipId={`clicked-${newsletter.id}`} tooltipId={`clicked-${newsletter.id}`}
/> />
</div> </div>
<div> <div>
<span className="mailpoet_stat_hidden">{ percentage_unsubscribed_display }%</span> <span className="mailpoet_stat_hidden">{ percentageUnsubscribedDisplay }%</span>
</div> </div>
</div> </div>
); );
@ -246,17 +245,17 @@ const _StatisticsMixin = {
content = ( content = (
<div> <div>
<span className="mailpoet_stats_text"> <span className="mailpoet_stats_text">
{ percentage_opened_display }%, { percentageOpenedDisplay }%,
{ ' ' } { ' ' }
{ percentage_clicked_display }% { percentageClickedDisplay }%
<span className="mailpoet_stat_hidden"> <span className="mailpoet_stat_hidden">
, { percentage_unsubscribed_display }% , { percentageUnsubscribedDisplay }%
</span> </span>
</span> </span>
{ too_early_for_stats && ( { tooEarlyForStats && (
<div className="mailpoet_badge mailpoet_badge_green"> <div className="mailpoet_badge mailpoet_badge_green">
{MailPoet.I18n.t('checkBackInHours') {MailPoet.I18n.t('checkBackInHours')
.replace('%$1d', show_stats_timeout - sent_hours_ago)} .replace('%$1d', showStatsTimeout - sentHoursAgo)}
</div> </div>
) } ) }
</div> </div>
@ -264,18 +263,18 @@ const _StatisticsMixin = {
} }
// thresholds to display bad open rate help // thresholds to display bad open rate help
const max_percentage_opened = 5; const maxPercentageOpened = 5;
const min_sent_hours_ago = 24; const minSentHoursAgo = 24;
const min_total_sent = 10; const minTotalSent = 10;
let after_content; let afterContent;
if (show_kb_link if (showKbLink
&& percentage_opened < max_percentage_opened && percentageOpened < maxPercentageOpened
&& sent_hours_ago >= min_sent_hours_ago && sentHoursAgo >= minSentHoursAgo
&& total_sent >= min_total_sent && totalSent >= minTotalSent
) { ) {
// help link for bad open rate // help link for bad open rate
after_content = ( afterContent = (
<div> <div>
<a <a
href={improveStatsKBLink} href={improveStatsKBLink}
@ -288,7 +287,7 @@ const _StatisticsMixin = {
); );
} }
if (total_sent > 0 && params.link) { if (totalSent > 0 && params.link) {
// wrap content in a link // wrap content in a link
if (params.externalLink) { if (params.externalLink) {
return ( return (
@ -300,29 +299,28 @@ const _StatisticsMixin = {
> >
{content} {content}
</a> </a>
{after_content} {afterContent}
</div>
);
} else {
return (
<div>
<Link
key={`stats-${newsletter.id}`}
to={params.link}
onClick={params.onClick || null}
>
{content}
</Link>
{after_content}
</div> </div>
); );
} }
return (
<div>
<Link
key={`stats-${newsletter.id}`}
to={params.link}
onClick={params.onClick || null}
>
{content}
</Link>
{afterContent}
</div>
);
} }
return ( return (
<div> <div>
{content} {content}
{after_content} {afterContent}
</div> </div>
); );
}, },
@ -341,8 +339,8 @@ const _StatisticsMixin = {
}, },
display: function (newsletter) { display: function (newsletter) {
// welcome emails provide explicit total_sent value // welcome emails provide explicit total_sent value
const count_processed = newsletter.queue && newsletter.queue.count_processed; const countProcessed = newsletter.queue && newsletter.queue.count_processed;
return ~~(newsletter.total_sent || count_processed) > 0; return Number(newsletter.total_sent || countProcessed) > 0;
}, },
}); });
return actions; return actions;
@ -359,7 +357,7 @@ const _StatisticsMixin = {
}, },
}; };
const _MailerMixin = { const MailerMixin = {
checkMailerStatus: function (state) { checkMailerStatus: function (state) {
if (state.meta.mta_log.error && state.meta.mta_log.status === 'paused') { if (state.meta.mta_log.error && state.meta.mta_log.status === 'paused') {
MailPoet.Notice.error( MailPoet.Notice.error(
@ -376,8 +374,8 @@ const _MailerMixin = {
} }
}, },
getMailerError(state) { getMailerError(state) {
let mailer_error_notice; let mailerErrorNotice;
const mailer_check_settings_notice = ReactStringReplace( const mailerCheckSettingsNotice = ReactStringReplace(
MailPoet.I18n.t('mailerCheckSettingsNotice'), MailPoet.I18n.t('mailerCheckSettingsNotice'),
/\[link\](.*?)\[\/link\]/g, /\[link\](.*?)\[\/link\]/g,
match => ( match => (
@ -385,19 +383,23 @@ const _MailerMixin = {
) )
); );
if (state.meta.mta_log.error.operation === 'send') { if (state.meta.mta_log.error.operation === 'send') {
mailer_error_notice = mailerErrorNotice =
MailPoet.I18n.t('mailerSendErrorNotice') MailPoet.I18n.t('mailerSendErrorNotice')
.replace('%$1s', state.meta.mta_method) .replace('%$1s', state.meta.mta_method)
.replace('%$2s', state.meta.mta_log.error.error_message); .replace('%$2s', state.meta.mta_log.error.error_message);
} else { } else {
mailer_error_notice = mailerErrorNotice =
MailPoet.I18n.t('mailerConnectionErrorNotice') MailPoet.I18n.t('mailerConnectionErrorNotice')
.replace('%$1s', state.meta.mta_log.error.error_message); .replace('%$1s', state.meta.mta_log.error.error_message);
} }
if (state.meta.mta_log.error.error_code) {
mailerErrorNotice += ` ${MailPoet.I18n.t('mailerErrorCode')
.replace('%$1s', state.meta.mta_log.error.error_code)}`;
}
return ( return (
<div> <div>
<p>{ mailer_error_notice }</p> <p>{ mailerErrorNotice }</p>
<p>{ mailer_check_settings_notice }</p> <p>{ mailerCheckSettingsNotice }</p>
<p> <p>
<a href="javascript:;" <a href="javascript:;"
className="button" className="button"
@ -419,7 +421,7 @@ const _MailerMixin = {
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -428,6 +430,6 @@ const _MailerMixin = {
}; };
export { _QueueMixin as QueueMixin }; export { QueueMixin };
export { _StatisticsMixin as StatisticsMixin }; export { StatisticsMixin };
export { _MailerMixin as MailerMixin }; export { MailerMixin };

View File

@ -18,7 +18,7 @@ import {
const messages = { const messages = {
onTrash: (response) => { onTrash: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -33,7 +33,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onDelete: (response) => { onDelete: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -48,7 +48,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onRestore: (response) => { onRestore: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -91,7 +91,7 @@ const columns = [
}, },
]; ];
const bulk_actions = [ const bulkActions = [
{ {
name: 'trash', name: 'trash',
label: MailPoet.I18n.t('moveToTrash'), label: MailPoet.I18n.t('moveToTrash'),
@ -99,7 +99,7 @@ const bulk_actions = [
}, },
]; ];
const newsletter_actions = [ const newsletterActions = [
{ {
name: 'view', name: 'view',
link: function (newsletter) { link: function (newsletter) {
@ -141,7 +141,7 @@ const newsletter_actions = [
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -165,7 +165,7 @@ const NewsletterListNotification = React.createClass({
endpoint: 'newsletters', endpoint: 'newsletters',
action: 'setStatus', action: 'setStatus',
data: { data: {
id: ~~(e.target.getAttribute('data-id')), id: Number(e.target.getAttribute('data-id')),
status: e.target.value, status: e.target.value,
}, },
}).done((response) => { }).done((response) => {
@ -195,12 +195,12 @@ const NewsletterListNotification = React.createClass({
}, },
renderSettings: function (newsletter) { renderSettings: function (newsletter) {
let sendingFrequency; let sendingFrequency;
let sendingToSegments;
// get list of segments' name // get list of segments' name
const segments = newsletter.segments.map((segment) => { const segments = newsletter.segments.map(segment => segment.name);
return segment.name; const sendingToSegments = MailPoet.I18n.t('ifNewContentToSegments').replace(
}); '%$1s', segments.join(', ')
);
// check if the user has specified segments to send to // check if the user has specified segments to send to
if (segments.length === 0) { if (segments.length === 0) {
@ -209,51 +209,52 @@ const NewsletterListNotification = React.createClass({
{ MailPoet.I18n.t('sendingToSegmentsNotSpecified') } { MailPoet.I18n.t('sendingToSegmentsNotSpecified') }
</span> </span>
); );
} else { }
sendingToSegments = MailPoet.I18n.t('ifNewContentToSegments').replace(
'%$1s', segments.join(', ')
);
// set sending frequency // set sending frequency
switch (newsletter.options.intervalType) { switch (newsletter.options.intervalType) {
case 'daily': case 'daily':
sendingFrequency = MailPoet.I18n.t('sendDaily').replace( sendingFrequency = MailPoet.I18n.t('sendDaily').replace(
'%$1s', timeOfDayValues[newsletter.options.timeOfDay] '%$1s', timeOfDayValues[newsletter.options.timeOfDay]
); );
break; break;
case 'weekly': case 'weekly':
sendingFrequency = MailPoet.I18n.t('sendWeekly').replace( sendingFrequency = MailPoet.I18n.t('sendWeekly').replace(
'%$1s', weekDayValues[newsletter.options.weekDay] '%$1s', weekDayValues[newsletter.options.weekDay]
).replace( ).replace(
'%$2s', timeOfDayValues[newsletter.options.timeOfDay] '%$2s', timeOfDayValues[newsletter.options.timeOfDay]
); );
break; break;
case 'monthly': case 'monthly':
sendingFrequency = MailPoet.I18n.t('sendMonthly').replace( sendingFrequency = MailPoet.I18n.t('sendMonthly').replace(
'%$1s', monthDayValues[newsletter.options.monthDay] '%$1s', monthDayValues[newsletter.options.monthDay]
).replace( ).replace(
'%$2s', timeOfDayValues[newsletter.options.timeOfDay] '%$2s', timeOfDayValues[newsletter.options.timeOfDay]
); );
break; break;
case 'nthWeekDay': case 'nthWeekDay':
sendingFrequency = MailPoet.I18n.t('sendNthWeekDay').replace( sendingFrequency = MailPoet.I18n.t('sendNthWeekDay').replace(
'%$1s', nthWeekDayValues[newsletter.options.nthWeekDay] '%$1s', nthWeekDayValues[newsletter.options.nthWeekDay]
).replace( ).replace(
'%$2s', weekDayValues[newsletter.options.weekDay] '%$2s', weekDayValues[newsletter.options.weekDay]
).replace( ).replace(
'%$3s', timeOfDayValues[newsletter.options.timeOfDay] '%$3s', timeOfDayValues[newsletter.options.timeOfDay]
); );
break; break;
case 'immediately': case 'immediately':
sendingFrequency = MailPoet.I18n.t('sendImmediately'); sendingFrequency = MailPoet.I18n.t('sendImmediately');
break; break;
}
default:
sendingFrequency = 'Invalid sending frequency';
break;
} }
return ( return (
<span> <span>
{ sendingFrequency } { sendingToSegments } { sendingFrequency } { sendingToSegments }
@ -261,18 +262,17 @@ const NewsletterListNotification = React.createClass({
); );
}, },
renderHistoryLink: function (newsletter) { renderHistoryLink: function (newsletter) {
const childrenCount = ~~(newsletter.children_count); const childrenCount = Number((newsletter.children_count));
if (childrenCount === 0) { if (childrenCount === 0) {
return ( return (
MailPoet.I18n.t('notSentYet') MailPoet.I18n.t('notSentYet')
); );
} else {
return (
<Link
to={`/notification/history/${newsletter.id}`}
>{ MailPoet.I18n.t('viewHistory') }</Link>
);
} }
return (
<Link
to={`/notification/history/${newsletter.id}`}
>{ MailPoet.I18n.t('viewHistory') }</Link>
);
}, },
renderItem: function (newsletter, actions) { renderItem: function (newsletter, actions) {
const rowClasses = classNames( const rowClasses = classNames(
@ -311,7 +311,7 @@ const NewsletterListNotification = React.createClass({
return ( return (
<div> <div>
<h1 className="title"> <h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link> {MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new" data-automation-id="new_email">{MailPoet.I18n.t('new')}</Link>
</h1> </h1>
<ListingTabs tab="notification" /> <ListingTabs tab="notification" />
@ -325,8 +325,8 @@ const NewsletterListNotification = React.createClass({
base_url="notification" base_url="notification"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
item_actions={newsletter_actions} item_actions={newsletterActions}
messages={messages} messages={messages}
auto_refresh={true} auto_refresh={true}
sort_by="updated_at" sort_by="updated_at"

View File

@ -13,7 +13,7 @@ import {
MailerMixin, MailerMixin,
} from 'newsletters/listings/mixins.jsx'; } from 'newsletters/listings/mixins.jsx';
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled'])); const mailpoetTrackingEnabled = (!!(window.mailpoet_tracking_enabled));
const columns = [ const columns = [
{ {
@ -31,7 +31,7 @@ const columns = [
{ {
name: 'statistics', name: 'statistics',
label: MailPoet.I18n.t('statistics'), label: MailPoet.I18n.t('statistics'),
display: mailpoet_tracking_enabled, display: mailpoetTrackingEnabled,
}, },
{ {
name: 'sent_at', name: 'sent_at',
@ -39,7 +39,7 @@ const columns = [
}, },
]; ];
let newsletter_actions = [ let newsletterActions = [
{ {
name: 'view', name: 'view',
link: function (newsletter) { link: function (newsletter) {
@ -53,7 +53,7 @@ let newsletter_actions = [
]; ];
Hooks.addFilter('mailpoet_newsletters_listings_notification_history_actions', StatisticsMixin.addStatsCTAAction); Hooks.addFilter('mailpoet_newsletters_listings_notification_history_actions', StatisticsMixin.addStatsCTAAction);
newsletter_actions = Hooks.applyFilters('mailpoet_newsletters_listings_notification_history_actions', newsletter_actions); newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_notification_history_actions', newsletterActions);
const NewsletterListNotificationHistory = React.createClass({ const NewsletterListNotificationHistory = React.createClass({
mixins: [QueueMixin, StatisticsMixin, MailerMixin], mixins: [QueueMixin, StatisticsMixin, MailerMixin],
@ -64,9 +64,7 @@ const NewsletterListNotificationHistory = React.createClass({
'has-row-actions' 'has-row-actions'
); );
const segments = newsletter.segments.map((segment) => { const segments = newsletter.segments.map(segment => segment.name).join(', ');
return segment.name;
}).join(', ');
return ( return (
<div> <div>
@ -85,7 +83,7 @@ const NewsletterListNotificationHistory = React.createClass({
<td className="column" data-colname={MailPoet.I18n.t('lists')}> <td className="column" data-colname={MailPoet.I18n.t('lists')}>
{ segments } { segments }
</td> </td>
{ (mailpoet_tracking_enabled === true) ? ( { (mailpoetTrackingEnabled === true) ? (
<td className="column" data-colname={MailPoet.I18n.t('statistics')}> <td className="column" data-colname={MailPoet.I18n.t('statistics')}>
{ this.renderStatistics(newsletter, undefined, meta.current_time) } { this.renderStatistics(newsletter, undefined, meta.current_time) }
</td> </td>
@ -100,7 +98,7 @@ const NewsletterListNotificationHistory = React.createClass({
return ( return (
<div> <div>
<h1 className="title"> <h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link> {MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new" data-automation-id="new_email">{MailPoet.I18n.t('new')}</Link>
</h1> </h1>
<ListingTabs tab="notification" /> <ListingTabs tab="notification" />
@ -119,7 +117,7 @@ const NewsletterListNotificationHistory = React.createClass({
base_url="notification/history/:parent_id" base_url="notification/history/:parent_id"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}
item_actions={newsletter_actions} item_actions={newsletterActions}
auto_refresh={true} auto_refresh={true}
sort_by="sent_at" sort_by="sent_at"
sort_order="desc" sort_order="desc"

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { confirmAlert } from 'react-confirm-alert';
import classNames from 'classnames'; import classNames from 'classnames';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
@ -13,11 +14,11 @@ import {
MailerMixin, MailerMixin,
} from 'newsletters/listings/mixins.jsx'; } from 'newsletters/listings/mixins.jsx';
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled'])); const mailpoetTrackingEnabled = (!!(window.mailpoet_tracking_enabled));
const messages = { const messages = {
onTrash: (response) => { onTrash: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -32,7 +33,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onDelete: (response) => { onDelete: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -47,7 +48,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onRestore: (response) => { onRestore: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -80,7 +81,7 @@ const columns = [
{ {
name: 'statistics', name: 'statistics',
label: MailPoet.I18n.t('statistics'), label: MailPoet.I18n.t('statistics'),
display: mailpoet_tracking_enabled, display: mailpoetTrackingEnabled,
}, },
{ {
name: 'sent_at', name: 'sent_at',
@ -89,7 +90,7 @@ const columns = [
}, },
]; ];
const bulk_actions = [ const bulkActions = [
{ {
name: 'trash', name: 'trash',
label: MailPoet.I18n.t('moveToTrash'), label: MailPoet.I18n.t('moveToTrash'),
@ -98,17 +99,27 @@ const bulk_actions = [
]; ];
const confirmEdit = (newsletter) => { const confirmEdit = (newsletter) => {
const redirectToEditing = () => {
window.location.href = `?page=mailpoet-newsletter-editor&id=${newsletter.id}`;
};
if ( if (
!newsletter.queue !newsletter.queue
|| newsletter.status != 'sending' || newsletter.status !== 'sending'
|| newsletter.queue.status !== null || newsletter.queue.status !== null
|| window.confirm(MailPoet.I18n.t('confirmEdit'))
) { ) {
window.location.href = `?page=mailpoet-newsletter-editor&id=${newsletter.id}`; redirectToEditing();
} else {
confirmAlert({
title: MailPoet.I18n.t('confirmTitle'),
message: MailPoet.I18n.t('confirmEdit'),
confirmLabel: MailPoet.I18n.t('confirmLabel'),
cancelLabel: MailPoet.I18n.t('cancelLabel'),
onConfirm: redirectToEditing,
});
} }
}; };
let newsletter_actions = [ let newsletterActions = [
{ {
name: 'view', name: 'view',
link: function (newsletter) { link: function (newsletter) {
@ -145,7 +156,7 @@ let newsletter_actions = [
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -158,7 +169,7 @@ let newsletter_actions = [
]; ];
Hooks.addFilter('mailpoet_newsletters_listings_standard_actions', StatisticsMixin.addStatsCTAAction); Hooks.addFilter('mailpoet_newsletters_listings_standard_actions', StatisticsMixin.addStatsCTAAction);
newsletter_actions = Hooks.applyFilters('mailpoet_newsletters_listings_standard_actions', newsletter_actions); newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_standard_actions', newsletterActions);
const NewsletterListStandard = React.createClass({ const NewsletterListStandard = React.createClass({
mixins: [QueueMixin, StatisticsMixin, MailerMixin], mixins: [QueueMixin, StatisticsMixin, MailerMixin],
@ -169,9 +180,7 @@ const NewsletterListStandard = React.createClass({
'has-row-actions' 'has-row-actions'
); );
const segments = newsletter.segments.map((segment) => { const segments = newsletter.segments.map(segment => segment.name).join(', ');
return segment.name;
}).join(', ');
return ( return (
<div> <div>
@ -191,7 +200,7 @@ const NewsletterListStandard = React.createClass({
<td className="column" data-colname={MailPoet.I18n.t('lists')}> <td className="column" data-colname={MailPoet.I18n.t('lists')}>
{ segments } { segments }
</td> </td>
{ (mailpoet_tracking_enabled === true) ? ( { (mailpoetTrackingEnabled === true) ? (
<td className="column" data-colname={MailPoet.I18n.t('statistics')}> <td className="column" data-colname={MailPoet.I18n.t('statistics')}>
{ this.renderStatistics(newsletter, undefined, meta.current_time) } { this.renderStatistics(newsletter, undefined, meta.current_time) }
</td> </td>
@ -211,6 +220,7 @@ const NewsletterListStandard = React.createClass({
onClick={() => MailPoet.trackEvent('Emails > Add New', onClick={() => MailPoet.trackEvent('Emails > Add New',
{ 'MailPoet Free version': window.mailpoet_version } { 'MailPoet Free version': window.mailpoet_version }
)} )}
data-automation-id="new_email"
> >
{MailPoet.I18n.t('new')} {MailPoet.I18n.t('new')}
</Link> </Link>
@ -227,8 +237,8 @@ const NewsletterListStandard = React.createClass({
base_url="standard" base_url="standard"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
item_actions={newsletter_actions} item_actions={newsletterActions}
messages={messages} messages={messages}
auto_refresh={true} auto_refresh={true}
sort_by="sent_at" sort_by="sent_at"

View File

@ -35,7 +35,7 @@ const ListingTabs = React.createClass({
return ( return (
<Link <Link
key={'tab-' + index} key={`tab-${index}`}
className={tabClasses} className={tabClasses}
to={tab.link} to={tab.link}
onClick={() => MailPoet.trackEvent(`Tab Emails > ${tab.name} clicked`, onClick={() => MailPoet.trackEvent(`Tab Emails > ${tab.name} clicked`,
@ -46,7 +46,7 @@ const ListingTabs = React.createClass({
}); });
return ( return (
<h2 className="nav-tab-wrapper"> <h2 className="nav-tab-wrapper" data-automation-id="newsletters_listing_tabs">
{ tabs } { tabs }
</h2> </h2>
); );

View File

@ -11,13 +11,13 @@ import MailPoet from 'mailpoet';
import _ from 'underscore'; import _ from 'underscore';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
const mailpoet_roles = window.mailpoet_roles || {}; const mailpoetRoles = window.mailpoet_roles || {};
const mailpoet_segments = window.mailpoet_segments || {}; const mailpoetSegments = window.mailpoet_segments || {};
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled'])); const mailpoetTrackingEnabled = (!!(window.mailpoet_tracking_enabled));
const messages = { const messages = {
onTrash: (response) => { onTrash: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -32,7 +32,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onDelete: (response) => { onDelete: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -47,7 +47,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onRestore: (response) => { onRestore: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -81,7 +81,7 @@ const columns = [
{ {
name: 'statistics', name: 'statistics',
label: MailPoet.I18n.t('statistics'), label: MailPoet.I18n.t('statistics'),
display: mailpoet_tracking_enabled, display: mailpoetTrackingEnabled,
}, },
{ {
name: 'updated_at', name: 'updated_at',
@ -90,7 +90,7 @@ const columns = [
}, },
]; ];
const bulk_actions = [ const bulkActions = [
{ {
name: 'trash', name: 'trash',
label: MailPoet.I18n.t('moveToTrash'), label: MailPoet.I18n.t('moveToTrash'),
@ -98,7 +98,7 @@ const bulk_actions = [
}, },
]; ];
let newsletter_actions = [ let newsletterActions = [
{ {
name: 'view', name: 'view',
link: function (newsletter) { link: function (newsletter) {
@ -125,7 +125,7 @@ let newsletter_actions = [
]; ];
Hooks.addFilter('mailpoet_newsletters_listings_welcome_notification_actions', StatisticsMixin.addStatsCTAAction); Hooks.addFilter('mailpoet_newsletters_listings_welcome_notification_actions', StatisticsMixin.addStatsCTAAction);
newsletter_actions = Hooks.applyFilters('mailpoet_newsletters_listings_welcome_notification_actions', newsletter_actions); newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_welcome_notification_actions', newsletterActions);
const NewsletterListWelcome = React.createClass({ const NewsletterListWelcome = React.createClass({
mixins: [StatisticsMixin, MailerMixin], mixins: [StatisticsMixin, MailerMixin],
@ -139,7 +139,7 @@ const NewsletterListWelcome = React.createClass({
endpoint: 'newsletters', endpoint: 'newsletters',
action: 'setStatus', action: 'setStatus',
data: { data: {
id: ~~(e.target.getAttribute('data-id')), id: Number(e.target.getAttribute('data-id')),
status: e.target.value, status: e.target.value,
}, },
}).done((response) => { }).done((response) => {
@ -156,7 +156,7 @@ const NewsletterListWelcome = React.createClass({
}); });
}, },
renderStatus: function (newsletter) { renderStatus: function (newsletter) {
const total_sent = ( const totalSent = (
MailPoet.I18n.t('sentToXSubscribers') MailPoet.I18n.t('sentToXSubscribers')
.replace('%$1d', newsletter.total_sent.toLocaleString()) .replace('%$1d', newsletter.total_sent.toLocaleString())
); );
@ -173,13 +173,14 @@ const NewsletterListWelcome = React.createClass({
<option value="draft">{ MailPoet.I18n.t('inactive') }</option> <option value="draft">{ MailPoet.I18n.t('inactive') }</option>
</select> </select>
</p> </p>
<p>{ total_sent }</p> <p>{ totalSent }</p>
</div> </div>
); );
}, },
renderSettings: function (newsletter) { renderSettings: function (newsletter) {
let sendingEvent; let sendingEvent;
let sendingDelay; let sendingDelay;
let segment;
// set sending event // set sending event
switch (newsletter.options.event) { switch (newsletter.options.event) {
@ -189,16 +190,17 @@ const NewsletterListWelcome = React.createClass({
sendingEvent = MailPoet.I18n.t('welcomeEventWPUserAnyRole'); sendingEvent = MailPoet.I18n.t('welcomeEventWPUserAnyRole');
} else { } else {
sendingEvent = MailPoet.I18n.t('welcomeEventWPUserWithRole').replace( sendingEvent = MailPoet.I18n.t('welcomeEventWPUserWithRole').replace(
'%$1s', mailpoet_roles[newsletter.options.role] '%$1s', mailpoetRoles[newsletter.options.role]
); );
} }
break; break;
case 'segment': default:
// get segment // get segment
const segment = _.find(mailpoet_segments, (segment) => { segment = _.find(
return (~~(segment.id) === ~~(newsletter.options.segment)); mailpoetSegments,
}); seg => (Number(seg.id) === Number(newsletter.options.segment))
);
if (segment === undefined) { if (segment === undefined) {
return ( return (
@ -206,11 +208,11 @@ const NewsletterListWelcome = React.createClass({
{ MailPoet.I18n.t('sendingToSegmentsNotSpecified') } { MailPoet.I18n.t('sendingToSegmentsNotSpecified') }
</span> </span>
); );
} else { }
sendingEvent = MailPoet.I18n.t('welcomeEventSegment').replace( sendingEvent = MailPoet.I18n.t('welcomeEventSegment').replace(
'%$1s', segment.name '%$1s', segment.name
); );
}
break; break;
} }
@ -235,8 +237,12 @@ const NewsletterListWelcome = React.createClass({
'%$1d', newsletter.options.afterTimeNumber '%$1d', newsletter.options.afterTimeNumber
); );
break; break;
default:
sendingDelay = 'Invalid sending delay';
break;
} }
sendingEvent += ' [' + sendingDelay + ']'; sendingEvent += ` [${sendingDelay}]`;
} }
// add a "period" at the end if we do have a sendingEvent // add a "period" at the end if we do have a sendingEvent
sendingEvent += '.'; sendingEvent += '.';
@ -272,7 +278,7 @@ const NewsletterListWelcome = React.createClass({
<td className="column" data-colname={MailPoet.I18n.t('settings')}> <td className="column" data-colname={MailPoet.I18n.t('settings')}>
{ this.renderSettings(newsletter) } { this.renderSettings(newsletter) }
</td> </td>
{ (mailpoet_tracking_enabled === true) ? ( { (mailpoetTrackingEnabled === true) ? (
<td className="column" data-colname={MailPoet.I18n.t('statistics')}> <td className="column" data-colname={MailPoet.I18n.t('statistics')}>
{ this.renderStatistics( { this.renderStatistics(
newsletter, newsletter,
@ -290,7 +296,7 @@ const NewsletterListWelcome = React.createClass({
return ( return (
<div> <div>
<h1 className="title"> <h1 className="title">
{ MailPoet.I18n.t('pageTitle') } <Link className="page-title-action" to="/new">{ MailPoet.I18n.t('new') }</Link> { MailPoet.I18n.t('pageTitle') } <Link className="page-title-action" to="/new" data-automation-id="new_email">{ MailPoet.I18n.t('new') }</Link>
</h1> </h1>
<ListingTabs tab="welcome" /> <ListingTabs tab="welcome" />
@ -304,8 +310,8 @@ const NewsletterListWelcome = React.createClass({
base_url="welcome" base_url="welcome"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
item_actions={newsletter_actions} item_actions={newsletterActions}
messages={messages} messages={messages}
auto_refresh={true} auto_refresh={true}
sort_by="updated_at" sort_by="updated_at"

View File

@ -27,17 +27,18 @@ const App = React.createClass({
const container = document.getElementById('newsletters_container'); const container = document.getElementById('newsletters_container');
if (container) { if (container) {
let extra_routes = []; let extraRoutes = [];
extra_routes = Hooks.applyFilters('mailpoet_newsletters_before_router', extra_routes); extraRoutes = Hooks.applyFilters('mailpoet_newsletters_before_router', extraRoutes);
const mailpoet_listing = ReactDOM.render(( const mailpoetListing = ReactDOM.render((
<Router history={history}> <Router history={history}>
<Route path="/" component={App}> <Route path="/" component={App}>
<IndexRedirect to="standard" /> <IndexRedirect to="standard" />
{/* Listings */} {/* Listings */}
<Route path="standard(/)**" params={{ tab: 'standard' }} component={NewsletterListStandard} /> <Route path="standard(/)**" params={{ tab: 'standard' }} component={NewsletterListStandard} />
<Route path="welcome(/)**" component={NewsletterListWelcome} /> <Route path="welcome(/)**" component={NewsletterListWelcome} />
<Route path="notification/history/:parent_id(/)**" component={NewsletterListNotificationHistory} /> <Route path="notification/history/:parent_id(/)**"
component={NewsletterListNotificationHistory} />
<Route path="notification(/)**" component={NewsletterListNotification} /> <Route path="notification(/)**" component={NewsletterListNotification} />
{/* Newsletter: type selection */} {/* Newsletter: type selection */}
<Route path="new" component={NewsletterTypes} /> <Route path="new" component={NewsletterTypes} />
@ -49,10 +50,10 @@ if (container) {
{/* Sending options */} {/* Sending options */}
<Route path="send/:id" component={NewsletterSend} /> <Route path="send/:id" component={NewsletterSend} />
{/* Extra routes */} {/* Extra routes */}
{ extra_routes.map(rt => <Route key={rt.path} path={rt.path} component={rt.component} />) } { extraRoutes.map(rt => <Route key={rt.path} path={rt.path} component={rt.component} />) }
</Route> </Route>
</Router> </Router>
), container); ), container);
window.mailpoet_listing = mailpoet_listing; window.mailpoet_listing = mailpoetListing;
} }

View File

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

View File

@ -25,7 +25,6 @@ define(
HelpTooltip, HelpTooltip,
jQuery jQuery
) => { ) => {
const NewsletterSend = React.createClass({ const NewsletterSend = React.createClass({
contextTypes: { contextTypes: {
router: React.PropTypes.object.isRequired, router: React.PropTypes.object.isRequired,
@ -95,7 +94,7 @@ define(
if (!this.isValid()) { if (!this.isValid()) {
jQuery('#mailpoet_newsletter').parsley().validate(); jQuery('#mailpoet_newsletter').parsley().validate();
} else { } else {
this._save(e).done(() => { this.saveNewsletter(e).done(() => {
this.setState({ loading: true }); this.setState({ loading: true });
}).done((response) => { }).done((response) => {
switch (response.data.type) { switch (response.data.type) {
@ -109,21 +108,21 @@ define(
id: this.props.params.id, id: this.props.params.id,
status: 'active', status: 'active',
}, },
}).done((response) => { }).done((response2) => {
// redirect to listing based on newsletter type // redirect to listing based on newsletter type
this.context.router.push(`/${this.state.item.type || ''}`); this.context.router.push(`/${this.state.item.type || ''}`);
const opts = this.state.item.options; const opts = this.state.item.options;
// display success message depending on newsletter type // display success message depending on newsletter type
if (response.data.type === 'welcome') { if (response2.data.type === 'welcome') {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('welcomeEmailActivated') MailPoet.I18n.t('welcomeEmailActivated')
); );
MailPoet.trackEvent('Emails > Welcome email activated', { MailPoet.trackEvent('Emails > Welcome email activated', {
'MailPoet Free version': window.mailpoet_version, 'MailPoet Free version': window.mailpoet_version,
'List type': opts.event, 'List type': opts.event,
Delay: opts.afterTimeNumber + ' ' + opts.afterTimeType, Delay: `${opts.afterTimeNumber} ${opts.afterTimeType}`,
}); });
} else if (response.data.type === 'notification') { } else if (response2.data.type === 'notification') {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('postNotificationActivated') MailPoet.I18n.t('postNotificationActivated')
); );
@ -132,7 +131,7 @@ define(
Frequency: opts.intervalType, Frequency: opts.intervalType,
}); });
} }
}).fail(this._showError); }).fail(this.showError);
default: default:
return MailPoet.Ajax.post({ return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
@ -141,11 +140,11 @@ define(
data: { data: {
newsletter_id: this.props.params.id, newsletter_id: this.props.params.id,
}, },
}).done((response) => { }).done((response2) => {
// redirect to listing based on newsletter type // redirect to listing based on newsletter type
this.context.router.push(`/${this.state.item.type || ''}`); this.context.router.push(`/${this.state.item.type || ''}`);
if (response.data.status === 'scheduled') { if (response2.data.status === 'scheduled') {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('newsletterHasBeenScheduled') MailPoet.I18n.t('newsletterHasBeenScheduled')
); );
@ -162,10 +161,10 @@ define(
'MailPoet Free version': window.mailpoet_version, 'MailPoet Free version': window.mailpoet_version,
}); });
} }
}).fail(this._showError); }).fail(this.showError);
} }
}) })
.fail(this._showError) .fail(this.showError)
.always(() => { .always(() => {
this.setState({ loading: false }); this.setState({ loading: false });
}); });
@ -177,7 +176,7 @@ define(
if (!this.isValid()) { if (!this.isValid()) {
jQuery('#mailpoet_newsletter').parsley().validate(); jQuery('#mailpoet_newsletter').parsley().validate();
} else { } else {
this._save(e).done(() => { this.saveNewsletter(e).done(() => {
this.setState({ loading: true }); this.setState({ loading: true });
}).done(() => { }).done(() => {
MailPoet.Ajax.post({ MailPoet.Ajax.post({
@ -195,13 +194,13 @@ define(
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
}); });
}) })
.fail(this._showError) .fail(this.showError)
.always(() => { .always(() => {
this.setState({ loading: false }); this.setState({ loading: false });
}); });
@ -211,27 +210,27 @@ define(
handleSave: function (e) { handleSave: function (e) {
e.preventDefault(); e.preventDefault();
this._save(e).done(() => { this.saveNewsletter(e).done(() => {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('newsletterUpdated') MailPoet.I18n.t('newsletterUpdated')
); );
}).done(() => { }).done(() => {
this.context.router.push(`/${this.state.item.type || ''}`); this.context.router.push(`/${this.state.item.type || ''}`);
}).fail(this._showError); }).fail(this.showError);
}, },
handleRedirectToDesign: function (e) { handleRedirectToDesign: function (e) {
e.preventDefault(); e.preventDefault();
const redirectTo = e.target.href; const redirectTo = e.target.href;
this._save(e).done(() => { this.saveNewsletter(e).done(() => {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('newsletterUpdated') MailPoet.I18n.t('newsletterUpdated')
); );
}).done(() => { }).done(() => {
window.location = redirectTo; window.location = redirectTo;
}).fail(this._showError); }).fail(this.showError);
}, },
_save: function () { saveNewsletter: function () {
const data = this.state.item; const data = this.state.item;
data.queue = undefined; data.queue = undefined;
this.setState({ loading: true }); this.setState({ loading: true });
@ -255,10 +254,10 @@ define(
this.setState({ loading: false }); this.setState({ loading: false });
}); });
}, },
_showError: (response) => { showError: (response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -275,12 +274,12 @@ define(
return true; return true;
}, },
render: function () { render: function () {
const isPaused = this.state.item.status == 'sending' const isPaused = this.state.item.status === 'sending'
&& this.state.item.queue && this.state.item.queue
&& this.state.item.queue.status == 'paused'; && this.state.item.queue.status === 'paused';
const fields = this.state.fields.map((field) => { const fields = this.state.fields.map((field) => {
const newField = field; const newField = field;
if (field.name == 'segments' || field.name == 'options') { if (field.name === 'segments' || field.name === 'options') {
newField.disabled = isPaused; newField.disabled = isPaused;
} }
return newField; return newField;
@ -325,13 +324,13 @@ define(
&nbsp;{MailPoet.I18n.t('orSimply')}&nbsp; &nbsp;{MailPoet.I18n.t('orSimply')}&nbsp;
<a <a
href={ href={
'?page=mailpoet-newsletter-editor&id=' + this.props.params.id `?page=mailpoet-newsletter-editor&id=${this.props.params.id}`
} }
onClick={this.handleRedirectToDesign}> onClick={this.handleRedirectToDesign}>
{MailPoet.I18n.t('goBackToDesign')} {MailPoet.I18n.t('goBackToDesign')}
</a>. </a>.
</p> </p>
{ !isPaused && sendButtonOptions['disabled'] && sendButtonOptions['disabled'] === 'disabled' && ( { !isPaused && sendButtonOptions.disabled && sendButtonOptions.disabled === 'disabled' && (
<HelpTooltip <HelpTooltip
tooltip={MailPoet.I18n.t('helpTooltipSendEmail')} tooltip={MailPoet.I18n.t('helpTooltipSendEmail')}
tooltipId="helpTooltipSendEmail" tooltipId="helpTooltipSendEmail"

View File

@ -11,7 +11,6 @@ define(
Scheduling, Scheduling,
_ _
) => { ) => {
let fields = [ let fields = [
{ {
name: 'subject', name: 'subject',
@ -40,18 +39,14 @@ define(
endpoint: 'segments', endpoint: 'segments',
multiple: true, multiple: true,
filter: function (segment) { filter: function (segment) {
return !!(!segment.deleted_at); return !segment.deleted_at;
}, },
getLabel: function (segment) { getLabel: function (segment) {
return segment.name + ' (' + parseInt(segment.subscribers, 10).toLocaleString() + ')'; return `${segment.name} (${parseInt(segment.subscribers, 10).toLocaleString()})`;
}, },
transformChangedValue: function (segment_ids) { transformChangedValue: function (segmentIds) {
const all_segments = this.state.items; const allSegments = this.state.items;
return _.map(segment_ids, (id) => { return _.map(segmentIds, id => _.find(allSegments, segment => segment.id === id));
return _.find(all_segments, (segment) => {
return segment.id === id;
});
});
}, },
validation: { validation: {
'data-parsley-required': true, 'data-parsley-required': true,

View File

@ -13,11 +13,10 @@ define(
MailPoet, MailPoet,
Hooks Hooks
) => { ) => {
const jQuery = jq; const jQuery = jq;
const currentTime = window.mailpoet_current_time || '00:00'; const currentTime = window.mailpoet_current_time || '00:00';
const defaultDateTime = window.mailpoet_current_date + ' ' + '00:00:00'; const defaultDateTime = `${window.mailpoet_current_date} 00:00:00`;
const timeOfDayItems = window.mailpoet_schedule_time_of_day; const timeOfDayItems = window.mailpoet_schedule_time_of_day;
const dateDisplayFormat = window.mailpoet_date_display_format; const dateDisplayFormat = window.mailpoet_date_display_format;
const dateStorageFormat = window.mailpoet_date_storage_format; const dateStorageFormat = window.mailpoet_date_storage_format;
@ -169,15 +168,13 @@ define(
const TimeSelect = React.createClass({ const TimeSelect = React.createClass({
render: function () { render: function () {
const options = Object.keys(timeOfDayItems).map( const options = Object.keys(timeOfDayItems).map(
(value, index) => { (value, index) => (
return ( <option
<option key={`option-${index}`}
key={'option-' + index} value={value}>
value={value}> { timeOfDayItems[value] }
{ timeOfDayItems[value] } </option>
</option> )
);
}
); );
return ( return (
@ -195,16 +192,16 @@ define(
}); });
const DateTime = React.createClass({ const DateTime = React.createClass({
_DATE_TIME_SEPARATOR: ' ', DATE_TIME_SEPARATOR: ' ',
getInitialState: function () { getInitialState: function () {
return this._buildStateFromProps(this.props); return this.buildStateFromProps(this.props);
}, },
componentWillReceiveProps: function (nextProps) { componentWillReceiveProps: function (nextProps) {
this.setState(this._buildStateFromProps(nextProps)); this.setState(this.buildStateFromProps(nextProps));
}, },
_buildStateFromProps: function (props) { buildStateFromProps: function (props) {
const value = props.value || defaultDateTime; const value = props.value || defaultDateTime;
const [date, time] = value.split(this._DATE_TIME_SEPARATOR); const [date, time] = value.split(this.DATE_TIME_SEPARATOR);
return { return {
date: date, date: date,
time: time, time: time,
@ -220,7 +217,7 @@ define(
}, },
propagateChange: function () { propagateChange: function () {
if (this.props.onChange) { if (this.props.onChange) {
return this.props.onChange({ this.props.onChange({
target: { target: {
name: this.props.name || '', name: this.props.name || '',
value: this.getDateTime(), value: this.getDateTime(),
@ -229,7 +226,7 @@ define(
} }
}, },
getDateTime: function () { getDateTime: function () {
return [this.state.date, this.state.time].join(this._DATE_TIME_SEPARATOR); return [this.state.date, this.state.time].join(this.DATE_TIME_SEPARATOR);
}, },
render: function () { render: function () {
return ( return (
@ -254,7 +251,7 @@ define(
}); });
const StandardScheduling = React.createClass({ const StandardScheduling = React.createClass({
_getCurrentValue: function () { getCurrentValue: function () {
return _.defaults( return _.defaults(
this.props.item[this.props.field.name] || {}, this.props.item[this.props.field.name] || {},
{ {
@ -264,7 +261,7 @@ define(
); );
}, },
handleValueChange: function (event) { handleValueChange: function (event) {
const oldValue = this._getCurrentValue(); const oldValue = this.getCurrentValue();
const newValue = {}; const newValue = {};
newValue[event.target.name] = event.target.value; newValue[event.target.name] = event.target.value;
@ -281,7 +278,7 @@ define(
return this.handleValueChange(changeEvent); return this.handleValueChange(changeEvent);
}, },
isScheduled: function () { isScheduled: function () {
return this._getCurrentValue().isScheduled === '1'; return this.getCurrentValue().isScheduled === '1';
}, },
getDateValidation: function () { getDateValidation: function () {
return { return {
@ -298,7 +295,7 @@ define(
<span id="mailpoet_scheduling"> <span id="mailpoet_scheduling">
<DateTime <DateTime
name="scheduledAt" name="scheduledAt"
value={this._getCurrentValue().scheduledAt} value={this.getCurrentValue().scheduledAt}
onChange={this.handleValueChange} onChange={this.handleValueChange}
disabled={this.props.field.disabled} disabled={this.props.field.disabled}
dateValidation={this.getDateValidation()} /> dateValidation={this.getDateValidation()} />
@ -348,18 +345,14 @@ define(
endpoint: 'segments', endpoint: 'segments',
multiple: true, multiple: true,
filter: function (segment) { filter: function (segment) {
return !!(!segment.deleted_at); return !segment.deleted_at;
}, },
getLabel: function (segment) { getLabel: function (segment) {
return segment.name + ' (' + parseInt(segment.subscribers, 10).toLocaleString() + ')'; return `${segment.name} (${parseInt(segment.subscribers, 10).toLocaleString()})`;
}, },
transformChangedValue: function (segment_ids) { transformChangedValue: function (segmentIds) {
const all_segments = this.state.items; const allSegments = this.state.items;
return _.map(segment_ids, (id) => { return _.map(segmentIds, id => _.find(allSegments, segment => segment.id === id));
return _.find(all_segments, (segment) => {
return segment.id === id;
});
});
}, },
validation: { validation: {
'data-parsley-required': true, 'data-parsley-required': true,
@ -437,7 +430,7 @@ define(
if (newsletterOptions.status === 'sent' if (newsletterOptions.status === 'sent'
|| newsletterOptions.status === 'sending') { || newsletterOptions.status === 'sending') {
options['disabled'] = 'disabled'; options.disabled = 'disabled';
} }
return options; return options;

View File

@ -9,8 +9,6 @@ define(
Hooks, Hooks,
Scheduling Scheduling
) => { ) => {
let fields = [ let fields = [
{ {
name: 'subject', name: 'subject',

View File

@ -1,295 +1,286 @@
define( import React from 'react';
[ import _ from 'underscore';
'react', import MailPoet from 'mailpoet';
'underscore', import { confirmAlert } from 'react-confirm-alert';
'mailpoet', import classNames from 'classnames';
'react-router', import Breadcrumb from 'newsletters/breadcrumb.jsx';
'classnames', import HelpTooltip from 'help-tooltip.jsx';
'newsletters/breadcrumb.jsx',
'help-tooltip.jsx',
],
(
React,
_,
MailPoet,
Router,
classNames,
Breadcrumb,
HelpTooltip
) => {
const ImportTemplate = React.createClass({ const ImportTemplate = React.createClass({
saveTemplate: function (saveTemplate) { saveTemplate: function (saveTemplate) {
const template = saveTemplate; const template = saveTemplate;
// Stringify to enable transmission of primitive non-string value types // Stringify to enable transmission of primitive non-string value types
if (!_.isUndefined(template.body)) { if (!_.isUndefined(template.body)) {
template.body = JSON.stringify(template.body); template.body = JSON.stringify(template.body);
} }
MailPoet.Modal.loading(true); MailPoet.Modal.loading(true);
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
endpoint: 'newsletterTemplates', endpoint: 'newsletterTemplates',
action: 'save', action: 'save',
data: template, data: template,
}).always(() => { }).always(() => {
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
}).done((response) => { }).done((response) => {
this.props.onImport(response.data); this.props.onImport(response.data);
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
);
}
});
},
handleSubmit: function (e) {
e.preventDefault();
if (_.size(this.refs.templateFile.files) <= 0) return false;
const file = _.first(this.refs.templateFile.files);
const reader = new FileReader();
const saveTemplate = this.saveTemplate;
reader.onload = (e) => {
try {
saveTemplate(JSON.parse(e.target.result));
MailPoet.trackEvent('Emails > Template imported', {
'MailPoet Free version': window.mailpoet_version,
});
} catch (err) {
MailPoet.Notice.error(MailPoet.I18n.t('templateFileMalformedError'));
}
};
reader.readAsText(file);
},
render: function () {
return (
<div>
<h2>{MailPoet.I18n.t('importTemplateTitle')} <HelpTooltip
tooltip={MailPoet.I18n.t('helpTooltipTemplateUpload')}
place="right"
className="tooltip-help-import-template"
/></h2>
<form onSubmit={this.handleSubmit}>
<input type="file" placeholder={MailPoet.I18n.t('selectJsonFileToUpload')} ref="templateFile" />
<p className="submit">
<input
className="button button-primary"
type="submit"
value={MailPoet.I18n.t('upload')} />
</p>
</form>
</div>
); );
}, }
}); });
},
handleSubmit: function (e) {
e.preventDefault();
const NewsletterTemplates = React.createClass({ if (_.size(this.refs.templateFile.files) <= 0) return false;
getInitialState: function () {
return {
loading: false,
templates: [],
};
},
componentDidMount: function () {
this.getTemplates();
},
getTemplates: function () {
this.setState({ loading: true });
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({ const file = _.first(this.refs.templateFile.files);
api_version: window.mailpoet_api_version, const reader = new FileReader();
endpoint: 'newsletterTemplates', const saveTemplate = this.saveTemplate;
action: 'getAll',
}).always(() => {
MailPoet.Modal.loading(false);
}).done((response) => {
if (this.isMounted()) {
if (response.data.length === 0) {
response.data = [
{
name:
MailPoet.I18n.t('mailpoetGuideTemplateTitle'),
description:
MailPoet.I18n.t('mailpoetGuideTemplateDescription'),
readonly: '1',
},
];
}
this.setState({
templates: response.data,
loading: false,
});
}
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }),
{ scroll: true }
);
}
});
},
handleSelectTemplate: function (template) {
let body = template.body;
// Stringify to enable transmission of primitive non-string value types reader.onload = (evt) => {
if (!_.isUndefined(body)) { try {
body = JSON.stringify(body); saveTemplate(JSON.parse(evt.target.result));
} MailPoet.trackEvent('Emails > Template imported', {
MailPoet.trackEvent('Emails > Template selected', {
'MailPoet Free version': window.mailpoet_version, 'MailPoet Free version': window.mailpoet_version,
'Email name': template.name,
}); });
} catch (err) {
MailPoet.Notice.error(MailPoet.I18n.t('templateFileMalformedError'));
}
};
MailPoet.Ajax.post({ reader.readAsText(file);
api_version: window.mailpoet_api_version, return true;
endpoint: 'newsletters', },
action: 'save', render: function () {
data: { return (
id: this.props.params.id, <div>
body: body, <h2>{MailPoet.I18n.t('importTemplateTitle')} <HelpTooltip
}, tooltip={MailPoet.I18n.t('helpTooltipTemplateUpload')}
}).done((response) => { place="right"
// TODO: Move this URL elsewhere className="tooltip-help-import-template"
window.location = 'admin.php?page=mailpoet-newsletter-editor&id=' + response.data.id; /></h2>
}).fail((response) => { <form onSubmit={this.handleSubmit}>
if (response.errors.length > 0) { <input type="file" placeholder={MailPoet.I18n.t('selectJsonFileToUpload')} ref="templateFile" />
MailPoet.Notice.error( <p className="submit">
response.errors.map((error) => { return error.message; }), <input
{ scroll: true } className="button button-primary"
); type="submit"
} value={MailPoet.I18n.t('upload')} />
}); </p>
}, </form>
handleDeleteTemplate: function (template) { </div>
this.setState({ loading: true }); );
if ( },
window.confirm( });
(
MailPoet.I18n.t('confirmTemplateDeletion') const NewsletterTemplates = React.createClass({
).replace('%$1s', template.name) getInitialState: function () {
) return {
) { loading: false,
MailPoet.Ajax.post({ templates: [],
api_version: window.mailpoet_api_version, };
endpoint: 'newsletterTemplates', },
action: 'delete', componentDidMount: function () {
data: { this.getTemplates();
id: template.id, },
getTemplates: function () {
this.setState({ loading: true });
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletterTemplates',
action: 'getAll',
}).always(() => {
MailPoet.Modal.loading(false);
}).done((response) => {
if (this.isMounted()) {
if (response.data.length === 0) {
response.data = [
{
name:
MailPoet.I18n.t('mailpoetGuideTemplateTitle'),
description:
MailPoet.I18n.t('mailpoetGuideTemplateDescription'),
readonly: '1',
}, },
}).done(() => { ];
this.getTemplates();
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }),
{ scroll: true }
);
}
});
} else {
this.setState({ loading: false });
} }
}, this.setState({
handleShowTemplate: function (template) { templates: response.data,
MailPoet.Modal.popup({ loading: false,
title: template.name,
template: '<div class="mailpoet_boxes_preview" style="background-color: {{ body.globalStyles.body.backgroundColor }}"><img src="{{ thumbnail }}" /></div>',
data: template,
}); });
}, }
handleTemplateImport: function () { }).fail((response) => {
this.getTemplates(); if (response.errors.length > 0) {
}, MailPoet.Notice.error(
render: function () { response.errors.map(error => error.message),
const templates = this.state.templates.map((template, index) => { { scroll: true }
const deleteLink = (
<div className="mailpoet_delete">
<a
href="javascript:;"
onClick={this.handleDeleteTemplate.bind(null, template)}
>
{MailPoet.I18n.t('delete')}
</a>
</div>
);
let thumbnail = '';
if (typeof template.thumbnail === 'string'
&& template.thumbnail.length > 0) {
thumbnail = (
<a href="javascript:;" onClick={this.handleShowTemplate.bind(null, template)}>
<img src={template.thumbnail} />
<div className="mailpoet_overlay"></div>
</a>
);
}
return (
<li key={'template-' + index}>
<div className="mailpoet_thumbnail">
{ thumbnail }
</div>
<div className="mailpoet_description">
<h3>{ template.name }</h3>
<p>{ template.description }</p>
</div>
<div className="mailpoet_actions">
<a
className="button button-secondary"
onClick={this.handleShowTemplate.bind(null, template)}
>
{MailPoet.I18n.t('preview')}
</a>
&nbsp;
<a
className="button button-primary"
onClick={this.handleSelectTemplate.bind(null, template)}
>
{MailPoet.I18n.t('select')}
</a>
</div>
{ (template.readonly === '1') ? false : deleteLink }
</li>
);
});
const boxClasses = classNames(
'mailpoet_boxes',
'clearfix',
{ mailpoet_boxes_loading: this.state.loading }
); );
}
});
},
handleSelectTemplate: function (template) {
let body = template.body;
return ( // Stringify to enable transmission of primitive non-string value types
<div> if (!_.isUndefined(body)) {
<h1>{MailPoet.I18n.t('selectTemplateTitle')}</h1> body = JSON.stringify(body);
}
<Breadcrumb step="template" /> MailPoet.trackEvent('Emails > Template selected', {
'MailPoet Free version': window.mailpoet_version,
<ul className={boxClasses}> 'Email name': template.name,
{ templates }
</ul>
<ImportTemplate onImport={this.handleTemplateImport} />
</div>
);
},
}); });
return NewsletterTemplates; MailPoet.Ajax.post({
} api_version: window.mailpoet_api_version,
); endpoint: 'newsletters',
action: 'save',
data: {
id: this.props.params.id,
body: body,
},
}).done((response) => {
// TODO: Move this URL elsewhere
window.location = `admin.php?page=mailpoet-newsletter-editor&id=${response.data.id}`;
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(error => error.message),
{ scroll: true }
);
}
});
},
handleDeleteTemplate: function (template) {
this.setState({ loading: true });
const onConfirm = () => {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletterTemplates',
action: 'delete',
data: {
id: template.id,
},
}).done(() => {
this.getTemplates();
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(error => error.message),
{ scroll: true }
);
}
});
};
const onCancel = () => {
this.setState({ loading: false });
};
confirmAlert({
title: MailPoet.I18n.t('confirmTitle'),
message: MailPoet.I18n.t('confirmTemplateDeletion').replace('%$1s', template.name),
confirmLabel: MailPoet.I18n.t('confirmLabel'),
cancelLabel: MailPoet.I18n.t('cancelLabel'),
onConfirm: onConfirm,
onCancel: onCancel,
});
},
handleShowTemplate: function (template) {
MailPoet.Modal.popup({
title: template.name,
template: '<div class="mailpoet_boxes_preview" style="background-color: {{ body.globalStyles.body.backgroundColor }}"><img src="{{ thumbnail }}" /></div>',
data: template,
});
},
handleTemplateImport: function () {
this.getTemplates();
},
render: function () {
const templates = this.state.templates.map((template, index) => {
const deleteLink = (
<div className="mailpoet_delete">
<a
href="javascript:;"
onClick={this.handleDeleteTemplate.bind(null, template)}
>
{MailPoet.I18n.t('delete')}
</a>
</div>
);
let thumbnail = '';
if (typeof template.thumbnail === 'string'
&& template.thumbnail.length > 0) {
thumbnail = (
<a href="javascript:;" onClick={this.handleShowTemplate.bind(null, template)}>
<img src={template.thumbnail} />
<div className="mailpoet_overlay"></div>
</a>
);
}
return (
<li key={`template-${index}`}>
<div className="mailpoet_thumbnail">
{ thumbnail }
</div>
<div className="mailpoet_description">
<h3>{ template.name }</h3>
<p>{ template.description }</p>
</div>
<div className="mailpoet_actions">
<a
className="button button-secondary"
onClick={this.handleShowTemplate.bind(null, template)}
>
{MailPoet.I18n.t('preview')}
</a>
&nbsp;
<a
className="button button-primary"
data-automation-id={`select_template_${index}`}
onClick={this.handleSelectTemplate.bind(null, template)}
>
{MailPoet.I18n.t('select')}
</a>
</div>
{ (template.readonly === '1') ? false : deleteLink }
</li>
);
});
const boxClasses = classNames(
'mailpoet_boxes',
'clearfix',
{ mailpoet_boxes_loading: this.state.loading }
);
return (
<div>
<h1>{MailPoet.I18n.t('selectTemplateTitle')}</h1>
<Breadcrumb step="template" />
<ul className={boxClasses}>
{ templates }
</ul>
<ImportTemplate onImport={this.handleTemplateImport} />
</div>
);
},
});
export default NewsletterTemplates;

View File

@ -44,7 +44,7 @@ define(
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -58,7 +58,7 @@ define(
description: MailPoet.I18n.t('regularNewsletterTypeDescription'), description: MailPoet.I18n.t('regularNewsletterTypeDescription'),
action: function () { action: function () {
return ( return (
<a className="button button-primary" onClick={this.createNewsletter.bind(null, 'standard')}> <a className="button button-primary" data-automation-id="create_standard" onClick={this.createNewsletter.bind(null, 'standard')}>
{MailPoet.I18n.t('create')} {MailPoet.I18n.t('create')}
</a> </a>
); );
@ -68,7 +68,7 @@ define(
id: 'welcome', id: 'welcome',
title: MailPoet.I18n.t('welcomeNewsletterTypeTitle'), title: MailPoet.I18n.t('welcomeNewsletterTypeTitle'),
description: MailPoet.I18n.t('welcomeNewsletterTypeDescription'), description: MailPoet.I18n.t('welcomeNewsletterTypeDescription'),
action: function () { action: (function () {
return ( return (
<div> <div>
<a href="?page=mailpoet-premium" target="_blank"> <a href="?page=mailpoet-premium" target="_blank">
@ -76,7 +76,7 @@ define(
</a> </a>
</div> </div>
); );
}(), }()),
}, },
{ {
id: 'notification', id: 'notification',
@ -84,7 +84,7 @@ define(
description: MailPoet.I18n.t('postNotificationNewsletterTypeDescription'), description: MailPoet.I18n.t('postNotificationNewsletterTypeDescription'),
action: function () { action: function () {
return ( return (
<a className="button button-primary" onClick={this.setupNewsletter.bind(null, 'notification')}> <a className="button button-primary" data-automation-id="create_notification" onClick={this.setupNewsletter.bind(null, 'notification')}>
{MailPoet.I18n.t('setUp')} {MailPoet.I18n.t('setUp')}
</a> </a>
); );
@ -101,24 +101,22 @@ define(
<Breadcrumb step="type" /> <Breadcrumb step="type" />
<ul className="mailpoet_boxes clearfix"> <ul className="mailpoet_boxes clearfix">
{types.map((type, index) => { {types.map((type, index) => (
return ( <li key={index} data-type={type.id}>
<li key={index} data-type={type.id}> <div>
<div> <div className="mailpoet_thumbnail"></div>
<div className="mailpoet_thumbnail"></div>
<div className="mailpoet_description"> <div className="mailpoet_description">
<h3>{type.title}</h3> <h3>{type.title}</h3>
<p>{type.description}</p> <p>{type.description}</p>
</div>
<div className="mailpoet_actions">
{type.action}
</div>
</div> </div>
</li>
); <div className="mailpoet_actions">
}, this)} {type.action}
</div>
</div>
</li>
), this)}
</ul> </ul>
</div> </div>
); );

View File

@ -15,7 +15,6 @@ define(
Scheduling, Scheduling,
Breadcrumb Breadcrumb
) => { ) => {
const field = { const field = {
name: 'options', name: 'options',
type: 'reactComponent', type: 'reactComponent',
@ -56,7 +55,7 @@ define(
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }

View File

@ -35,11 +35,11 @@ const nthWeekDayField = {
}; };
const NotificationScheduling = React.createClass({ const NotificationScheduling = React.createClass({
_getCurrentValue: function () { getCurrentValue: function () {
return (this.props.item[this.props.field.name] || {}); return (this.props.item[this.props.field.name] || {});
}, },
handleValueChange: function (name, value) { handleValueChange: function (name, value) {
const oldValue = this._getCurrentValue(); const oldValue = this.getCurrentValue();
const newValue = {}; const newValue = {};
newValue[name] = value; newValue[name] = value;
@ -82,7 +82,7 @@ const NotificationScheduling = React.createClass({
); );
}, },
render: function () { render: function () {
const value = this._getCurrentValue(); const value = this.getCurrentValue();
let timeOfDaySelection; let timeOfDaySelection;
let weekDaySelection; let weekDaySelection;
let monthDaySelection; let monthDaySelection;
@ -92,7 +92,7 @@ const NotificationScheduling = React.createClass({
timeOfDaySelection = ( timeOfDaySelection = (
<Select <Select
field={timeOfDayField} field={timeOfDayField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleTimeOfDayChange} /> onValueChange={this.handleTimeOfDayChange} />
); );
} }
@ -101,7 +101,7 @@ const NotificationScheduling = React.createClass({
weekDaySelection = ( weekDaySelection = (
<Select <Select
field={weekDayField} field={weekDayField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleWeekDayChange} /> onValueChange={this.handleWeekDayChange} />
); );
} }
@ -110,7 +110,7 @@ const NotificationScheduling = React.createClass({
monthDaySelection = ( monthDaySelection = (
<Select <Select
field={monthDayField} field={monthDayField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleMonthDayChange} /> onValueChange={this.handleMonthDayChange} />
); );
} }
@ -119,7 +119,7 @@ const NotificationScheduling = React.createClass({
nthWeekDaySelection = ( nthWeekDaySelection = (
<Select <Select
field={nthWeekDayField} field={nthWeekDayField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleNthWeekDayChange} /> onValueChange={this.handleNthWeekDayChange} />
); );
} }
@ -128,7 +128,7 @@ const NotificationScheduling = React.createClass({
<div> <div>
<Select <Select
field={intervalField} field={intervalField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleIntervalChange} /> onValueChange={this.handleIntervalChange} />
{nthWeekDaySelection} {nthWeekDaySelection}

View File

@ -11,7 +11,6 @@ define(
MailPoet, MailPoet,
Breadcrumb Breadcrumb
) => { ) => {
const NewsletterStandard = React.createClass({ const NewsletterStandard = React.createClass({
contextTypes: { contextTypes: {
router: React.PropTypes.object.isRequired, router: React.PropTypes.object.isRequired,
@ -33,7 +32,7 @@ define(
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }

View File

@ -8,9 +8,7 @@ import { timeDelayValues } from 'newsletters/scheduling/common.jsx';
const availableRoles = window.mailpoet_roles || {}; const availableRoles = window.mailpoet_roles || {};
const availableSegments = _.filter( const availableSegments = _.filter(
window.mailpoet_segments || [], window.mailpoet_segments || [],
(segment) => { segment => segment.type === 'default'
return segment.type === 'default';
}
); );
const events = { const events = {
@ -24,7 +22,7 @@ const events = {
const availableSegmentValues = _.object(_.map( const availableSegmentValues = _.object(_.map(
availableSegments, availableSegments,
(segment) => { (segment) => {
const name = segment.name + ' (' + parseInt(segment.subscribers, 10).toLocaleString() + ')'; const name = `${segment.name} (${parseInt(segment.subscribers, 10).toLocaleString()})`;
return [segment.id, name]; return [segment.id, name];
} }
)); ));
@ -53,11 +51,11 @@ const WelcomeScheduling = React.createClass({
contextTypes: { contextTypes: {
router: React.PropTypes.object.isRequired, router: React.PropTypes.object.isRequired,
}, },
_getCurrentValue: function () { getCurrentValue: function () {
return (this.props.item[this.props.field.name] || {}); return (this.props.item[this.props.field.name] || {});
}, },
handleValueChange: function (name, value) { handleValueChange: function (name, value) {
const oldValue = this._getCurrentValue(); const oldValue = this.getCurrentValue();
const newValue = {}; const newValue = {};
newValue[name] = value; newValue[name] = value;
@ -113,7 +111,7 @@ const WelcomeScheduling = React.createClass({
}).fail((response) => { }).fail((response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -123,7 +121,7 @@ const WelcomeScheduling = React.createClass({
this.context.router.push(`/template/${newsletterId}`); this.context.router.push(`/template/${newsletterId}`);
}, },
render: function () { render: function () {
const value = this._getCurrentValue(); const value = this.getCurrentValue();
let roleSegmentSelection; let roleSegmentSelection;
let timeNumber; let timeNumber;
@ -131,14 +129,14 @@ const WelcomeScheduling = React.createClass({
roleSegmentSelection = ( roleSegmentSelection = (
<Select <Select
field={roleField} field={roleField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleRoleChange} /> onValueChange={this.handleRoleChange} />
); );
} else { } else {
roleSegmentSelection = ( roleSegmentSelection = (
<Select <Select
field={segmentField} field={segmentField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleSegmentChange} /> onValueChange={this.handleSegmentChange} />
); );
} }
@ -146,7 +144,7 @@ const WelcomeScheduling = React.createClass({
timeNumber = ( timeNumber = (
<Text <Text
field={afterTimeNumberField} field={afterTimeNumberField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleAfterTimeNumberChange} /> onValueChange={this.handleAfterTimeNumberChange} />
); );
} }
@ -155,7 +153,7 @@ const WelcomeScheduling = React.createClass({
<div> <div>
<Select <Select
field={events} field={events}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleEventChange} /> onValueChange={this.handleEventChange} />
{ roleSegmentSelection } { roleSegmentSelection }
@ -164,7 +162,7 @@ const WelcomeScheduling = React.createClass({
<Select <Select
field={afterTimeTypeField} field={afterTimeTypeField}
item={this._getCurrentValue()} item={this.getCurrentValue()}
onValueChange={this.handleAfterTimeTypeChange} /> onValueChange={this.handleAfterTimeTypeChange} />
</div> </div>
); );

View File

@ -115,9 +115,8 @@ define('notice', ['mailpoet', 'jquery'], function (mp, jQuery) {
formatMessage: function (message) { formatMessage: function (message) {
if (Array.isArray(message)) { if (Array.isArray(message)) {
return message.join('<br />'); return message.join('<br />');
} else {
return message;
} }
return message;
}, },
show: function (options) { show: function (options) {
// initialize // initialize

View File

@ -18,5 +18,4 @@ define('num',
); );
} }
}; };
}); });

View File

@ -30,54 +30,54 @@ function (
}); });
form.parsley().on('form:submit', function (parsley) { form.parsley().on('form:submit', function (parsley) {
var form_data = form.mailpoetSerializeObject() || {}; var formData = form.mailpoetSerializeObject() || {};
// check if we're on the same domain // check if we're on the same domain
if (isSameDomain(window.MailPoetForm.ajax_url) === false) { if (isSameDomain(window.MailPoetForm.ajax_url) === false) {
// non ajax post request // non ajax post request
return true; return true;
} else { }
// ajax request // ajax request
MailPoet.Ajax.post({ MailPoet.Ajax.post({
url: window.MailPoetForm.ajax_url, url: window.MailPoetForm.ajax_url,
token: form_data.token, token: formData.token,
api_version: form_data.api_version, api_version: formData.api_version,
endpoint: 'subscribers', endpoint: 'subscribers',
action: 'subscribe', action: 'subscribe',
data: form_data.data data: formData.data
}).fail(function (response) { }).fail(function (response) {
form.find('.mailpoet_validate_error').html( form.find('.mailpoet_validate_error').html(
response.errors.map(function (error) { response.errors.map(function (error) {
return error.message; return error.message;
}).join('<br />') }).join('<br />')
).show(); ).show();
}).done(function (response) { }).done(function (response) {
// successfully subscribed // successfully subscribed
if ( if (
response.meta !== undefined response.meta !== undefined
&& response.meta.redirect_url !== undefined && response.meta.redirect_url !== undefined
) { ) {
// go to page // go to page
window.location.href = response.meta.redirect_url; window.location.href = response.meta.redirect_url;
} else { } else {
// display success message // display success message
form.find('.mailpoet_validate_success').show(); form.find('.mailpoet_validate_success').show();
} }
// reset form // reset form
form.trigger('reset'); form.trigger('reset');
// reset validation // reset validation
parsley.reset(); parsley.reset();
// resize iframe // resize iframe
if ( if (
window.frameElement !== null window.frameElement !== null
&& MailPoet !== undefined && MailPoet !== undefined
&& MailPoet['Iframe'] && MailPoet['Iframe']
) { ) {
MailPoet.Iframe.autoSize(window.frameElement); MailPoet.Iframe.autoSize(window.frameElement);
} }
}); });
}
return false; return false;
}); });
}); });

View File

@ -11,7 +11,6 @@ define(
MailPoet, MailPoet,
Form Form
) => { ) => {
const fields = [ const fields = [
{ {
name: 'name', name: 'name',

View File

@ -40,7 +40,7 @@ const columns = [
const messages = { const messages = {
onTrash: (response) => { onTrash: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -55,7 +55,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onDelete: (response) => { onDelete: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -70,7 +70,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onRestore: (response) => { onRestore: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -86,7 +86,7 @@ const messages = {
}, },
}; };
const bulk_actions = [ const bulkActions = [
{ {
name: 'trash', name: 'trash',
label: MailPoet.I18n.t('moveToTrash'), label: MailPoet.I18n.t('moveToTrash'),
@ -94,7 +94,7 @@ const bulk_actions = [
}, },
]; ];
const item_actions = [ const itemActions = [
{ {
name: 'edit', name: 'edit',
link: function (item) { link: function (item) {
@ -109,26 +109,24 @@ const item_actions = [
{ {
name: 'duplicate_segment', name: 'duplicate_segment',
label: MailPoet.I18n.t('duplicate'), label: MailPoet.I18n.t('duplicate'),
onClick: (item, refresh) => { onClick: (item, refresh) => MailPoet.Ajax.post({
return MailPoet.Ajax.post({ api_version: window.mailpoet_api_version,
api_version: window.mailpoet_api_version, endpoint: 'segments',
endpoint: 'segments', action: 'duplicate',
action: 'duplicate', data: {
data: { id: item.id,
id: item.id, },
}, }).done((response) => {
}).done((response) => { MailPoet.Notice.success(
MailPoet.Notice.success(
MailPoet.I18n.t('listDuplicated').replace('%$1s', response.data.name) MailPoet.I18n.t('listDuplicated').replace('%$1s', response.data.name)
); );
refresh(); refresh();
}).fail((response) => { }).fail((response) => {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
}); }),
},
display: function (segment) { display: function (segment) {
return (segment.type !== 'wp_users'); return (segment.type !== 'wp_users');
}, },
@ -166,7 +164,7 @@ const item_actions = [
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }), response.errors.map(error => error.message),
{ scroll: true } { scroll: true }
); );
} }
@ -200,20 +198,20 @@ const SegmentList = React.createClass({
'has-row-actions' 'has-row-actions'
); );
const subscribed = ~~(segment.subscribers_count.subscribed || 0); const subscribed = Number(segment.subscribers_count.subscribed || 0);
const unconfirmed = ~~(segment.subscribers_count.unconfirmed || 0); const unconfirmed = Number(segment.subscribers_count.unconfirmed || 0);
const unsubscribed = ~~(segment.subscribers_count.unsubscribed || 0); const unsubscribed = Number(segment.subscribers_count.unsubscribed || 0);
const bounced = ~~(segment.subscribers_count.bounced || 0); const bounced = Number(segment.subscribers_count.bounced || 0);
let segment_name; let segmentName;
if (segment.type === 'wp_users') { if (segment.type === 'wp_users') {
// the WP users segment is not editable so just display its name // the WP users segment is not editable so just display its name
segment_name = ( segmentName = (
<span className="row-title">{ segment.name }</span> <span className="row-title">{ segment.name }</span>
); );
} else { } else {
segment_name = ( segmentName = (
<Link <Link
className="row-title" className="row-title"
to={`/edit/${segment.id}`} to={`/edit/${segment.id}`}
@ -225,7 +223,7 @@ const SegmentList = React.createClass({
<div> <div>
<td className={rowClasses}> <td className={rowClasses}>
<strong> <strong>
{ segment_name } { segmentName }
</strong> </strong>
{ actions } { actions }
</td> </td>
@ -266,8 +264,8 @@ const SegmentList = React.createClass({
endpoint="segments" endpoint="segments"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
item_actions={item_actions} item_actions={itemActions}
sort_by="name" sort_by="name"
sort_order="asc" sort_order="asc"
/> />

View File

@ -19,7 +19,7 @@ define(
label: MailPoet.I18n.t('email'), label: MailPoet.I18n.t('email'),
type: 'text', type: 'text',
disabled: function (subscriber) { disabled: function (subscriber) {
return ~~(subscriber.wp_user_id > 0); return Number(subscriber.wp_user_id > 0);
}, },
}, },
{ {
@ -27,7 +27,7 @@ define(
label: MailPoet.I18n.t('firstname'), label: MailPoet.I18n.t('firstname'),
type: 'text', type: 'text',
disabled: function (subscriber) { disabled: function (subscriber) {
return ~~(subscriber.wp_user_id > 0); return Number(subscriber.wp_user_id > 0);
}, },
}, },
{ {
@ -35,7 +35,7 @@ define(
label: MailPoet.I18n.t('lastname'), label: MailPoet.I18n.t('lastname'),
type: 'text', type: 'text',
disabled: function (subscriber) { disabled: function (subscriber) {
return ~~(subscriber.wp_user_id > 0); return Number(subscriber.wp_user_id > 0);
}, },
}, },
{ {
@ -49,7 +49,7 @@ define(
bounced: MailPoet.I18n.t('bounced'), bounced: MailPoet.I18n.t('bounced'),
}, },
filter: function (subscriber, value) { filter: function (subscriber, value) {
if (~~(subscriber.wp_user_id) > 0 && value === 'unconfirmed') { if (Number(subscriber.wp_user_id) > 0 && value === 'unconfirmed') {
return false; return false;
} }
return true; return true;
@ -68,34 +68,32 @@ define(
return null; return null;
} }
return subscriber.subscriptions.map((subscription) => { return subscriber.subscriptions
if (subscription.status === 'subscribed') { .filter(subscription => subscription.status === 'subscribed')
return subscription.segment_id; .map(subscription => subscription.segment_id);
}
});
}, },
filter: function (segment) { filter: function (segment) {
return !!(!segment.deleted_at && segment.type === 'default'); return (!segment.deleted_at && segment.type === 'default');
}, },
getLabel: function (segment) { getLabel: function (segment) {
return segment.name + ' (' + segment.subscribers + ')'; return `${segment.name} (${segment.subscribers})`;
}, },
getSearchLabel: function (segment, subscriber) { getSearchLabel: function (segment, subscriber) {
let label = ''; let label = '';
if (subscriber.subscriptions !== undefined) { if (subscriber.subscriptions !== undefined) {
subscriber.subscriptions.map((subscription) => { subscriber.subscriptions.forEach((subscription) => {
if (segment.id === subscription.segment_id) { if (segment.id === subscription.segment_id) {
label = segment.name; label = segment.name;
if (subscription.status === 'unsubscribed') { if (subscription.status === 'unsubscribed') {
const unsubscribed_at = MailPoet.Date const unsubscribedAt = MailPoet.Date
.format(subscription.updated_at); .format(subscription.updated_at);
label += ' (%$1s)'.replace( label += ' (%$1s)'.replace(
'%$1s', '%$1s',
MailPoet.I18n.t('unsubscribedOn').replace( MailPoet.I18n.t('unsubscribedOn').replace(
'%$1s', '%$1s',
unsubscribed_at unsubscribedAt
) )
); );
} }
@ -107,23 +105,23 @@ define(
}, },
]; ];
const custom_fields = window.mailpoet_custom_fields || []; const customFields = window.mailpoet_custom_fields || [];
custom_fields.map((custom_field) => { customFields.forEach((customField) => {
const field = { const field = {
name: 'cf_' + custom_field.id, name: `cf_${customField.id}`,
label: custom_field.name, label: customField.name,
type: custom_field.type, type: customField.type,
}; };
if (custom_field.params) { if (customField.params) {
field.params = custom_field.params; field.params = customField.params;
} }
if (custom_field.params.values) { if (customField.params.values) {
field.values = custom_field.params.values; field.values = customField.params.values;
} }
// add placeholders for selects (date, select) // add placeholders for selects (date, select)
switch (custom_field.type) { switch (customField.type) {
case 'date': case 'date':
field.year_placeholder = MailPoet.I18n.t('year'); field.year_placeholder = MailPoet.I18n.t('year');
field.month_placeholder = MailPoet.I18n.t('month'); field.month_placeholder = MailPoet.I18n.t('month');
@ -133,6 +131,10 @@ define(
case 'select': case 'select':
field.placeholder = '-'; field.placeholder = '-';
break; break;
default:
field.placeholder = '';
break;
} }
fields.push(field); fields.push(field);
@ -151,7 +153,7 @@ define(
}; };
const beforeFormContent = function (subscriber) { const beforeFormContent = function (subscriber) {
if (~~(subscriber.wp_user_id) > 0) { if (Number(subscriber.wp_user_id) > 0) {
return ( return (
<p className="description"> <p className="description">
{ ReactStringReplace( { ReactStringReplace(
@ -168,6 +170,7 @@ define(
</p> </p>
); );
} }
return undefined;
}; };
const afterFormContent = function () { const afterFormContent = function () {

View File

@ -21,15 +21,25 @@ define(
var groupBySegmentOptionElement; var groupBySegmentOptionElement;
var nextStepButton; var nextStepButton;
var renderSegmentsAndFields; var renderSegmentsAndFields;
var subscribers_export_template; var subscribersExportTemplate;
if (!window.exportData.segments) { if (!window.exportData.segments) {
return; return;
} }
subscribers_export_template = subscribersExportTemplate =
Handlebars.compile(jQuery('#mailpoet_subscribers_export_template').html()); Handlebars.compile(jQuery('#mailpoet_subscribers_export_template').html());
// render template // render template
jQuery('#mailpoet_subscribers_export > div.inside').html(subscribers_export_template(window.exportData)); jQuery('#mailpoet_subscribers_export > div.inside').html(subscribersExportTemplate(window.exportData));
function toggleNextStepButton(condition) {
var disabled = 'button-disabled';
if (condition === 'on') {
nextStepButton.removeClass(disabled);
}
else {
nextStepButton.addClass(disabled);
}
}
// define reusable variables // define reusable variables
segmentsContainerElement = jQuery('#export_lists'); segmentsContainerElement = jQuery('#export_lists');
@ -128,16 +138,6 @@ define(
segmentsContainerElement.val(selectedSegments).trigger('change'); segmentsContainerElement.val(selectedSegments).trigger('change');
}); });
function toggleNextStepButton(condition) {
var disabled = 'button-disabled';
if (condition === 'on') {
nextStepButton.removeClass(disabled);
}
else {
nextStepButton.addClass(disabled);
}
}
nextStepButton.click(function () { nextStepButton.click(function () {
var exportFormat; var exportFormat;
if (jQuery(this).hasClass('button-disabled')) { if (jQuery(this).hasClass('button-disabled')) {

View File

@ -99,8 +99,8 @@ define(
// define method change behavior // define method change behavior
methodSelectionElement.change(function () { methodSelectionElement.change(function () {
var available_methods = jQuery(':radio[name="select_method"]'); var availableMethods = jQuery(':radio[name="select_method"]');
var selected_method = available_methods.index(available_methods.filter(':checked')); var selectedMethod = availableMethods.index(availableMethods.filter(':checked'));
MailPoet.Notice.hide(); MailPoet.Notice.hide();
// hide all methods // hide all methods
currentStepE.find('.inside') currentStepE.find('.inside')
@ -108,7 +108,7 @@ define(
.hide(); .hide();
// show selected method // show selected method
currentStepE.find('.inside') currentStepE.find('.inside')
.children('div[id^="method_"]:eq(' + selected_method + ')') .children('div[id^="method_"]:eq(' + selectedMethod + ')')
.show() .show()
.find('table') .find('table')
.show(); .show();
@ -117,6 +117,196 @@ define(
// start step 1 // start step 1
showCurrentStep(); showCurrentStep();
function toggleNextStepButton(element, condition) {
var disabled = 'button-disabled';
if (condition === 'on') {
element.closest('table a').removeClass(disabled);
return;
}
element.closest('table a').addClass(disabled);
}
function parseCSV(isFile) {
var processedSubscribers = [];
var parsedEmails = [];
var duplicateEmails = [];
var invalidEmails = [];
var emailColumnPosition = null;
var columnCount = null;
var isHeaderFound = false;
var advancedOptionHeader = true;
var advancedOptionDelimiter = '';
var advancedOptionNewline = '';
var advancedOptionComments = false;
// trim spaces, commas, periods,
// single/double quotes and convert to lowercase
var detectAndCleanupEmail = function (emailString) {
var test;
// decode HTML entities
var email = jQuery('<div />').html(emailString).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, '');
// detect e-mails that will be otherwise rejected by email regex
test = /<(.*?)>/.exec(email);
if (test) {
// is the email inside angle brackets (e.g., 'some@email.com <some@email.com>')?
email = test[1].trim();
}
test = /mailto:(?:\s+)?(.*)/.exec(email);
if (test) {
// is the email in 'mailto:email' format?
email = test[1].trim();
}
// test for valid characters using WP's rule (https://core.trac.wordpress.org/browser/tags/4.7.3/src/wp-includes/formatting.php#L2902)
if (!/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.\-@]+$/.test(email)) {
return false;
}
return email;
};
return {
skipEmptyLines: true,
delimiter: advancedOptionDelimiter,
newline: advancedOptionNewline,
comments: advancedOptionComments,
error: function () {
MailPoet.Notice.hide();
MailPoet.Notice.error(MailPoet.I18n.t('dataProcessingError'));
},
complete: function (CSV) {
var email;
var emailAddress;
var column;
var rowCount;
var rowData;
var rowColumnCount;
var errorNotice;
for (rowCount in CSV.data) {
rowData = CSV.data[rowCount].map(function (el) {
return el.trim();
});
rowColumnCount = rowData.length;
// set the number of row elements based on the first non-empty row
if (columnCount === null) {
columnCount = rowColumnCount;
}
// Process the row with the following assumptions:
// 1. Each row should contain the same number of elements
// 2. There should be at least 1 valid (as per HTML5 e-mail regex)
// e-mail address on each row EXCEPT when the header option is set to true
// 3. Duplicate addresses are skipped
if (rowColumnCount === columnCount) {
// determine position of email address inside an array; this is
// done once and then email regex is run just on that element for each row
if (emailColumnPosition === null) {
for (column in rowData) {
emailAddress = detectAndCleanupEmail(rowData[column]);
if (emailColumnPosition === null
&& window.emailRegex.test(emailAddress)) {
emailColumnPosition = column;
parsedEmails[emailAddress] = true; // add current e-mail to an object index
rowData[column] = emailAddress;
processedSubscribers[emailAddress] = rowData;
}
}
if (emailColumnPosition === null
&& advancedOptionHeader
&& parseInt(rowCount) === 0) {
isHeaderFound = true;
processedSubscribers[0] = rowData;
}
}
else if (rowData[emailColumnPosition] !== '') {
email = detectAndCleanupEmail(rowData[emailColumnPosition]);
if (_.has(parsedEmails, email)) {
duplicateEmails.push(email);
}
else if (!window.emailRegex.test(email)) {
invalidEmails.push(rowData[emailColumnPosition]);
}
// if we haven't yet processed this e-mail and it passed
// the regex test, then process the row
else {
parsedEmails[email] = true;
rowData[emailColumnPosition] = email;
processedSubscribers[email] = rowData;
}
}
}
}
// reindex array to avoid non-numeric indices
processedSubscribers = _.values(processedSubscribers);
// if the header options is set, there should be at least
// 2 data rows, otherwise at least 1 data row
if (processedSubscribers &&
(isHeaderFound && processedSubscribers.length >= 2) ||
(!isHeaderFound && processedSubscribers.length >= 1)
) {
// since we assume that the header line is always present, we need
// to detect the header by checking if it contains a valid e-mail address
window.importData.step1 = {
header: (!window.emailRegex.test(
processedSubscribers[0][emailColumnPosition])
) ? processedSubscribers.shift() : null,
subscribers: processedSubscribers,
subscribersCount: processedSubscribers.length,
duplicate: duplicateEmails,
invalid: invalidEmails
};
MailPoet.trackEvent('Subscribers import started', {
source: isFile ? 'file upload' : 'pasted data',
'MailPoet Free version': window.mailpoet_version
});
router.navigate('step2', { trigger: true });
}
else {
MailPoet.Modal.loading(false);
errorNotice = MailPoet.I18n.t('noValidRecords');
errorNotice = errorNotice.replace('[link]', MailPoet.I18n.t('csvKBLink'));
errorNotice = errorNotice.replace('[/link]', '</a>');
MailPoet.Notice.error(errorNotice);
}
}
};
}
function displayMailChimpLists(data) {
var listSelectElement = mailChimpListsContainerElement.find('select');
if (listSelectElement.data('select2')) {
listSelectElement.select2('data', data);
listSelectElement.trigger('change');
}
else {
listSelectElement
.select2({
data: data,
width: '20em',
templateResult: function (item) {
return item.name;
},
templateSelection: function (item) {
return item.name;
}
})
.change(function () {
if (jQuery(this).val() !== null) {
toggleNextStepButton(mailChimpProcessButtonElement, 'on');
}
else {
toggleNextStepButton(mailChimpProcessButtonElement, 'off');
}
})
.trigger('change');
}
mailChimpListsContainerElement.show();
}
/* /*
* Paste * Paste
*/ */
@ -261,196 +451,6 @@ define(
} }
}); });
}); });
function displayMailChimpLists(data) {
var listSelectElement = mailChimpListsContainerElement.find('select');
if (listSelectElement.data('select2')) {
listSelectElement.select2('data', data);
listSelectElement.trigger('change');
}
else {
listSelectElement
.select2({
data: data,
width: '20em',
templateResult: function (item) {
return item.name;
},
templateSelection: function (item) {
return item.name;
}
})
.change(function () {
if (jQuery(this).val() !== null) {
toggleNextStepButton(mailChimpProcessButtonElement, 'on');
}
else {
toggleNextStepButton(mailChimpProcessButtonElement, 'off');
}
})
.trigger('change');
}
mailChimpListsContainerElement.show();
}
function toggleNextStepButton(element, condition) {
var disabled = 'button-disabled';
if (condition === 'on') {
element.closest('table a').removeClass(disabled);
return;
}
element.closest('table a').addClass(disabled);
}
function parseCSV(isFile) {
var processedSubscribers = [];
var parsedEmails = [];
var duplicateEmails = [];
var invalidEmails = [];
var emailColumnPosition = null;
var columnCount = null;
var isHeaderFound = false;
var advancedOptionHeader = true;
var advancedOptionDelimiter = '';
var advancedOptionNewline = '';
var advancedOptionComments = false;
// trim spaces, commas, periods,
// single/double quotes and convert to lowercase
var detectAndCleanupEmail = function (emailString) {
var test;
// decode HTML entities
var email = jQuery('<div />').html(emailString).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, '');
// detect e-mails that will be otherwise rejected by email regex
test = /<(.*?)>/.exec(email);
if (test) {
// is the email inside angle brackets (e.g., 'some@email.com <some@email.com>')?
email = test[1].trim();
}
test = /mailto:(?:\s+)?(.*)/.exec(email);
if (test) {
// is the email in 'mailto:email' format?
email = test[1].trim();
}
// test for valid characters using WP's rule (https://core.trac.wordpress.org/browser/tags/4.7.3/src/wp-includes/formatting.php#L2902)
if (!/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.\-@]+$/.test(email)) {
return false;
}
return email;
};
return {
skipEmptyLines: true,
delimiter: advancedOptionDelimiter,
newline: advancedOptionNewline,
comments: advancedOptionComments,
error: function () {
MailPoet.Notice.hide();
MailPoet.Notice.error(MailPoet.I18n.t('dataProcessingError'));
},
complete: function (CSV) {
var email;
var emailAddress;
var column;
var rowCount;
var rowData;
var rowColumnCount;
var errorNotice;
for (rowCount in CSV.data) {
rowData = CSV.data[rowCount].map(function (el) {
return el.trim();
});
rowColumnCount = rowData.length;
// set the number of row elements based on the first non-empty row
if (columnCount === null) {
columnCount = rowColumnCount;
}
// Process the row with the following assumptions:
// 1. Each row should contain the same number of elements
// 2. There should be at least 1 valid (as per HTML5 e-mail regex)
// e-mail address on each row EXCEPT when the header option is set to true
// 3. Duplicate addresses are skipped
if (rowColumnCount === columnCount) {
// determine position of email address inside an array; this is
// done once and then email regex is run just on that element for each row
if (emailColumnPosition === null) {
for (column in rowData) {
emailAddress = detectAndCleanupEmail(rowData[column]);
if (emailColumnPosition === null
&& window.emailRegex.test(emailAddress)) {
emailColumnPosition = column;
parsedEmails[emailAddress] = true; // add current e-mail to an object index
rowData[column] = emailAddress;
processedSubscribers[emailAddress] = rowData;
}
}
if (emailColumnPosition === null
&& advancedOptionHeader
&& parseInt(rowCount) === 0) {
isHeaderFound = true;
processedSubscribers[0] = rowData;
}
}
else if (rowData[emailColumnPosition] !== '') {
email = detectAndCleanupEmail(rowData[emailColumnPosition]);
if (_.has(parsedEmails, email)) {
duplicateEmails.push(email);
}
else if (!window.emailRegex.test(email)) {
invalidEmails.push(rowData[emailColumnPosition]);
}
// if we haven't yet processed this e-mail and it passed
// the regex test, then process the row
else {
parsedEmails[email] = true;
rowData[emailColumnPosition] = email;
processedSubscribers[email] = rowData;
}
}
}
}
// reindex array to avoid non-numeric indices
processedSubscribers = _.values(processedSubscribers);
// if the header options is set, there should be at least
// 2 data rows, otherwise at least 1 data row
if (processedSubscribers &&
(isHeaderFound && processedSubscribers.length >= 2) ||
(!isHeaderFound && processedSubscribers.length >= 1)
) {
// since we assume that the header line is always present, we need
// to detect the header by checking if it contains a valid e-mail address
window.importData.step1 = {
header: (!window.emailRegex.test(
processedSubscribers[0][emailColumnPosition])
) ? processedSubscribers.shift() : null,
subscribers: processedSubscribers,
subscribersCount: processedSubscribers.length,
duplicate: duplicateEmails,
invalid: invalidEmails
};
MailPoet.trackEvent('Subscribers import started', {
source: isFile ? 'file upload' : 'pasted data',
'MailPoet Free version': window.mailpoet_version
});
router.navigate('step2', { trigger: true });
}
else {
MailPoet.Modal.loading(false);
errorNotice = MailPoet.I18n.t('noValidRecords');
errorNotice = errorNotice.replace('[link]', MailPoet.I18n.t('csvKBLink'));
errorNotice = errorNotice.replace('[/link]', '</a>');
MailPoet.Notice.error(errorNotice);
}
}
};
}
}); });
router.on('route:step2', function () { router.on('route:step2', function () {
@ -465,7 +465,7 @@ define(
var filler; var filler;
var fillerArray; var fillerArray;
var fillerPosition; var fillerPosition;
var import_results; var importResults;
var duplicates; var duplicates;
var email; var email;
if (typeof (window.importData.step1) === 'undefined') { if (typeof (window.importData.step1) === 'undefined') {
@ -492,6 +492,15 @@ define(
showCurrentStep(); showCurrentStep();
function toggleNextStepButton(condition) {
var disabled = 'button-disabled';
if (condition === 'on') {
nextStepButton.removeClass(disabled);
return;
}
nextStepButton.addClass(disabled);
}
// hide previous statistics/import results // hide previous statistics/import results
jQuery('#subscribers_data_parse_results:visible').html(''); jQuery('#subscribers_data_parse_results:visible').html('');
jQuery('#subscribers_data_import_results:visible').hide(); jQuery('#subscribers_data_import_results:visible').hide();
@ -501,8 +510,8 @@ define(
// count repeating e-mails inside duplicate array and present them in // count repeating e-mails inside duplicate array and present them in
// 'email (xN)' format // 'email (xN)' format
duplicates = {}; duplicates = {};
subscribers.duplicate.forEach(function (email) { subscribers.duplicate.forEach(function (subscriberEmail) {
duplicates[email] = (duplicates[email] || 0) + 1; duplicates[subscriberEmail] = (duplicates[subscriberEmail] || 0) + 1;
}); });
subscribers.duplicate = []; subscribers.duplicate = [];
for (email in duplicates) { for (email in duplicates) {
@ -514,7 +523,7 @@ define(
} }
} }
import_results = { importResults = {
notice: MailPoet.I18n.t('importNoticeSkipped').replace( notice: MailPoet.I18n.t('importNoticeSkipped').replace(
'%1$s', '%1$s',
'<strong>' + (subscribers.invalid.length + subscribers.duplicate.length) + '</strong>' '<strong>' + (subscribers.invalid.length + subscribers.duplicate.length) + '</strong>'
@ -531,7 +540,7 @@ define(
: null : null
}; };
jQuery('#subscribers_data_parse_results').html( jQuery('#subscribers_data_parse_results').html(
subscribersDataParseResultsTemplate(import_results) subscribersDataParseResultsTemplate(importResults)
); );
} }
@ -619,22 +628,22 @@ define(
description: segmentDescription description: segmentDescription
} }
}).done(function (response) { }).done(function (response) {
var selected_values; var selectedValues;
window.mailpoetSegments.push({ window.mailpoetSegments.push({
id: response.data.id, id: response.data.id,
name: response.data.name, name: response.data.name,
subscriberCount: 0 subscriberCount: 0
}); });
selected_values = segmentSelectElement.val(); selectedValues = segmentSelectElement.val();
if (selected_values === null) { if (selectedValues === null) {
selected_values = [response.data.id]; selectedValues = [response.data.id];
} else { } else {
selected_values.push(response.data.id); selectedValues.push(response.data.id);
} }
enableSegmentSelection(window.mailpoetSegments); enableSegmentSelection(window.mailpoetSegments);
segmentSelectElement.val(selected_values).trigger('change'); segmentSelectElement.val(selectedValues).trigger('change');
jQuery('.mailpoet_segments:hidden').show(); jQuery('.mailpoet_segments:hidden').show();
jQuery('.mailpoet_no_segments:visible').hide(); jQuery('.mailpoet_no_segments:visible').hide();
MailPoet.Modal.close(); MailPoet.Modal.close();
@ -662,7 +671,7 @@ define(
// autodetect column types // autodetect column types
Handlebars.registerHelper( Handlebars.registerHelper(
'show_and_match_columns', 'show_and_match_columns',
function (subscribers, options) { function (helperSubscribers, options) {
var displayedColumns = []; var displayedColumns = [];
var displayedColumnsIds = []; var displayedColumnsIds = [];
var i; var i;
@ -671,14 +680,14 @@ define(
var headerName; var headerName;
var headerNameMatch; var headerNameMatch;
// go through all elements of the first row in subscribers data // go through all elements of the first row in subscribers data
for (i in subscribers.subscribers[0]) { for (i in helperSubscribers.subscribers[0]) {
columnData = subscribers.subscribers[0][i]; columnData = helperSubscribers.subscribers[0][i];
columnId = 'ignore'; // set default column type columnId = 'ignore'; // set default column type
// if the column is not undefined and has a valid e-mail, set type as email // if the column is not undefined and has a valid e-mail, set type as email
if (columnData % 1 !== 0 && window.emailRegex.test(columnData)) { if (columnData % 1 !== 0 && window.emailRegex.test(columnData)) {
columnId = 'email'; columnId = 'email';
} else if (subscribers.header) { } else if (helperSubscribers.header) {
headerName = subscribers.header[i]; headerName = helperSubscribers.header[i];
headerNameMatch = window.mailpoetColumns.map(function (el) { headerNameMatch = window.mailpoetColumns.map(function (el) {
return el.name; return el.name;
}).indexOf(headerName); }).indexOf(headerName);
@ -729,9 +738,8 @@ define(
// if we're on the last line, show the total count of subscribers data // if we're on the last line, show the total count of subscribers data
else if (index === (subscribers.subscribers.length - 1)) { else if (index === (subscribers.subscribers.length - 1)) {
return subscribers.subscribersCount.toLocaleString(); return subscribers.subscribersCount.toLocaleString();
} else {
return index + 1;
} }
return index + 1;
}); });
// reduce subscribers object if the total length is greater than the // reduce subscribers object if the total length is greater than the
@ -743,127 +751,14 @@ define(
); );
} }
// render template
jQuery('#subscribers_data > table').html(subscribersDataTemplate(subscribers));
// filter displayed data
jQuery('select.mailpoet_subscribers_column_data_match')
.select2({
data: window.mailpoetColumnsSelect2,
width: '15em',
templateResult: function (item) {
return item.name;
},
templateSelection: function (item) {
return item.name;
}
})
.on('select2:selecting', function (selectEvent) {
var selectElement = this;
var selectedOptionId = selectEvent.params.args.data.id;
// CREATE CUSTOM FIELD
if (selectedOptionId === 'create') {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
MailPoet.Modal.popup({
title: MailPoet.I18n.t('addNewField'),
template: jQuery('#form_template_field_form').html()
});
jQuery('#form_field_new').parsley().on('form:submit', function () {
// get data
var data = jQuery(this.$element).mailpoetSerializeObject();
// save custom field
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'customFields',
action: 'save',
data: data
}).done(function (response) {
var new_column_data = {
id: response.data.id,
name: response.data.name,
type: response.data.type,
params: response.data.params,
custom: true
};
// if this is the first custom column, create an "optgroup"
if (window.mailpoetColumnsSelect2.length === 2) {
window.mailpoetColumnsSelect2.push({
name: MailPoet.I18n.t('userColumns'),
children: []
});
}
window.mailpoetColumnsSelect2[2].children.push(new_column_data);
window.mailpoetColumns.push(new_column_data);
jQuery('select.mailpoet_subscribers_column_data_match')
.each(function () {
jQuery(this)
.html('')
.select2('destroy')
.select2({
data: window.mailpoetColumnsSelect2,
width: '15em',
templateResult: function (item) {
return item.name;
},
templateSelection: function (item) {
return item.name;
}
});
});
jQuery(selectElement).data('column-id', new_column_data.id);
jQuery(selectElement).data('validation-rule', false);
filterSubscribers();
// close popup
MailPoet.Modal.close();
}).fail(function (response) {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function (error) { return error.message; }),
{ positionAfter: '#field_name' }
);
}
});
return false;
});
}
// CHANGE COLUMN
else {
// check for duplicate values in all select options
jQuery('select.mailpoet_subscribers_column_data_match')
.each(function () {
var element = this;
var elementId = jQuery(element).val();
// if another column has the same value and it's not an 'ignore', prompt user
if (elementId === selectedOptionId
&& elementId !== 'ignore') {
if (confirm(MailPoet.I18n.t('selectedValueAlreadyMatched') + ' ' + MailPoet.I18n.t('confirmCorrespondingColumn'))) {
jQuery(element).data('column-id', 'ignore');
}
else {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
}
}
});
}
})
.on('select2:select', function (selectEvent) {
var selectElement = this;
var selectedOptionId = selectEvent.params.data.id;
jQuery(selectElement).data('column-id', selectedOptionId);
filterSubscribers();
});
// filter subscribers' data to detect dates, emails, etc. // filter subscribers' data to detect dates, emails, etc.
function filterSubscribers() { function filterSubscribers() {
var subscribersClone = jQuery.extend(true, {}, subscribers); var subscribersClone = jQuery.extend(true, {}, subscribers);
var preventNextStep = false; var preventNextStep = false;
var displayedColumns; var displayedColumns;
jQuery( jQuery(
'[data-id="notice_invalidEmail"], [data-id="notice_invalidDate"]') '[data-id="notice_invalidEmail"], [data-id="notice_invalidDate"]')
.remove(); .remove();
displayedColumns = jQuery.map( displayedColumns = jQuery.map(
jQuery('.mailpoet_subscribers_column_data_match'), function (element, elementIndex) { jQuery('.mailpoet_subscribers_column_data_match'), function (element, elementIndex) {
var columnId = jQuery(element).data('column-id'); var columnId = jQuery(element).data('column-id');
@ -977,25 +872,129 @@ define(
}); });
// refresh table with susbcribers' data // refresh table with susbcribers' data
jQuery('#subscribers_data > table > tbody') jQuery('#subscribers_data > table > tbody')
.html(subscribersDataTemplatePartial(subscribersClone)); .html(subscribersDataTemplatePartial(subscribersClone));
if (preventNextStep) { if (preventNextStep) {
toggleNextStepButton('off'); toggleNextStepButton('off');
} }
else if (!jQuery('.mailpoet_notice.error:visible').length else if (!jQuery('.mailpoet_notice.error:visible').length
&& segmentSelectElement.val()) { && segmentSelectElement.val()) {
toggleNextStepButton('on'); toggleNextStepButton('on');
} }
} }
function toggleNextStepButton(condition) { // render template
var disabled = 'button-disabled'; jQuery('#subscribers_data > table').html(subscribersDataTemplate(subscribers));
if (condition === 'on') {
nextStepButton.removeClass(disabled); // filter displayed data
return; jQuery('select.mailpoet_subscribers_column_data_match')
} .select2({
nextStepButton.addClass(disabled); data: window.mailpoetColumnsSelect2,
} width: '15em',
templateResult: function (item) {
return item.name;
},
templateSelection: function (item) {
return item.name;
}
})
.on('select2:selecting', function (selectEvent) {
var selectElement = this;
var selectedOptionId = selectEvent.params.args.data.id;
// CREATE CUSTOM FIELD
if (selectedOptionId === 'create') {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
MailPoet.Modal.popup({
title: MailPoet.I18n.t('addNewField'),
template: jQuery('#form_template_field_form').html()
});
jQuery('#form_field_new').parsley().on('form:submit', function () {
// get data
var data = jQuery(this.$element).mailpoetSerializeObject();
// save custom field
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'customFields',
action: 'save',
data: data
}).done(function (response) {
var newColumnData = {
id: response.data.id,
name: response.data.name,
type: response.data.type,
params: response.data.params,
custom: true
};
// if this is the first custom column, create an "optgroup"
if (window.mailpoetColumnsSelect2.length === 2) {
window.mailpoetColumnsSelect2.push({
name: MailPoet.I18n.t('userColumns'),
children: []
});
}
window.mailpoetColumnsSelect2[2].children.push(newColumnData);
window.mailpoetColumns.push(newColumnData);
jQuery('select.mailpoet_subscribers_column_data_match')
.each(function () {
jQuery(this)
.html('')
.select2('destroy')
.select2({
data: window.mailpoetColumnsSelect2,
width: '15em',
templateResult: function (item) {
return item.name;
},
templateSelection: function (item) {
return item.name;
}
});
});
jQuery(selectElement).data('column-id', newColumnData.id);
jQuery(selectElement).data('validation-rule', false);
filterSubscribers();
// close popup
MailPoet.Modal.close();
}).fail(function (response) {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function (error) { return error.message; }),
{ positionAfter: '#field_name' }
);
}
});
return false;
});
}
// CHANGE COLUMN
else {
// check for duplicate values in all select options
jQuery('select.mailpoet_subscribers_column_data_match')
.each(function () {
var element = this;
var elementId = jQuery(element).val();
// if another column has the same value and it's not an 'ignore', prompt user
if (elementId === selectedOptionId
&& elementId !== 'ignore') {
if (confirm(MailPoet.I18n.t('selectedValueAlreadyMatched') + ' ' + MailPoet.I18n.t('confirmCorrespondingColumn'))) {
jQuery(element).data('column-id', 'ignore');
}
else {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
}
}
});
}
})
.on('select2:select', function (selectEvent) {
var selectElement = this;
var selectedOptionId = selectEvent.params.data.id;
jQuery(selectElement).data('column-id', selectedOptionId);
filterSubscribers();
});
previousStepButton.off().on('click', function () { previousStepButton.off().on('click', function () {
router.navigate('step1', { trigger: true }); router.navigate('step1', { trigger: true });
@ -1007,22 +1006,21 @@ define(
var batchNumber = 0; var batchNumber = 0;
var batchSize = 2000; var batchSize = 2000;
var timestamp = Date.now() / 1000; var timestamp = Date.now() / 1000;
var subscribers = []; var clickImportResults = {
var importResults = {
created: 0, created: 0,
updated: 0, updated: 0,
errors: [], errors: [],
segments: [] segments: []
}; };
var subscribers; var clickSubscribers;
var splitSubscribers; var splitSubscribers;
if (jQuery(this).hasClass('button-disabled')) { if (jQuery(this).hasClass('button-disabled')) {
return; return;
} }
MailPoet.Modal.loading(true); MailPoet.Modal.loading(true);
splitSubscribers = function (subscribers, size) { splitSubscribers = function (localSubscribers, size) {
return subscribers.reduce(function (res, item, index) { return localSubscribers.reduce(function (res, item, index) {
if (index % size === 0) { if (index % size === 0) {
res.push([]); res.push([]);
} }
@ -1030,7 +1028,7 @@ define(
return res; return res;
}, []); }, []);
}; };
subscribers = splitSubscribers(window.importData.step1.subscribers, batchSize); clickSubscribers = splitSubscribers(window.importData.step1.subscribers, batchSize);
_.each(jQuery('select.mailpoet_subscribers_column_data_match'), _.each(jQuery('select.mailpoet_subscribers_column_data_match'),
function (column, columnIndex) { function (column, columnIndex) {
@ -1042,26 +1040,26 @@ define(
columns[columnId] = { index: columnIndex, validation_rule: validationRule }; columns[columnId] = { index: columnIndex, validation_rule: validationRule };
}); });
_.each(subscribers, function () { _.each(clickSubscribers, function () {
queue.add(function (queue) { queue.add(function (addQueue) {
queue.pause(); addQueue.pause();
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
endpoint: 'ImportExport', endpoint: 'ImportExport',
action: 'processImport', action: 'processImport',
data: JSON.stringify({ data: JSON.stringify({
columns: columns, columns: columns,
subscribers: subscribers[batchNumber], subscribers: clickSubscribers[batchNumber],
timestamp: timestamp, timestamp: timestamp,
segments: segmentSelectElement.val(), segments: segmentSelectElement.val(),
updateSubscribers: (jQuery(':radio[name="subscriber_update_option"]:checked').val() === 'yes') updateSubscribers: (jQuery(':radio[name="subscriber_update_option"]:checked').val() === 'yes')
}) })
}).done(function (response) { }).done(function (response) {
importResults.created += response.data.created; clickImportResults.created += response.data.created;
importResults.updated += response.data.updated; clickImportResults.updated += response.data.updated;
importResults.segments = response.data.segments; clickImportResults.segments = response.data.segments;
importResults.added_to_segment_with_welcome_notification = response.data.added_to_segment_with_welcome_notification; clickImportResults.added_to_segment_with_welcome_notification = response.data.added_to_segment_with_welcome_notification;
queue.run(); addQueue.run();
}).fail(function (response) { }).fail(function (response) {
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
if (response.errors.length > 0) { if (response.errors.length > 0) {
@ -1079,17 +1077,17 @@ define(
queue.onComplete(function () { queue.onComplete(function () {
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
if (importResults.errors.length > 0 && !importResults.updated && !importResults.created) { if (clickImportResults.errors.length > 0 && !clickImportResults.updated && !clickImportResults.created) {
MailPoet.Notice.error(_.flatten(importResults.errors) MailPoet.Notice.error(_.flatten(clickImportResults.errors)
); );
} }
else { else {
window.mailpoetSegments = importResults.segments; window.mailpoetSegments = clickImportResults.segments;
importResults.segments = _.map(segmentSelectElement.select2('data'), clickImportResults.segments = _.map(segmentSelectElement.select2('data'),
function (data) { function (data) {
return data.name; return data.name;
}); });
window.importData.step2 = importResults; window.importData.step2 = clickImportResults;
enableSegmentSelection(window.mailpoetSegments); enableSegmentSelection(window.mailpoetSegments);
router.navigate('step3', { trigger: true }); router.navigate('step3', { trigger: true });
} }

View File

@ -38,7 +38,7 @@ const columns = [
const messages = { const messages = {
onTrash: (response) => { onTrash: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -53,7 +53,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onDelete: (response) => { onDelete: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -68,7 +68,7 @@ const messages = {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
}, },
onRestore: (response) => { onRestore: (response) => {
const count = ~~response.meta.count; const count = Number(response.meta.count);
let message = null; let message = null;
if (count === 1) { if (count === 1) {
@ -100,7 +100,7 @@ const messages = {
}, },
}; };
const bulk_actions = [ const bulkActions = [
{ {
name: 'moveToList', name: 'moveToList',
label: MailPoet.I18n.t('moveToList'), label: MailPoet.I18n.t('moveToList'),
@ -122,13 +122,13 @@ const bulk_actions = [
}, },
getData: function () { getData: function () {
return { return {
segment_id: ~~(jQuery('#move_to_segment').val()), segment_id: Number(jQuery('#move_to_segment').val()),
}; };
}, },
onSuccess: function (response) { onSuccess: function (response) {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersMovedToList') MailPoet.I18n.t('multipleSubscribersMovedToList')
.replace('%$1d', (~~(response.meta.count)).toLocaleString()) .replace('%$1d', (Number(response.meta.count)).toLocaleString())
.replace('%$2s', response.meta.segment) .replace('%$2s', response.meta.segment)
); );
}, },
@ -154,13 +154,13 @@ const bulk_actions = [
}, },
getData: function () { getData: function () {
return { return {
segment_id: ~~(jQuery('#add_to_segment').val()), segment_id: Number(jQuery('#add_to_segment').val()),
}; };
}, },
onSuccess: function (response) { onSuccess: function (response) {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersAddedToList') MailPoet.I18n.t('multipleSubscribersAddedToList')
.replace('%$1d', (~~response.meta.count).toLocaleString()) .replace('%$1d', (Number(response.meta.count)).toLocaleString())
.replace('%$2s', response.meta.segment) .replace('%$2s', response.meta.segment)
); );
}, },
@ -186,13 +186,13 @@ const bulk_actions = [
}, },
getData: function () { getData: function () {
return { return {
segment_id: ~~(jQuery('#remove_from_segment').val()), segment_id: Number(jQuery('#remove_from_segment').val()),
}; };
}, },
onSuccess: function (response) { onSuccess: function (response) {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersRemovedFromList') MailPoet.I18n.t('multipleSubscribersRemovedFromList')
.replace('%$1d', (~~response.meta.count).toLocaleString()) .replace('%$1d', (Number(response.meta.count)).toLocaleString())
.replace('%$2s', response.meta.segment) .replace('%$2s', response.meta.segment)
); );
}, },
@ -203,7 +203,7 @@ const bulk_actions = [
onSuccess: function (response) { onSuccess: function (response) {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersRemovedFromAllLists') MailPoet.I18n.t('multipleSubscribersRemovedFromAllLists')
.replace('%$1d', (~~response.meta.count).toLocaleString()) .replace('%$1d', (Number(response.meta.count)).toLocaleString())
); );
}, },
}, },
@ -213,7 +213,7 @@ const bulk_actions = [
onSuccess: function (response) { onSuccess: function (response) {
MailPoet.Notice.success( MailPoet.Notice.success(
MailPoet.I18n.t('multipleConfirmationEmailsSent') MailPoet.I18n.t('multipleConfirmationEmailsSent')
.replace('%$1d', (~~response.meta.count).toLocaleString()) .replace('%$1d', (Number(response.meta.count)).toLocaleString())
); );
}, },
}, },
@ -224,7 +224,7 @@ const bulk_actions = [
}, },
]; ];
const item_actions = [ const itemActions = [
{ {
name: 'edit', name: 'edit',
label: MailPoet.I18n.t('edit'), label: MailPoet.I18n.t('edit'),
@ -237,23 +237,23 @@ const item_actions = [
{ {
name: 'trash', name: 'trash',
display: function (subscriber) { display: function (subscriber) {
return !!(~~subscriber.wp_user_id === 0); return Number(subscriber.wp_user_id) === 0;
}, },
}, },
]; ];
const SubscriberList = React.createClass({ const SubscriberList = React.createClass({
getSegmentFromId: function (segment_id) { getSegmentFromId: function (segmentId) {
let result = false; let result = false;
window.mailpoet_segments.map((segment) => { window.mailpoet_segments.forEach((segment) => {
if (segment.id === segment_id) { if (segment.id === segmentId) {
result = segment; result = segment;
} }
}); });
return result; return result;
}, },
renderItem: function (subscriber, actions) { renderItem: function (subscriber, actions) {
const row_classes = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
'column-primary', 'column-primary',
'has-row-actions', 'has-row-actions',
@ -278,32 +278,36 @@ const SubscriberList = React.createClass({
case 'bounced': case 'bounced':
status = MailPoet.I18n.t('bounced'); status = MailPoet.I18n.t('bounced');
break; break;
default:
status = 'Invalid';
break;
} }
let segments = false; let segments = false;
// Subscriptions // Subscriptions
if (subscriber.subscriptions.length > 0) { if (subscriber.subscriptions.length > 0) {
const subscribed_segments = []; const subscribedSegments = [];
subscriber.subscriptions.map((subscription) => { subscriber.subscriptions.forEach((subscription) => {
const segment = this.getSegmentFromId(subscription.segment_id); const segment = this.getSegmentFromId(subscription.segment_id);
if (segment === false) return; if (segment === false) return;
if (subscription.status === 'subscribed') { if (subscription.status === 'subscribed') {
subscribed_segments.push(segment.name); subscribedSegments.push(segment.name);
} }
}); });
segments = ( segments = (
<span> <span>
{ subscribed_segments.join(', ') } { subscribedSegments.join(', ') }
</span> </span>
); );
} }
return ( return (
<div> <div>
<td className={row_classes}> <td className={rowClasses}>
<strong> <strong>
<Link <Link
className="row-title" className="row-title"
@ -356,8 +360,8 @@ const SubscriberList = React.createClass({
endpoint="subscribers" endpoint="subscribers"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}
bulk_actions={bulk_actions} bulk_actions={bulkActions}
item_actions={item_actions} item_actions={itemActions}
messages={messages} messages={messages}
sort_by={'created_at'} sort_by={'created_at'}
sort_order={'desc'} sort_order={'desc'}

View File

@ -16,10 +16,12 @@
"nesbot/carbon": "^1.21", "nesbot/carbon": "^1.21",
"soundasleep/html2text": "^0.3.4", "soundasleep/html2text": "^0.3.4",
"sabberworm/php-css-parser": "^8.1", "sabberworm/php-css-parser": "^8.1",
"symfony/polyfill-xml": "^1.3" "symfony/polyfill-xml": "^1.3",
"symfony/polyfill-mbstring": "1.6.0",
"sensiolabs/security-checker": "^4.1"
}, },
"require-dev": { "require-dev": {
"codeception/aspect-mock": "^2.0", "codeception/aspect-mock": "2.0.1",
"codeception/codeception": "2.3.5", "codeception/codeception": "2.3.5",
"codeception/verify": "^0.3.3", "codeception/verify": "^0.3.3",
"consolidation/robo": "^1.0.5", "consolidation/robo": "^1.0.5",

1351
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ services:
codeception: codeception:
build: . build: .
depends_on: depends_on:
- mailhog
- wordpress - wordpress
volumes: volumes:
- ./:/project - ./:/project
@ -11,6 +12,12 @@ services:
- ./:/wp-core/wp-content/plugins/mailpoet - ./:/wp-core/wp-content/plugins/mailpoet
entrypoint: /docker-entrypoint.sh entrypoint: /docker-entrypoint.sh
mailhog:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
wordpress: wordpress:
build: ./tests/wordpressDockerfile build: ./tests/wordpressDockerfile
image: wordpress:latest image: wordpress:latest
@ -25,8 +32,6 @@ services:
- 8080:80 - 8080:80
environment: environment:
WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_TABLE_PREFIX: mp_
mysql: mysql:
image: mysql:5.6 image: mysql:5.6
environment: environment:
@ -42,7 +47,7 @@ services:
- /dev/shm:/dev/shm - /dev/shm:/dev/shm
image: selenium/standalone-chrome-debug image: selenium/standalone-chrome-debug
ports: ports:
- '4444' - 4444
- '5900:5900' - 5900:5900
volumes: volumes:
wp-core: wp-core:

View File

@ -28,9 +28,11 @@ if ! $(wp-su core is-installed); then
echo "Configuring WordPress" echo "Configuring WordPress"
# The development version of Gravity Flow requires SCRIPT_DEBUG # The development version of Gravity Flow requires SCRIPT_DEBUG
wp-su core config --dbhost=mysql --dbname=wordpress --dbuser=wordpress --dbpass=wordpress --extra-php="define( 'SCRIPT_DEBUG', true );" --force wp-su core config --dbhost=mysql --dbname=wordpress --dbuser=wordpress --dbpass=wordpress --extra-php="define( 'SCRIPT_DEBUG', true );" --force
fi fi
# Change default table prefix
sed -i "s/\$table_prefix = 'wp_';/\$table_prefix = 'mp_';/" ./wp-config.php
# Load Composer dependencies # Load Composer dependencies
# Set KEEP_DEPS environment flag to not redownload them on each run, only for the 1st time, useful for development. # Set KEEP_DEPS environment flag to not redownload them on each run, only for the 1st time, useful for development.
# Example: docker-compose run -e KEEP_DEPS=1 codeception ... # Example: docker-compose run -e KEEP_DEPS=1 codeception ...

View File

@ -10,7 +10,9 @@ use MailPoet\Form\Util\FieldNameObfuscator;
use MailPoet\Models\Form; use MailPoet\Models\Form;
use MailPoet\Models\StatisticsForms; use MailPoet\Models\StatisticsForms;
use MailPoet\Models\Subscriber; use MailPoet\Models\Subscriber;
use MailPoet\Segments\SubscribersListings;
use MailPoet\Subscription\Throttling as SubscriptionThrottling; use MailPoet\Subscription\Throttling as SubscriptionThrottling;
use MailPoet\WP\Hooks;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
@ -40,12 +42,15 @@ class Subscribers extends APIEndpoint {
} }
function listing($data = array()) { function listing($data = array()) {
$listing = new Listing\Handler(
'\MailPoet\Models\Subscriber',
$data
);
$listing_data = $listing->get(); if(!isset($data['filter']['segment'])) {
$listing = new Listing\Handler('\MailPoet\Models\Subscriber', $data);
$listing_data = $listing->get();
} else {
$listings = new SubscribersListings();
$listing_data = $listings->getListingsInSegment($data);
}
$data = array(); $data = array();
foreach($listing_data['items'] as $subscriber) { foreach($listing_data['items'] as $subscriber) {
@ -54,6 +59,10 @@ class Subscribers extends APIEndpoint {
->asArray(); ->asArray();
} }
$listing_data['filters']['segment'] = Hooks::applyFilters(
'mailpoet_subscribers_listings_filters_segments',
$listing_data['filters']['segment']
);
return $this->successResponse($data, array( return $this->successResponse($data, array(
'count' => $listing_data['count'], 'count' => $listing_data['count'],
'filters' => $listing_data['filters'], 'filters' => $listing_data['filters'],

View File

@ -42,8 +42,12 @@ class API {
} }
function subscribeToLists($subscriber_id, array $segments_ids) { function subscribeToLists($subscriber_id, array $segments_ids) {
$subscriber = Subscriber::findOne($subscriber_id); if(empty($segments_ids)) {
throw new \Exception(__('At least one segment ID is required.', 'mailpoet'));
}
// throw exception when subscriber does not exist // throw exception when subscriber does not exist
$subscriber = Subscriber::findOne($subscriber_id);
if(!$subscriber) { if(!$subscriber) {
throw new \Exception(__('This subscriber does not exist.', 'mailpoet')); throw new \Exception(__('This subscriber does not exist.', 'mailpoet'));
} }
@ -51,7 +55,8 @@ class API {
// throw exception when none of the segments exist // throw exception when none of the segments exist
$found_segments = Segment::whereIn('id', $segments_ids)->findMany(); $found_segments = Segment::whereIn('id', $segments_ids)->findMany();
if(!$found_segments) { if(!$found_segments) {
throw new \Exception(__('These lists do not exist.', 'mailpoet')); $exception = _n('This list does not exist.', 'These lists do not exist.', count($segments_ids), 'mailpoet');
throw new \Exception($exception);
} }
// throw exception when trying to subscribe to a WP Users segment // throw exception when trying to subscribe to a WP Users segment
@ -66,13 +71,62 @@ class API {
// throw an exception when one or more segments do not exist // throw an exception when one or more segments do not exist
if(count($found_segments_ids) !== count($segments_ids)) { if(count($found_segments_ids) !== count($segments_ids)) {
$missing_ids = array_values(array_diff($segments_ids, $found_segments_ids)); $missing_ids = array_values(array_diff($segments_ids, $found_segments_ids));
throw new \Exception(__(sprintf('Lists with ID %s do not exist.', implode(', ', $missing_ids)), 'mailpoet')); $exception = sprintf(
_n('List with ID %s does not exist.', 'Lists with IDs %s do not exist.', count($missing_ids), 'mailpoet'),
implode(', ', $missing_ids)
);
throw new \Exception(sprintf($exception, implode(', ', $missing_ids)));
} }
SubscriberSegment::subscribeToSegments($subscriber, $found_segments_ids); SubscriberSegment::subscribeToSegments($subscriber, $found_segments_ids);
return $subscriber->withCustomFields()->withSubscriptions()->asArray(); return $subscriber->withCustomFields()->withSubscriptions()->asArray();
} }
function unsubscribeFromList($subscriber_id, $segment_id) {
return $this->unsubscribeFromLists($subscriber_id, array($segment_id));
}
function unsubscribeFromLists($subscriber_id, array $segments_ids) {
if(empty($segments_ids)) {
throw new \Exception(__('At least one segment ID is required.', 'mailpoet'));
}
// throw exception when subscriber does not exist
$subscriber = Subscriber::findOne($subscriber_id);
if(!$subscriber) {
throw new \Exception(__('This subscriber does not exist.', 'mailpoet'));
}
// throw exception when none of the segments exist
$found_segments = Segment::whereIn('id', $segments_ids)->findMany();
if(!$found_segments) {
$exception = _n('This list does not exist.', 'These lists do not exist.', count($segments_ids), 'mailpoet');
throw new \Exception($exception);
}
// throw exception when trying to subscribe to a WP Users segment
$found_segments_ids = array();
foreach($found_segments as $segment) {
if($segment->type === Segment::TYPE_WP_USERS) {
throw new \Exception(__(sprintf("Can't subscribe to a WordPress Users list with ID %d.", $segment->id), 'mailpoet'));
}
$found_segments_ids[] = $segment->id;
}
// throw an exception when one or more segments do not exist
if(count($found_segments_ids) !== count($segments_ids)) {
$missing_ids = array_values(array_diff($segments_ids, $found_segments_ids));
$exception = sprintf(
_n('List with ID %s does not exist.', 'Lists with IDs %s do not exist.', count($missing_ids), 'mailpoet'),
implode(', ', $missing_ids)
);
throw new \Exception($exception);
}
SubscriberSegment::unsubscribeFromSegments($subscriber, $found_segments_ids);
return $subscriber->withCustomFields()->withSubscriptions()->asArray();
}
function getLists() { function getLists() {
return Segment::whereNotEqual('type', Segment::TYPE_WP_USERS)->findArray(); return Segment::whereNotEqual('type', Segment::TYPE_WP_USERS)->findArray();
} }
@ -133,6 +187,46 @@ class API {
return $new_subscriber->withCustomFields()->withSubscriptions()->asArray(); return $new_subscriber->withCustomFields()->withSubscriptions()->asArray();
} }
function addList(array $list) {
// throw exception when list name is missing
if(empty($list['name'])) {
throw new \Exception(
__('List name is required.', 'mailpoet')
);
}
// throw exception when list already exists
if(Segment::where('name', $list['name'])->findOne()) {
throw new \Exception(
__('This list already exists.', 'mailpoet')
);
}
// add list
$new_list = Segment::create();
$new_list->hydrate($list);
$new_list->save();
if($new_list->getErrors() !== false) {
throw new \Exception(
__(sprintf('Failed to add list: %s', strtolower(implode(', ', $new_list->getErrors()))), 'mailpoet')
);
}
// reload list to get the saved created|updated|delete dates/other fields
$new_list = Segment::findOne($new_list->id);
return $new_list->asArray();
}
function getSubscriber($subscriber_email) {
$subscriber = Subscriber::findOne($subscriber_email);
// throw exception when subscriber does not exist
if(!$subscriber) {
throw new \Exception(__('This subscriber does not exist.', 'mailpoet'));
}
return $subscriber->withCustomFields()->withSubscriptions()->asArray();
}
protected function _sendConfirmationEmail(Subscriber $subscriber) { protected function _sendConfirmationEmail(Subscriber $subscriber) {
return $subscriber->sendConfirmationEmail(); return $subscriber->sendConfirmationEmail();
} }

View File

@ -12,7 +12,7 @@ use MailPoet\Settings\Pages;
class Reporter { class Reporter {
function getData() { function getData() {
global $wpdb, $wp_version;
$mta = Setting::getValue('mta', array()); $mta = Setting::getValue('mta', array());
$newsletters = Newsletter::getAnalytics(); $newsletters = Newsletter::getAnalytics();
$isCronTriggerMethodWP = Setting::getValue('cron_trigger.method') === CronTrigger::$available_methods['wordpress']; $isCronTriggerMethodWP = Setting::getValue('cron_trigger.method') === CronTrigger::$available_methods['wordpress'];
@ -22,6 +22,15 @@ class Reporter {
return array( return array(
'PHP version' => PHP_VERSION, 'PHP version' => PHP_VERSION,
'MySQL version' => $wpdb->db_version(),
'WordPress version' => $wp_version,
'Multisite environment' => is_multisite() ? 'yes' : 'no',
'RTL' => is_rtl() ? 'yes' : 'no',
'WP_MEMORY_LIMIT' => WP_MEMORY_LIMIT,
'WP_MAX_MEMORY_LIMIT' => WP_MAX_MEMORY_LIMIT,
'PHP memory_limit' => ini_get('memory_limit'),
'PHP max_execution_time' => ini_get('max_execution_time'),
'users_can_register' => get_option('users_can_register') ? 'yes' : 'no',
'MailPoet Free version' => MAILPOET_VERSION, 'MailPoet Free version' => MAILPOET_VERSION,
'MailPoet Premium version' => (defined('MAILPOET_PREMIUM_VERSION')) ? MAILPOET_PREMIUM_VERSION : 'N/A', 'MailPoet Premium version' => (defined('MAILPOET_PREMIUM_VERSION')) ? MAILPOET_PREMIUM_VERSION : 'N/A',
'Total number of subscribers' => Subscriber::getTotalSubscribers(), 'Total number of subscribers' => Subscriber::getTotalSubscribers(),

View File

@ -22,8 +22,6 @@ class AccessControl {
function __construct() { function __construct() {
$this->permissions = self::getDefaultPermissions(); $this->permissions = self::getDefaultPermissions();
$this->user_roles = $this->getUserRoles();
$this->user_capabilities = $this->getUserCapabilities();
} }
static function getDefaultPermissions() { static function getDefaultPermissions() {
@ -80,30 +78,8 @@ class AccessControl {
); );
} }
function getUserRoles() {
$user = wp_get_current_user();
return $user->roles;
}
function getUserCapabilities() {
$user = wp_get_current_user();
return array_keys($user->allcaps);
}
function getUserFirstCapability() {
return (!empty($this->user_capabilities)) ?
$this->user_capabilities[0] :
null;
}
function validatePermission($permission) { function validatePermission($permission) {
if($permission === self::NO_ACCESS_RESTRICTION) return true; if($permission === self::NO_ACCESS_RESTRICTION) return true;
foreach($this->user_roles as $role) { return current_user_can($permission);
$role_object = get_role($role);
if($role_object && $role_object->has_cap($permission)) {
return true;
}
}
return false;
} }
} }

View File

@ -115,8 +115,7 @@ class Initializer {
} }
function setupWidget() { function setupWidget() {
$widget = new Widget($this->renderer); register_widget('\MailPoet\Form\Widget');
$widget->init();
} }
function initialize() { function initialize() {
@ -232,6 +231,7 @@ class Initializer {
$this->setupHooks(); $this->setupHooks();
$this->setupJSONAPI(); $this->setupJSONAPI();
$this->setupRouter(); $this->setupRouter();
$this->setupUserLocale();
} catch(\Exception $e) { } catch(\Exception $e) {
$this->handleFailedInitialization($e); $this->handleFailedInitialization($e);
} }
@ -247,6 +247,13 @@ class Initializer {
$router->init(); $router->init();
} }
function setupUserLocale() {
if(get_user_locale() === get_locale()) return;
unload_textdomain(Env::$plugin_name);
$localizer = new Localizer();
$localizer->init();
}
function setupPages() { function setupPages() {
$pages = new \MailPoet\Settings\Pages(); $pages = new \MailPoet\Settings\Pages();
$pages->init(); $pages->init();

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