Compare commits

...

406 Commits

Author SHA1 Message Date
22bf4ec38b Release 4.0.1 2022-11-22 09:29:32 +01:00
7570c9fa0e Make automations Beta badge smaller
[MAILPOET-4836]
2022-11-22 09:23:57 +01:00
7642c4da4f Update machine executor images
[MAILPOET-4833]
2022-11-21 16:06:58 -03:00
dce13e5b8e Update sentence, add full stop.
MAILPOET-4669
2022-11-21 15:21:29 +01:00
8b922681b6 Use boolean instead of integer for displayInManageSubscriptionPage column
MAILPOET-4669
2022-11-21 15:21:29 +01:00
bd33cdbc24 Add new migration file for DisplayInManageSubscriptionPage column
MAILPOET-4669
2022-11-21 15:21:29 +01:00
bc04ad2528 Enable default checked state for standard lists checkbox
MAILPOET-4669
2022-11-21 15:21:29 +01:00
c54a5213b3 Update tests
MAILPOET-4669
2022-11-21 15:21:29 +01:00
07cf0cd79b Remove list management from settings page
MAILPOET-4669
2022-11-21 15:21:29 +01:00
e64dc1669f Add support for segment displayInManageSubscriptionPage on Subscription management page
MAILPOET-4669
2022-11-21 15:21:29 +01:00
48181994d8 Handle saving and loading list page. Add support for showInManageSubscriptionPage checkbox input
MAILPOET-4669
2022-11-21 15:21:29 +01:00
31c2915075 Add showInManageSubscriptionPage checkbox to edit list page
MAILPOET-4669
2022-11-21 15:21:29 +01:00
67f05ecfe2 Prefix forgotten Twig call in generated template code
[MAILPOET-4718]
2022-11-21 12:47:19 +01:00
c121c60e8c Fix condition for the running status
[MAILPOET-4664]
2022-11-21 12:45:28 +01:00
110859508e Fix tasks order on the status page
[MAILPOET-4664]
2022-11-21 12:45:28 +01:00
efd3377ecd Fix count of tasks on the status page
[MAILPOET-4664]
2022-11-21 12:45:28 +01:00
a7c1d8431d Update tests for [subscriber:count] shortcode
MAILPOET-4527
2022-11-21 12:36:12 +01:00
127f3e6363 Fix [subscriber:count] counts only subscribed subscribers
MAILPOET-4527
2022-11-21 12:36:12 +01:00
6bec12c762 Use helper function to create migration tables
[MAILPOET-4835]
2022-11-21 12:15:10 +01:00
d1df2d4f42 Add a helper function to create new table
[MAILPOET-4835]
2022-11-21 12:15:10 +01:00
5598f0af19 Fixing small typo
Automatoin should be automation
2022-11-21 11:23:01 +01:00
8dd80ce0f2 Fix the circle rerun command output
[PREMIUM-207]
2022-11-18 16:05:18 +01:00
e45510e064 Add rerunning premium workflow into the prepare command
[PREMIUM-207]
2022-11-18 16:05:18 +01:00
bfafff132a Make the project parameter optional
[PREMIUM-207]
2022-11-18 16:05:18 +01:00
d6748fe979 Add command for rerunning circle workflow
[PREMIUM-207]
2022-11-18 16:05:18 +01:00
2169522818 Add methods for rerunning Circle workflow
[PREMIUM-207]
2022-11-18 16:05:18 +01:00
06cfa3fc60 Shorten test and run action-scheduler group
[MAILPOET-4746]
2022-11-17 22:25:52 +01:00
f9e507eb15 Use latest WooCommerce version for Woo HPOS related tests
Before WooCommerce 7.1.0 was released, we were defining a specific
version of Woo that included HPOS to be used when running HPOS related
tests. Now that 7.1.0 was released and since we started using it by
default in bd51822b961a6f8ad10d3454cde912917acac0b6, we don't need to
specify a different Woo version for the HPOS tests.

[MAILPOET-4801]
2022-11-17 14:55:38 +01:00
90332a23b3 Use latest WooCommerce version in the CircleCI jobs
[MAILPOET-4801]
2022-11-17 14:55:38 +01:00
a82a810269 Update form templates to include privacy page link (if found)
MAILPOET-4651
2022-11-16 16:51:12 +01:00
5a378b3c5f Add replacePrivacyLinkTags method for Form Template
MAILPOET-4651
2022-11-16 16:51:12 +01:00
49a2df3dbb Release 4.0.0 2022-11-16 09:43:17 +01:00
dee5ff38f5 Fix error when sending a preview email with MSS
With HTTPS and "$oneClickUnsubscribeUrl = false", we were sending "false" instead of an actual URL.

[MAILPOET-4813]
2022-11-15 15:05:03 +01:00
70aefb421b Clear error when rerunning migration
[MAILPOET-4811]
2022-11-14 20:12:35 +03:00
764e1a1bf0 Rename "Not active" state to "Draft" on automations listing page
[MAILPOET-4810]
2022-11-14 19:51:34 +03:00
b140011f92 Update minimal version check for WooCommerceCheckoutBlockCest
Since we support the Woo Blocks integration only for versions from 8.0.0
we need to update minimal version check code for acceptance test.
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
b48c9e437e Update plugins in tests for feature and trunk branches
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
f34efb4d6f Update acceptance test oldest job configuration
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
324c03e8b9 Print WordPress version when starting test environment
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
e718101425 Upgrade minimum required WordPress version to 5.8
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
080ce50fea Open automations Beacon links in a new tab instead of HelpScout Beacon
[MAILPOET-4809]
2022-11-14 18:53:20 +03:00
1b9eb223b0 Load templates late
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
ea2fa794ac Limit varchar length to 191 to avoid unique/primary key issues
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
c46c61a923 Simplify and optimize automation stats
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
1136431551 Use $wpdb->prepare() for automation stats
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
957be23212 Improve query formatting
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
91a88b3e91 Rename alias "w" to "a"
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
7c5d239267 Fix and unify cleanup in automation tests
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
c748b80447 Fix timestamp column problems with some MySQL versions and some SQL modes
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
725e0ecb00 Remove automation feature flag
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
eb71dd8a68 Remove automation testing buttons and hooks
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
ce1687cf97 Replace custom mutation hook with a @wordpress/api-fetch based one
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
2e328b6d7f Move temporary automation migrator to a new migration
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
8489e63d34 Delete automation testing system endpoints
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
f8c17730fc Update tracking campaign
[MAILPOET-4712]
2022-11-14 11:22:57 +01:00
93e43eee7a Update sales banner for Black Friday 2022
[MAILPOET-4712]
2022-11-14 11:22:57 +01:00
9063dc3079 Remove duplicities in naming
[MAILPOET-4793]
2022-11-14 12:14:35 +02:00
9d55d3f134 Use "automation" instead of "workflow"
[MAILPOET-4793]
2022-11-14 12:14:35 +02:00
d199c3768a Show Bounced and Unsubscribed badge for minimum of 100 newsletter sent
The reason is that at low volumes the percentages can be misleading. E.g. if you send 20 emails and 1 hard bounces - that’s a hard bounce rate of 5%.

The percentage is very high, but a single hard bounce doesn’t really mean anything, so to avoid confusion I’d hide the badge altogether for low volumes.

MAILPOET-4688
2022-11-14 10:47:04 +01:00
0e0c2447d9 Fix issue with badge stats
We were using MailPoet.Num.toLocaleFixed which returns a string locale of the number. This can cause issues with some locales e.g some locale returns 2,3 instead of 2.3

MAILPOET-4688
2022-11-14 10:47:04 +01:00
99dfb3d24b Add formatForStats method for Unsubscribed and Bounced Stats
MAILPOET-4688
2022-11-14 10:47:04 +01:00
c4366e009b Fix crashing page due to newsletter not found error
MAILPOET-4688
2022-11-14 10:47:04 +01:00
1d31202607 Add badge for Unsubscribe, bounced and opened stat to the Advanced email statistics page
MAILPOET-4688
2022-11-14 10:47:04 +01:00
78d25ffb69 Add Stats Badge for opened rate on email listing
MAILPOET-4688
2022-11-14 10:47:04 +01:00
b710682c66 Replace average with critical badge on email listing for opened and clicked stats
MAILPOET-4688
2022-11-14 10:47:04 +01:00
52c6b94315 Add support for critical tag in newsletter badge
MAILPOET-4688
2022-11-14 10:47:04 +01:00
fd6b49e598 Add type TagVariant for variant strings
MAILPOET-4688
2022-11-14 10:47:04 +01:00
df2982454e Add support for Critical Tag and critical tag color
MAILPOET-4688
2022-11-14 10:47:04 +01:00
8a19fd906f Update test to check for distinct one-click urls in bulk
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
99198e5c2d Fix MailPoet\WP\Functions newly added method name
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
9204f37560 Remove redundant injection
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
765aa6efab Augment tests to acknowledge oneclick unsubscribe url
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
50b613365f Make sure one-click unsubscribe url won't redirect internally
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
4fe8d10d6c Update tests for Subscription and pass the new argument
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
162dab790d Implement 1-click unsubscribe strategy for post requests
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
6e8c9731d8 Define new isSiteUsingHttps method for WP_Functions
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
235552f91d Define Request utility class with isPost method
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
4ee08c296b Fix unknown storage engine 'InnoDB' error
[MAILPOET-4802]
2022-11-10 12:59:24 +01:00
bda979ec4f Fix trash error when workflow status is draft
[MAILPOET-4792]
2022-11-09 19:58:44 +03:00
78f10f064e Fix clicking on no longer existing text
[MAILPOET-4796]
2022-11-09 15:58:36 +01:00
eca4a68fec Revert broken test fix
Reverts: https://github.com/mailpoet/mailpoet/pull/4511

[MAILPOET-4796]
2022-11-09 15:58:36 +01:00
9daa5d58c7 Adjust tests to new templates
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
6cc232fbad Make premium badge mailpoet orange
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
6a07bd44ff Improve 'Coming soon' styles
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
63cd326191 Update template descriptions
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
40b15b0eb1 Lower the grid size for the badge
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
3825e9cb11 Fix test
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
7db2942140 Show PremiumModal for premium templates
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
3a5d28f24a Export const premiumValidAndActive
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
d02c63844d Add badges to template items
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
c3045dba07 Add type to WorkflowTemplate definition
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
c239566e1e Add premium and coming-soon template stubs
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
dfdc8cfd09 Add WorkflowTemplate types
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
4977b7ffa2 Add free templates
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
8e69299a6a Adjust naming and use wp wrapper I/O of direct usage
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
903bcbb92e Update tests and adapt to the new unsubscribe key structure
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
8d6492ac8c Adjust constructor injections of instantiators of MailPoet
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
c39ae1fe1f Adjust the unsubscribe key to use the new structure
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
e3539b06a2 Define URL utility method to check if url is using https
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
d34a265ac2 Prevent leaving unsaved automation
[MAILPOET-4776]
2022-11-09 13:42:56 +03:00
9a72e361b1 Fix plugin activtion for the smtp plugin
[MAILPOET-4785]
2022-11-09 12:59:29 +03:00
a520e4bd93 Run full activation of user account on multisite
[MAILPOET-4785]
2022-11-09 12:59:29 +03:00
9ab6ebbe0d Use always SMTP to send wp_mail
[MAILPOET-4785]
2022-11-09 12:59:29 +03:00
6a3cfd05e7 Update CircleCI to wait for Woo jobs to complete before starting build_release_zip
In [MAILPOET-4572] we added new CircleCI jobs to run WooCommerce tests in
different scenarios. But we forgot to add those jobs to the list of jobs
that need to finish before starting the build_release_zip job.

This can be problematic as we don’t want the build_release_zip to start
before all the test jobs have finished and, even worst, we don’t want it
to run if one of the test jobs failed.

This commit adds the following jobs to the list of jobs that are
required to finish before running build_release_zip:

- integration_test_woo_cot_no_sync
- integration_test_woo_cot_off
- integration_test_woo_cot_sync
- acceptance_tests_woo_cot_sync
- acceptance_tests_woo_cot_off
- acceptance_tests_woo_cot_no_sync

[MAILPOET-4791]
2022-11-09 09:07:21 +01:00
f825e535e3 Save next step in trigger handler
[MAILPOET-4787]
2022-11-08 22:55:57 +02:00
a2c5420d7f Save updated at timestamp in workflow runs
[MAILPOET-4787]
2022-11-08 22:55:57 +02:00
c6d3573652 Save next step ID to workflow runs
[MAILPOET-4787]
2022-11-08 22:55:57 +02:00
e41bcd0d02 Manage ActivationPanel in store and close when sidebar opens
[MAILPOET-4769]
2022-11-08 23:31:36 +03:00
f805907954 Release 3.103.1 2022-11-08 17:35:04 +01:00
d970efc0da Reduce size of a database field to avoid error in some MySQL versions
This commit reduces the size of the `name` field from varchar(255) to
varchar(191) to avoid the following fatal error in some MySQL versions:

```
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes
```

See the ticket descriptions and the links in it for more information.

[MAILPOET-4790]
2022-11-08 12:27:01 -03:00
c0ce5944dc Use short table aliases
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
467f354eb1 Do not skip workflow trigger queries when trigger keys are empty
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
3f016b45f9 Don't escape table names when not needed
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
679f74e498 Unify query error checking
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
08b314e0b4 Unify table variable naming
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
cc5959805b Use "workflow_triggers" table instead of an inline JSON
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
407f3d1609 Hide duplicate workflow button
[MAILPOET-4786]
2022-11-08 16:48:35 +03:00
503df3584c Track errors
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
fa9510f0c1 Add getStepById method
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
cc92df4e7f Track Automations > Listing viewed event
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
d76c5d32f2 Track Workflow deactivated
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
c6198cba4c Track Workflow activated
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
e29dd4286e Track template selected
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
98c1c33341 Release 3.103.0 2022-11-08 10:33:23 -03:00
78cc8743d5 Skip tests that are failing to unblock a release preparation
Those two tests that are failing are related to the automation feature
which is not yet available to the users. So skipping them should be
safe.

[MAILPOET-4784]
2022-11-07 23:42:16 +03:00
89e75b3828 Make test passable for both multi-side and single 2022-11-07 15:16:44 -03:00
982d104ba0 Fix the issue in test dot instead plus [MAILPOET-4782]
[MAILPOET-4782]
2022-11-07 15:16:44 -03:00
2ce4461c63 Fix acceptance test UserRegistrationTriggered [MAILPOET-4782]
[MAILPOET-4782](https://mailpoet.atlassian.net/browse/MAILPOET-4782)
2022-11-07 15:16:44 -03:00
146a55b921 Improve stability of SchedulerTest by adding 1s delta to time comparisons
[MAILPOET-4780]
2022-11-07 19:15:43 +01:00
7e883d3e5b Rename method from checkForNullOrUndefined to isNullOrUndefined
MAILPOET-4681
2022-11-07 14:17:50 +01:00
7896efcd6c Enforce user selection for welcome wizard privacy and data sharing
We are making the two buttons mandatory. Users have to select either yes or no before they can proceed with the welcome wizard

MAILPOET-4681
2022-11-07 14:17:50 +01:00
a1ef87ef78 Remove later phrase from options
MAILPOET-4658
2022-11-07 12:42:58 +01:00
d058f1741f Add after text for immediately option on the post notification create screen.
MAILPOET-4658
2022-11-07 12:42:58 +01:00
ff0852fa39 Update after delay text
MAILPOET-4658
2022-11-07 12:42:58 +01:00
5f07b68b07 Update post notification emails
Remove select frequency subheader and add "after publishing a post" to immediately text

MAILPOET-4658
2022-11-07 12:42:58 +01:00
0c2934a556 Update Post notifications email setup
Add "When to send this post notification email" text to the page

MAILPOET-4658
2022-11-07 12:42:58 +01:00
885b51a6cf Update PurchasedInCategory and PurchasedProduct
Add "after purchase" text to the page

MAILPOET-4658
2022-11-07 12:42:58 +01:00
6cb5ceca21 Update First purchase automatic emails
Add "When to send this email" and "after first purchase" texts to the page

MAILPOET-4658
2022-11-07 12:42:58 +01:00
9a14ddb57c Fire mailpoet_segment_subscribed only when subscriber status changed
[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
5c6f32488a Remove old hook
[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
4ec8f27d36 Delete schema if necessary
[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
9554727370 Fire mailpoet_segment_subscribed when user registers and is subscribed
When no confirmation mail gets send, the user is subscribed immediately. In this case
we need to fire the mailpoet_segment_subscribed action, so the trigger can listen to this
event.

[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
394f9abd67 Make sure, we have a fresh subscriber from the database
[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
6ee45f0f54 Add method to retrieve subsccriber
[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
2e9ae1ed0b Add info about subscribe in registration from setting
[MAILPOET-4772]
2022-11-07 04:41:47 -06:00
74b12bd2d9 Change trigger to mailpoet_segment_subscribed
We should listen to mailpoet_segment_subscribed to start the trigger.
This means the subscriber is confirmed. And it removes the fatal error
which was produced by not finding the $segment during signup.

[MAILPOET-4772]
[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
279adcfbcf Test user registration triggers automation
[MAILPOET-4773]
2022-11-07 04:41:47 -06:00
64d655857b Explicitly declare compatibility with Woo High Performance Order Tables
This was done following the instructions published by the WooCommerce
team: pcShBQ-oY-p2

[MAILPOET-4726]
2022-11-07 10:42:38 +01:00
b2b5f3c22a Add Woo tags "WC requires at least" and "WC tested up to"
As discussed in this P2 post (pcNwfB-1lW-p2), we will start using the
Woo tags "WC requires at least" and "WC tested up to". The "WC tested up
to" tag is required by WooCommerce for extensions to explicitly declare
compatibility with Woo High Performance Order Tables (pcShBQ-oY-p2). The
"WC requires at least" is not currently used by WooCommerce, but it
helps us make more explicit which versions of Woo MailPoet currently
supports.

[MAILPOET-4726]
2022-11-07 10:42:38 +01:00
588f3a9feb Use "reply to" from settings only when they are different than "from" settings
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
8c676b773d Fix hook type
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
c76a0adb5e Move validation rules for active workflows below structural rules
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
848fc51070 Run full workflow validation for all non-draft statuses
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
af1e09f46f Remove "inactive" status for now, use "draft" instead
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
f1abfe557a Use undefined for no value in GA campaign field
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
aa52ca804e Expose send email create step filter
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
273551fa36 Add type for a step creation callback
[MAILPOET-4757]x
2022-11-07 11:17:36 +02:00
17898b243a Use default reply to values when no previous values are set
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
81ed12d454 Use previous reply to values when toggled off and on
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
2f78c99381 Fix reply-to panel empty value handling
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
480c7d25e0 Delete step args when they are set to "undefined"
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
c1a6c5a215 Force sidebar remount to avoid different steps mixing their data
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
8fadc87036 Enable delay in minutes
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
5176566882 Remove debug info from step panel
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
b52b9990da Improve workflow name rendering in listing
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
8796599d89 Automatically open sidebar panels when their fields contain errors
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
9808245d30 Set errors object also for non-step errors to propagate correct state
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
12f2d1730f Do not enforce workflow content for non-active workflows
[MAILPOET-4757]
2022-11-07 11:17:36 +02:00
dac8c1e2f3 Improve assertEqualDateTimes helper interface
Make sure that both dates parameters have to be passed into the method.
[MAILPOET-4723]
2022-11-07 10:10:17 +01:00
a3e1f8d8c6 Fix flaky AbandonedCartTest
Fix the flaky DateTime comparisons test by allowing 1 second delta
[MAILPOET-4723]
2022-11-07 10:10:17 +01:00
2c28449b58 Add helper method for comparing DateTimeInterface objects
This commit adds a method to the integration tester that allows
comparing two DateTimeInterface objects and specify tolerated delta.
It also allows passing null and assert the DateTimeInterface internally.
This is because often our entities have return type DateTimeInterface|null
So with the internal checks we don't have to make those instance of checks
each time we pass date from entity into the method.
Note: I was not able to use $this->assertInstanceOf because PHPStan was not accepting that
and still complained.
[MAILPOET-4723]
2022-11-07 10:10:17 +01:00
43197bf859 Use 24-hour format in migration timestamps
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
bd34c4e7ec Ignore index.php in migrations directory
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
511b8ad7ff Retry timed out and failed migrations
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
a60562254c Temporarily fix reinstall test wiping out automation tables
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
96368cbd3f Move one-time migrations from populator to the new initial migration
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
e384e7dea3 Merge inactive subscribers frequency test into the new initial migration test
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
86fbcd3a57 Annotate types for PHPStan rather than using baseline files
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
9730fb9272 Move the old dbDelta-based migrator to a migration
This will ensure it runs once and from there, it will continue using the new migrations.

[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
a3f2ee8bbb Implement CLI output logging when running migrations
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
a177b3b8da Add the possibility to use a logger in migrator
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
f369fdfa4a Add ./do migrations:new command to generate migration files
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
80a6e78d48 Allow underscored naming in lib/Migrations
This is to allow readable naming of migration files and classes, such as "Migration_20221023_040819".
2022-11-07 10:05:42 +01:00
f65821256c Run migrations on activation
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
dd10389c55 Implement WP CLI command to show migrations info
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
3dd6ef3da7 Implement WP CLI command to run migrations
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
036d0a29ae Implement full migration running logic
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
d199e0b77e Add migrator service with ability to resolve status of all migrations
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
17ab79aa6f Add store method to list all migrations
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
b10cd32449 Add runner to execute a migration and save its status
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
f9001c1826 Add store methods to start, complete, and fail a migration
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
31050394b9 Add a store with a method to create migrations table when it doesn't exist
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
e9970f3cc8 Implement loading existing migration classes from the filesystem
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
516c460ace Add repository with the ability to create new migration classes
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
03d80290e6 Add a migration template that will be used to generate new migrations
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
d65b567858 Add abstract migration class with access to connection, entity manager, and the DI container
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
0d10f4b1dd Enable mocking non-object properties in test service overrides
[MAILPOET-4466]
2022-11-07 10:05:42 +01:00
610d0204eb Add link to email statistics page
[MAILPOET-4777]
2022-11-07 10:25:21 +02:00
545a4adc66 Fix typo
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
38d8a1cd00 Wrap updateingActiveWorkflowNotPossible in useEffect
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
53201c7422 Rename imported select
[MAILPOET4744]
2022-11-07 02:22:21 -06:00
6f447e8a93 Show snackbar message on boot when updating not possible
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
368a400fea Allow updating an active workflow when no user is in progress
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
902eeccea1 Block updating workflows with active runs in the backend
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
c8cf00a151 Add delay to tooltips
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
f41423e6a2 Add period at the end of the sentence
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
774485d5f2 Deactivate 'update/activate' buttons when status is active/deactivting
[MAILPOET-4744]
2022-11-07 02:22:21 -06:00
8f4d62b080 Hide search on Listing page
[MAILPOET-4779]
2022-11-07 02:14:40 -06:00
8003a3e97f Do only trigger when segment is of type default
[MAILPOET-4771]
2022-11-05 07:40:43 +01:00
cf572985a7 Add test to ensure WC checkout only triggers one flow
[MAILPOET-4771]
2022-11-05 07:40:43 +01:00
c4966a9cc1 Prevent flakiness when comparing time in NewsletterTest
When comparing the sentAt and processedAt dates there might be one second difference.
This may happen also in the application, because they are updated by separate queries.

This commit fixes the flakiness by comparing timestamps with delta 1 second instead of comparing date time strings.
[MAILPOET-4710]
2022-11-04 12:14:36 -03:00
c6e6b13731 Run Woo HPOS tests in all branches
This commit reverts 4256a18b82 so that the
Woo HPOS tests are executed in all branches instead of just trunk and
the Woo HPOS related branches.

[MAILPOET-4648]
2022-11-04 14:17:28 +01:00
975041d211 Stop allowing Woo HPOS tests to fail
This commit basically reverts all the changes from
5d8ccb96b4 to stop allowing Woo HPOS
related tests to fail on the CircleCI builds now that all the issues
were fixed and all tests should pass.

[MAILPOET-4648]
2022-11-04 14:17:28 +01:00
ea16472ab1 Use Codeception\TestInterface::getName() instead of Codeception\TestInterface::getMetadata()
This commit changes the code to use $event->getTest()->getName() instead
of $event->getTest()->getMetadata()->getName() to get the name of the
current test. This was necessary because the latter was returning null
for integration tests while the former works both for acceptance and
integration tests. I did not investigate why Codeception behaves this
way due to lack of time, there is a chance that this is a bug in
Codeception.

Since $event->getTest() returns an instance of TestInterface and this
class does not define getName() (but the call works probably due to some
magic method), it was necessary to edit phpstan.neon to skip a PHPStan
error.

[MAILPOET-4765]
2022-11-04 12:43:48 +01:00
713b0255f7 Add WooCommerce Subscriptions tests to $allowedToSkipList list
We skip those tests in some cenarios because WooCommerce Subscriptions
does not support Woo HPOS yet. When the code to skip them was added, we
forgot to add those tests to the list of tests that are allowed to skip
even in the trunk and release branches. This is done now in this commit.

[MAILPOET-4765]
2022-11-04 12:43:48 +01:00
2aa63bbb8e Fix typo
[MAILPOET-4765]
2022-11-04 12:43:48 +01:00
f6708d9526 Change the system that is used to prevent skipped tests
This commit changes the system that is used to prevent skipped tests
when CircleCI executes the integration tests against the trunk
and release branches. After this commit, the integration tests will use
the same system that is used by the acceptance tests. Besides
consolidating everything into a single system, this change is necessary
because we will need to know the name of the tests as we want to allow
some integration tests to be skipped in some circumstances, and this was
not possible with the old system.

[MAILPOET-4765]
2022-11-04 12:43:48 +01:00
65b56271d4 Update Woo HPOS build in test to the latest version
The latest version is 7.1.0-rc.2. Since this build is available on
WP.org, this commit also removes the custom code that we had to download
builds from GitHub. We can now use \RoboFile::downloadWooCommerceZip()
to download builds to test Woo HPOS.

[MAILPOET-4704]
2022-11-04 10:39:59 +01:00
c9c35d591b Update URLs in email templates
[MAILPOET-4758]
2022-11-04 10:51:23 +02:00
e9cfcb51ba Add missing strings
[MAILPOET-4758]
2022-11-04 10:51:23 +02:00
dc5e5b4f8b Use "automation" instead of "workflow" in user-facing strings
[MAILPOET-4758]
2022-11-04 10:51:23 +02:00
ccb5369c57 Use https everywhere for www.mailpoet.com
[MAILPOET-4758]
2022-11-04 10:51:23 +02:00
bfe3ff17af Fix translation strings
[MAILPOET-4758]
2022-11-04 10:51:23 +02:00
756b0587bc Return an empty string for footer when on automation page
[MAILPOET-4755]
2022-11-04 06:26:21 +02:00
af3d08ff36 Add method to detect automation pages
[MAILPOET-4755]
2022-11-04 06:26:21 +02:00
a91913e633 Make 'Give feedback' translateable
[MAILPOET-4755]
2022-11-04 06:26:21 +02:00
736d8a8b12 Add 'Give feedback' beacon ID to automation pages
[MAILPOET-4755]
2022-11-04 06:26:21 +02:00
9b5bdb2206 Release 3.102.1 2022-11-03 22:49:42 +03:00
3ad606a767 Revert "Change the system that is used to prevent skipped tests"
This reverts commit 363ee93c25.
2022-11-03 12:29:48 -03:00
e0cab11293 Revert "Fix typo"
This reverts commit da110bed80.
2022-11-03 12:29:48 -03:00
bffafdcd33 Revert "Add WooCommerce Subscriptions tests to $allowedToSkipList list"
This reverts commit 6b4d2409cc.
2022-11-03 12:29:48 -03:00
6dfeb7a100 Prevent enabling Woo Blocks integration for older versions
Version 8.0.0 was shipped with WooCommerce 6.8.0 which is the oldest currently supported Woo version.
[MAILPOET-4774]
2022-11-03 15:16:43 +01:00
a6431678ed Remove backward compatibility for Woo Blocks ExtendRestApi
The new ExtendSchema was introduced in WCBlocks 7.2 which are way older
then we currently want to support.
[MAILPOET-4774]
2022-11-03 15:16:43 +01:00
cdcdfb135c Update php-stubs/woocommerce-stubs to the latest version
[MAILPOET-4774]
2022-11-03 15:16:43 +01:00
7821c5767b Use current namespace for Woo store API checkout schema class
The new namespace is supported from WooCommerce 6.4.0 so we don't need
to support the old namespace any more.
Some users reported issues with BC class aliases provided by Woo.
[MAILPOET-4774]
2022-11-03 15:16:43 +01:00
6a5b7adc16 Prefix polyfilled normalizer functions
[MAILPOET-4770]
2022-11-03 12:39:24 +01:00
e58de01950 Improve mobile behavior of hero image
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
ae6e790879 Adjust hight of hero picture to align with blue button
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
d21b2fe21d Ensure same width of template buttons
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
6b8e8988b8 Keep template button columns aligned on top
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
0db495eb1d Use DropdownMenu in OptionButton component
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
6ad315b8de Add mailpoet-automation-section-content to define a max-width for section content
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
945ff65358 Use cdn url for automation assets
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
e0458ca444 Sort use statements alphabetically
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
5bd639e0be Add automation listing header
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
c78a050d86 Reduce nesting in CSS
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
e37a824d86 Hide notices on onboarding screen
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
fe4d78992f Let image go below build-your-own list on mobile
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
1c380933e1 Adjust layout of build-your-own list
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
a8c3465133 Add space between options menu and options button
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
edbfb957b1 Ensure there is space between template items and enable wrap
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
161b8d8f34 Reduce right padding of sections by 20px
Those 20px to the border are already in the double usage of the .wrap class in the DOM

[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
96d9347cff Move automation image assets directory
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
dfb7133dbb Use images with a better quality
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
a264b00200 Simplify full-width definition
The values to define fullwidth are derived from #wpcontent and .wrap
The div.wrap exists actually twice in our templates, so those values
needed to be multiplied by 2.

[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
100a301476 Ensure template items are of same height
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
35c3ebfb20 Add Non-breaking space
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
e3db69282d Rephrase to 'Explore essentials'
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
45999f98b2 align template-list items layout
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
6f5c39e97c Add missing period
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
4644f3fe08 Adjust automation test to new UI
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
d951ebd351 Add different sections for welcome screen
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
a7bc4e7c70 Add <OptionButton> component
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
a08553c9bc Add path the image assets
[MAILPOET-4536]
2022-11-03 11:56:21 +01:00
8161b83ded Add check on list type when deleting list
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
ad46d05c6b Add check on list type
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
f231f3dc6d Fix type error for list update
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
933b947f45 Add deleteList documentation
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
f6a5e0117c Add deleteList test cases
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
56c79dd66a Add API method deleteList
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
25ba667cb1 Move segment id validation to private method
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
1ac9c86a66 Add updateList documentation
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
94a2e54c1e Add test cases for updateList
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
fbf4c853c1 Add new API method updateList
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
5fba25d823 Fix the exception message
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
18efc02921 Move list validation to a private method
We move the segment's name validation to a private method to avoid code repetition.
[MAILPOET-4752]
2022-11-02 14:05:39 +01:00
bdd803c517 Support text wrap in forms submit button
MAILPOET-4638
2022-11-02 11:33:44 +01:00
382e232538 Add CSS class form-editor-sidebar-heading back to component
MAILPOET-4650
2022-11-02 11:24:57 +01:00
a2cddc761c Make the labels in form editor consistent
MAILPOET-4650
2022-11-02 11:24:57 +01:00
6b4d2409cc Add WooCommerce Subscriptions tests to $allowedToSkipList list
We skip those tests in some cenarios because WooCommerce Subscriptions
does not support Woo HPOS yet. When the code to skip them was added, we
forgot to add those tests to the list of tests that are allowed to skip
even in the trunk and release branches. This is done now in this commit.

[MAILPOET-4765]
2022-11-02 10:27:34 +01:00
da110bed80 Fix typo
[MAILPOET-4765]
2022-11-02 10:27:34 +01:00
363ee93c25 Change the system that is used to prevent skipped tests
This commit changes the system that is used to prevent skipped tests
when CircleCI executes the integration tests against the trunk
and release branches. After this commit, the integration tests will use
the same system that is used by the acceptance tests. Besides
consolidating everything into a single system, this change is necessary
because we will need to know the name of the tests as we want to allow
some integration tests to be skipped in some circumstances, and this was
not possible with the old system.

[MAILPOET-4765]
2022-11-02 10:27:34 +01:00
db0024dc58 Specify return types for each filter
[MAILPOET-4674]
2022-11-01 15:56:28 +01:00
1b59b95f52 Use controls property of DropdownMenu
[MAILPOET-4674]
2022-11-01 15:56:28 +01:00
2d0673a864 Export @wordpress/icons
[MAILPOET-4674]
2022-11-01 15:56:28 +01:00
9f4b0d0ca1 Add filter so the more menu can be extended by third party
* Removes deprecated mailpoet.automation.workflow.delete_step_callback filter
* Adds mailpoet.automation.workflow.step.more-controls filter

[MAILPOET-4674]
2022-11-01 15:56:28 +01:00
97c1712fa6 Expose MenuItem and ConfirmDialog
[MAILPOET-4674]
2022-11-01 15:56:28 +01:00
455e3fbb90 Fix issue of site-url in none-lowercase failing validation
When validating the MSS/Premium keys sending a none-lowercase
site-url would cause the bridge to correctly validating

[MAILPOET-4754]
2022-11-01 15:00:07 +01:00
3c718e3f68 Update ViewInBrowserRenderer, Fix tracking config error
MAILPOET-4599
2022-11-01 14:59:28 +01:00
63e797ba37 Add more tests for new site shortcodes
MAILPOET-4599
2022-11-01 14:59:28 +01:00
48340872f8 Update acceptance tests for confirmation email
MAILPOET-4599
2022-11-01 14:59:28 +01:00
1b5d85d41a Render the current site title for [site:title] on signup confirmation page description
MAILPOET-4599
2022-11-01 14:59:28 +01:00
0595852d5a Update subscription confirmation email subject
Allow shortcode processing in subject

MAILPOET-4599
2022-11-01 14:59:28 +01:00
448e89d062 Update tests for confirmation emails
MAILPOET-4599
2022-11-01 14:59:28 +01:00
f92ee90e9b Add support for [site:title] and [site:homepage_link]
These will add support for these two shortcodes in the Shortcode engine

MAILPOET-4599
2022-11-01 14:59:28 +01:00
80f22c5b50 Process confirmation email content with newsletter shortcode engine
MAILPOET-4599
2022-11-01 14:59:28 +01:00
4b74d66529 Update subscription confirmation email content description
MAILPOET-4599
2022-11-01 14:59:28 +01:00
ca72e11d9a Update default subscription confirmation email content
MAILPOET-4599
2022-11-01 14:59:28 +01:00
5a0ec403f6 Update test cases to reflect change in method location
MAILPOET-4663
2022-11-01 14:58:47 +01:00
b6e7e39fb0 Rename method and move wcGetPageId to Woo helper class
MAILPOET-4663
2022-11-01 14:58:47 +01:00
8c24d59f43 Add tests for form display on Woo Shop listing page
MAILPOET-4663
2022-11-01 14:58:47 +01:00
5ed0c3fb2d Allow form display on Woo Shop listing page
The WooCommerce Shop page is a special kind of page. It’s basically a post archive.

The Shop page is a placeholder for a post type archive for products. It may render differently than other pages in your install.
https://woocommerce.com/document/woocommerce-pages/#section-4

It doesn’t have any content and WooCommerce core team is also removing the the-content filter from the Shop listing page hence why we are unable to hook into the the-content filter we use for other parts of the codebase

MAILPOET-4663
2022-11-01 14:58:47 +01:00
2151118183 Release 3.102.0 2022-11-01 16:53:44 +03:00
4c65374a0c Add some POT file headers that we've been using
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
32cf2084b8 Unify quotes in strings
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
9dc834e14a Pass translated automation strings to our @wordpress/i18n
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
3db11c443d Use WP-native translation system for marketing optin block
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
421549d6ee Make all automation strings in PHP translatable, improve context/comments
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
a7277f3437 Add context and translators comments where needed
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
4d56911230 Make all automation strings translatable
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
0f23dea7fc Don't add hash of JS bundles to their filenames, use plugin version parameter instead
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
742e3e85e9 Load translations for automation scripts using wp_set_script_translations()
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
6eeb5bb1bf Load all automation scripts using wp_enqueue_script()
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
01eb59a295 Keep only the part of the old makepot logic that is required for HTML and HBS files
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
8ddd42ea4c Generate translations after JS assets are built
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
eb80adc2ed Extract translations from PHP and JS/TS files using WP CLI
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
3103ef11d5 Use old translation extraction logic only for HTML and HBS files
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
a1ead743fa Complete translation directory exclusion list, make it more readable
[MAILPOET-4611]
2022-10-31 10:03:45 +01:00
c8d0291e41 Fix acceptance test flakyness for allowSubscribeInComments 2022-10-28 13:42:09 +01:00
bd44d77973 Fix acceptance test failure for woocommerce setup wizard 2022-10-28 13:42:09 +01:00
f872ab1819 Update expectation for confirmation emails on multisite
MAILPOET-4667
2022-10-28 13:42:09 +01:00
55371ce7ee Reset fetchingPreviewLink and disable button while link is fetched
[MAILPOET-4631]
2022-10-27 10:43:53 -05:00
ba45d23307 Fetch preview link when user clicks on preview
[MAILPOET-4631]
2022-10-27 10:43:53 -05:00
f660bd9bd7 Add Email Preview link
[MAILPOET-4631]
2022-10-27 10:43:53 -05:00
1332fb89e4 Update acceptance tests for confirmation page
MAILPOET-4667
2022-10-27 13:42:50 +02:00
dc5254721e Update the confirmation page title
Show website title instead of list names

MAILPOET-4667
2022-10-27 13:42:50 +02:00
918a4d7c74 Refresh the sending queue entity when fetching it from the old model
By moving the refresh code into the method used for getting sending queue entity for the old model object
we want to make our code better protected from working with inconsistent sending queue data.
[MAILPOET-4750]
2022-10-27 12:08:31 +02:00
ec29c8fb49 Change order of saving sending task related data
In case we save the task and and queue saving fails the sending may be triggered with incorrect data in the queue.
Since the scheduled task is used for controlling sending state lets save it as last so that we are sure all related
data are up to date before saving the task.
[MAILPOET-4750]
2022-10-27 12:08:31 +02:00
43922a7c27 Notify about subscriber changes in WP and Woo synchronizations
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
76b1166269 Add methods for batch changes
Because sometimes can be difficult to get updated and created subscribers, I decided to create methods that doesn't need subscriber ids.
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
12c0605f92 Fix variable name typo
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
0bbf1b96d5 Add notifications about imported subscribers
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
b716b427a7 Fix UTC time for changes
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
c1ac9f7922 Add test case on notifications during shutdown
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
3dd872e211 Add SubscriberListener integration test
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
a09a9cdcbf Add typecast to batch methods
Because functions can return ids as a string, the typecast is necessary to avoid type errors.
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
2d835cdec1 Add new action hooks for multiple changes
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
28b91ed994 Notify about subscriber changes in repository
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
1e147c9dd5 Add notify actions for multiple subscribers
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
6f89b47b30 Store time of change in SubscriberChangesNotifier
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
8d5c07a317 Add notifying about deleted subscribers
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
9ff72b4d4c Add SubscriberListener for notification about changes
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
4fe5219b89 Add notifying about changes to the shutdown hook
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
2dab7fdb0c Add new service for handling subscriber changes
This new service should store all changes and notify about them at the end of the HTTP request.
[MAILPOET-4727]
2022-10-27 09:31:40 +02:00
5075982cd3 Rename validateArray to validateSchemaArray and specify the format in doc comment
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
1a58166a0b Improve error message for the case when no email has been designed yet.
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
ca0681b1a7 Use alert icon from Figma
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
540b34c63f Validate multiple properties
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
c8565b1197 Add validateArray method
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
75906820e5 Reset recent changes in Validator
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
2cacb5809d Avoid iteration
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
900faa2a71 order error cases
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
c140eea734 Adjust DelayActionTest to new error format
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
5201c359d3 Adjust SendEmailActionTest to new error format
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
4e1b1b3c27 Ensure at least one trigger is present and triggers are followed by actions
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
8af01c3bcb Use Rest API error codes and map them to error messages
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
d94dabb8d6 Style error messages and fields
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
63b1f1670b Render error fields and messages in steps
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
2d050b0ea5 Update StepError type to conform with new API response
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
686f702ec9 Add errors per field in Response data
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
5754302154 Add all errors from WordPress to the \ValidationException
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
e2ede3e568 use withError to build error message
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
f95824dea0 Add withError() method to exceptions
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
650a8704cd Show saved message when workflow is saved
[MAILPOET-4700]
2022-10-26 12:51:16 +02:00
cd37f92592 PR feedback: add the default value back
[MAILPOET-4521]
2022-10-26 12:28:22 +02:00
6bf201b45e Remove propTypes and defaultProps from AuthorizeSenderEmailModal
Removing the defaultProps is safe since the defaulted prop
is mandatory by TS

[MAILPOET-4521]
2022-10-26 12:28:22 +02:00
e178d6519c Remove propTypes from StylesSettingsPanel
[MAILPOET-4521]
2022-10-26 12:28:22 +02:00
97d4a8e4f8 Remove propTypes definition for InputStylesSettings
[MAILPOET-4521]
2022-10-26 12:28:22 +02:00
e5ce252587 Remove propTypes from FormFieldTokenField
[MAILPOET-4521]
2022-10-26 12:28:22 +02:00
b7cae6dac4 Remove propTypes & defaultProps from authorize_sender_domain_modal
Safe removal of defaultProps since the defauleted prop is
mandatory by TS and always set by users

[MAILPOET-4521]
2022-10-26 12:28:22 +02:00
07d3b753b8 Remove babel-plugin-typescript-to-proptypes package
[MAILPOET-4521]
2022-10-26 12:28:22 +02:00
72ef0909f5 Stop executing workers when execution limit is reached
When execution limit is reached it makes no sense to continue and
execute additional workers or log the execution limit exception as an error.
[MAILPOET-4725]
2022-10-26 10:13:05 +02:00
a5473bf882 Use proper logger name for logging cron errors
[MAILPOET-4725]
2022-10-26 10:13:05 +02:00
c4e7ea4c8e Use addFilter instead of addAction
[MAILPOET-4728]
2022-10-25 12:04:44 -05:00
ce462f1643 Close correct HTML element type
[MAILPOET-4728]
2022-10-25 12:04:44 -05:00
91936aff92 Rename Automation to Automations in Menu
[MAILPOET-4728]
2022-10-25 12:04:44 -05:00
57858bbe9a Move Automation menu item between Emails and Forms
[MAILPOET-4728]
2022-10-25 12:04:44 -05:00
42d9b1241e Make menu strings translateable and use esc_html__() for menu titles
[MAILPOET-4728]
2022-10-25 12:04:44 -05:00
fc4a5315df Add beta badge
[MAILPOET-4728]
2022-10-25 12:04:44 -05:00
e771bd53da Release 3.101.1 2022-10-25 15:42:37 +02:00
291f46d732 Fix spacing in docs 2022-10-25 13:13:47 +01:00
e236d3312c Fix listId typo 2022-10-25 13:15:28 +02:00
794090291a Update getSubscribers filter arguments
Filter arguments 'list_id' and 'min_updated_at' do not work. These arguments currently only work with lowerCamelCase.
2022-10-25 13:15:28 +02:00
380eec1ed6 Add prop types for deactivation modals
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
7c283b7fda Use " for quotation marks
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
916080fa5a Reset step runners to not interfer with other tests
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
c10f80457d Use new getCountForWorkflow for performance
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
9773408c8b Add getCountForWorkflow method to return how many runs exist for specific status
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
5ba5426281 truncate entries of WorkflowRunLogStorage
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
098aebfaf9 Add truncate() method to WorkflowRunLogStorage
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
75d79f19cc Add information that deleting a workflow deletes also the workflow runs
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
1b70e6d494 Ensure workflows are active
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
2c57251bae Revert e8cfb2565
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
9bdb32c073 Verify deactivating workflow gets inactive after last workflow run
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
b74890137a Set a deactivating Workflow to inactive once all runs are completed
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
57548b579f Add method to fetch all runs of a workflow
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
7aa1a5f4ba Enable setStatus to handle multiple WorkflowRuns at the same time
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
d60e399a3b Alter buttons depending on workflow state, use DeactivateImmediatelyModal
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
da34614b5c Add new DeactivateImmediatelyModal
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
a0d330fded Deactivate workflow on button click depending on selected status
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
f766661f8c Show Deactivating state in header
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
82ddce42df Use WorkflowStatus enum for valid status values in Workflow type
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
bfe95676c3 Add switch to decide which deactivation state we want to set the workflow to
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
789bb0b396 Use workflow status constants
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
2620ef0b57 Do not handle workflow runs when workflow status is not active/deactivating
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
627962570e Add new workflow status 'deactivating'
[MAILPOET-4731]
2022-10-25 12:46:07 +02:00
548 changed files with 11502 additions and 9371 deletions

View File

@ -71,13 +71,6 @@ anchors:
- trunk
- release
only_trunk_and_cot: &only_trunk_and_cot
filters:
branches:
only:
- trunk
- /^cot-.*/
multisite_acceptance_config: &multisite_acceptance_config
multisite: 1
requires:
@ -186,10 +179,10 @@ jobs:
- run:
name: Download additional WP Plugins for tests
command: |
./do download:woo-commerce-zip 6.8.2
./do download:woo-commerce-subscriptions-zip 4.5.1
./do download:woo-commerce-memberships-zip 1.23.0
./do download:woo-commerce-blocks-zip 8.4.0
./do download:woo-commerce-zip 7.1.0
./do download:woo-commerce-subscriptions-zip 4.6.0
./do download:woo-commerce-memberships-zip 1.23.1
./do download:woo-commerce-blocks-zip 8.8.2
- run:
name: Dump tests ENV variables for acceptance tests
command: |
@ -318,7 +311,7 @@ jobs:
parallelism: 20
working_directory: /home/circleci/mailpoet/mailpoet
machine:
image: ubuntu-2204:2022.07.1
image: ubuntu-2204:2022.10.2
parameters:
multisite:
type: integer
@ -356,9 +349,6 @@ jobs:
enable_cot_sync:
type: integer
default: 0
allow_fail:
type: integer
default: 0
environment:
MYSQL_COMMAND: << parameters.mysql_command >>
MYSQL_IMAGE_VERSION: << parameters.mysql_image_version >>
@ -434,9 +424,6 @@ jobs:
--xml
-g circleci_split_group
)
if [[ << parameters.allow_fail >> == 1 ]]; then
args+=(--no-exit)
fi
docker-compose run -e SKIP_DEPS=1 \
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
-e CIRCLE_JOB=${CIRCLE_JOB} \
@ -444,18 +431,13 @@ jobs:
-e ENABLE_COT=<< parameters.enable_cot >> \
-e ENABLE_COT_SYNC=<< parameters.enable_cot_sync >> \
codeception_acceptance "${args[@]}"
- when:
condition:
not:
equal: [1, << parameters.allow_fail >>]
steps:
- run:
name: Check exceptions
command: |
if [ "$(ls tests/_output/exceptions/*.html)" ]; then
echo "There were some exceptions during the tests run"
exit 1
fi
- run:
name: Check exceptions
command: |
if [ "$(ls tests/_output/exceptions/*.html)" ]; then
echo "There were some exceptions during the tests run"
exit 1
fi
- store_artifacts:
path: tests/_output
- store_test_results:
@ -494,7 +476,7 @@ jobs:
integration_tests:
working_directory: /home/circleci/mailpoet/mailpoet
machine:
image: ubuntu-2204:2022.07.1
image: ubuntu-2204:2022.10.2
environment:
CODECEPTION_IMAGE_VERSION: << parameters.codeception_image_version >>
parameters:
@ -519,12 +501,6 @@ jobs:
multisite:
type: integer
default: 0
woo_core_version:
type: string
default: ''
allow_fail:
type: integer
default: 0
steps:
- attach_workspace:
at: /home/circleci
@ -532,14 +508,6 @@ jobs:
name: 'Pull test docker images'
# Pull docker images with 3 retries
command: i='0';while ! docker-compose -f tests/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
- when:
condition: << parameters.woo_core_version >>
steps:
- run:
name: Download WooCommerce Core
command: |
cd tests/docker
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip << parameters.woo_core_version >>" --no-deps codeception_integration
- run:
name: 'PHP Integration tests'
command: |
@ -558,9 +526,6 @@ jobs:
if [[ -n '<< parameters.skip_group >>' ]]; then
args+=(--skip-group << parameters.skip_group >>)
fi
if [[ << parameters.allow_fail >> == 1 ]]; then
args+=(--no-exit)
fi
docker-compose run -e SKIP_DEPS=1 \
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
-e CIRCLE_JOB=${CIRCLE_JOB} \
@ -661,13 +626,10 @@ workflows:
- qa_php
- acceptance_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
name: acceptance_tests_woo_cot_sync
group: woo
enable_cot: 1
enable_cot_sync: 1
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
@ -675,13 +637,10 @@ workflows:
- qa_php
- acceptance_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
name: acceptance_tests_woo_cot_no_sync
group: woo
enable_cot: 1
enable_cot_sync: 0
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
@ -689,10 +648,8 @@ workflows:
- qa_php
- acceptance_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
name: acceptance_tests_woo_cot_off
group: woo
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
@ -713,12 +670,9 @@ workflows:
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
enable_cot: 1
enable_cot_sync: 1
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_sync
requires:
- unit_tests
@ -727,12 +681,9 @@ workflows:
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
enable_cot: 1
enable_cot_sync: 0
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_no_sync
requires:
- unit_tests
@ -741,9 +692,7 @@ workflows:
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_off
requires:
- unit_tests
@ -783,6 +732,12 @@ workflows:
- js_tests
- integration_test_woocommerce
- integration_test_base
- integration_test_woo_cot_no_sync
- integration_test_woo_cot_off
- integration_test_woo_cot_sync
- acceptance_tests_woo_cot_sync
- acceptance_tests_woo_cot_off
- acceptance_tests_woo_cot_no_sync
nightly:
triggers:
@ -807,14 +762,14 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_oldest
woo_core_version: 6.2.2
woo_core_version: 6.8.0
woo_subscriptions_version: 4.3.0
woo_memberships_version: 1.21.0
woo_blocks_version: 5.3.2
woo_blocks_version: 6.8.0
mysql_command: --max_allowed_packet=100M
mysql_image_version: 5.7.36
codeception_image_version: 7.4-cli_20210126.1
wordpress_image_version: wp-5.6_php7.2_20220406.1
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: wp-5.8_php7.3_20221104.1
requires:
- build
- unit_tests:

View File

@ -28,6 +28,7 @@ Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by Wo
- [Add List (addList)](api_methods/AddList.md)
- [Add Subscriber (addSubscriber)](api_methods/AddSubscriber.md)
- [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md)
- [Delete List (deleteList)](api_methods/DeleteList.md)
- [Get Lists (getLists)](api_methods/GetLists.md)
- [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md)
- [Get Subscribers (getSubscribers)](api_methods/GetSubscribers.md)
@ -38,6 +39,7 @@ Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by Wo
- [Subscribe to Lists (subscribeToLists)](api_methods/SubscribeToLists.md)
- [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md)
- [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md)
- [Update List (updateList)](api_methods/UpdateList.md)
### Usage examples

View File

@ -0,0 +1,27 @@
[back to list](../Readme.md)
# Delete List
## `bool deleteList(string $list_id)`
This method provides functionality for deleting a list that is of the type 'default'.
It returns a boolean value.
## Error handling
All expected errors from the API are exceptions of class `\MailPoet\API\MP\v1\APIException`.
Code of the exception is populated to distinguish between different errors.
An exception of base class `\Exception` can be thrown when something unexpected happens.
Codes description:
| Code | Description |
| ---- | --------------------------------------------------------------- |
| 5 | List does not exist |
| 18 | List id is empty |
| 20 | List cannot be deleted because its used for an automatic email |
| 21 | List cannot be deleted because its used for a form |
| 22 | The list couldnt be deleted from the database |
| 23 | Only lists of the type 'default' can be deleted |

View File

@ -18,8 +18,8 @@ This method returns a list of subscribers. To see the subscriber data structure,
Filter argument supports following array keys.
| Key | Type | Description |
| -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------- |
| status | string | Specific status of subscribers. One of values: `unconfirmed`, `subscribed`, `unsubscribed`, `bounced`, `inactive` |
| list_id | int | List id or dynamic segment id |
| min_updated_at | DateTime\int | DateTime object or timestamp of the minimal last update of subscribers |
| Key | Type | Description |
| ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------- |
| status | string | Specific status of subscribers. One of values: `unconfirmed`, `subscribed`, `unsubscribed`, `bounced`, `inactive` |
| listId | int | List id or dynamic segment id |
| minUpdatedAt | DateTime\int | DateTime object or timestamp of the minimal last update of subscribers |

View File

@ -0,0 +1,39 @@
[back to list](../Readme.md)
# Add Subscriber
## `array updateList(array $list)`
This method provides functionality for updating a list name or description. Only lists of type 'default' are supported.
It returns the updated list. See [Get Lists](GetLists.md) for a list data structure description.
## Arguments
### `$list` (required)
An associative array which contains list data.
| Property | Type | Limits | Description |
| ---------------------- | ------------ | --------- | -------------------------- |
| id (required) | string | 11 chars | A id of the list. |
| name (required) | string | 90 chars | A name of the list. |
| description (optional) | string\|null | 250 chars | A description of the list. |
## Error handling
All expected errors from the API are exceptions of class `\MailPoet\API\MP\v1\APIException`.
Code of the exception is populated to distinguish between different errors.
An exception of base class `\Exception` can be thrown when something unexpected happens.
Codes description:
| Code | Description |
| ---- | ----------------------------------------------- |
| 5 | The list was not found by id |
| 14 | Missing list name |
| 15 | Trying to use a list name that is already used |
| 18 | Missing list id |
| 19 | The list couldnt be updated in the database |
| 23 | Only lists of the type 'default' can be updated |

View File

@ -5,7 +5,6 @@
"@babel/preset-env"
],
"plugins": [
"babel-plugin-typescript-to-proptypes",
[
"@babel/plugin-transform-runtime",
{

View File

@ -117,11 +117,45 @@ class RoboFile extends \Robo\Tasks {
}
public function translationsBuild() {
$exclude = implode(',', [
'.mp_svn',
'assets/css',
'assets/img',
'assets/js',
'generated',
'lang',
'lib-3rd-party',
'mailpoet-premium',
'node_modules',
'plugin_repository',
'prefixer',
'tasks',
'temp',
'tests',
'tools',
'vendor',
'vendor-prefixed',
]);
$headers = escapeshellarg(
json_encode([
'Report-Msgid-Bugs-To' => 'http://support.mailpoet.com/',
'Last-Translator' => 'MailPoet i18n (https://www.transifex.com/organization/wysija)',
'Language-Team' => 'MailPoet i18n <https://www.transifex.com/organization/wysija>',
'Plural-Forms' => 'nplurals=2; plural=(n != 1);',
])
);
$this->collectionBuilder()
->taskExec('mkdir -p ' . __DIR__ . '/lang')
->taskExec(
'php -d memory_limit=-1 tasks/makepot/grunt-makepot.php wp-plugin . lang/mailpoet.pot mailpoet .mp_svn,assets,lang,node_modules,plugin_repository,tasks,tests,vendor'
)->run();
// HTML, HBS
->taskExec("php -d memory_limit=-1 tasks/makepot/makepot-views.php . > lang/mailpoet.pot")
// PHP, JS/TS
->taskExec("vendor/bin/wp i18n make-pot --merge --slug=mailpoet --domain=mailpoet --exclude=$exclude --headers=$headers . lang/mailpoet.pot")
->run();
}
public function translationsGetPotFileFromBuild() {
@ -351,6 +385,25 @@ class RoboFile extends \Robo\Tasks {
$this->say("Validator metadata generated to: $validatorMetadataDir");
}
public function migrationsNew() {
$generator = new \MailPoet\Migrator\Repository();
$result = $generator->create();
$path = realpath($result['path']);
$this->output->writeln('MAILPOET DATABASE MIGRATIONS');
$this->output->writeln("============================\n");
$this->output->writeln("New migration created ✔\n");
$this->output->writeln(" Name: {$result['name']}");
$this->output->writeln(" Path: $path");
}
public function migrationsStatus() {
return $this->taskExec('vendor/bin/wp mailpoet:migrations:status');
}
public function migrationsRun() {
return $this->taskExec('vendor/bin/wp mailpoet:migrations:run');
}
public function qa() {
$collection = $this->collectionBuilder();
$collection->addCode([$this, 'qaPhp']);
@ -746,6 +799,9 @@ class RoboFile extends \Robo\Tasks {
->addCode(function () use ($version) {
$this->releaseCreatePullRequest($version);
})
->addCode(function () use ($version) {
$this->releaseRerunCircleWorkflow(\MailPoetTasks\Release\CircleCiController::PROJECT_PREMIUM);
})
->addCode(function () use ($version) {
$this->translationsPrepareLanguagePacks($version);
})
@ -1063,6 +1119,18 @@ class RoboFile extends \Robo\Tasks {
$this->say("Release '$version[name]' info was published on Slack.");
}
public function releaseRerunCircleWorkflow(string $project = null) {
$circleciController = $this->createCircleCiController();
$result = $circleciController->rerunLatestWorkflow($project);
// Sometimes can be useful to know which Circle project workflow was restarted
$project = $project ? " for the project '{$project}'" : '';
if (!$result) {
$this->yell("Circle Workflow{$project} was not restarted", 40, 'red');
} else {
$this->say("Circle Workflow{$project} was started from the beginning");
}
}
public function downloadWooCommerceBlocksZip($tag = null) {
$this->createWpOrgDownloader('woo-gutenberg-products-block')
->downloadPluginZip('woo-gutenberg-products-block.zip', __DIR__ . '/tests/plugins/', $tag);
@ -1087,20 +1155,10 @@ class RoboFile extends \Robo\Tasks {
}
public function downloadWooCommerceZip($tag = null) {
if ($tag === 'woo-cot-beta') {
$this->downloadWooCommerceCotZip();
return;
}
$this->createWpOrgDownloader('woocommerce')
->downloadPluginZip('woocommerce.zip', __DIR__ . '/tests/plugins/', $tag);
}
public function downloadWooCommerceCotZip() {
$cotBuildUrl = 'https://github.com/woocommerce/woocommerce/files/9706609/woocommerce.zip';
file_put_contents(__DIR__ . '/tests/plugins/woocommerce.zip', file_get_contents($cotBuildUrl));
file_put_contents(__DIR__ . '/tests/plugins/woocommerce.zip-info', $cotBuildUrl);
}
public function generateData($generatorName = null, $threads = 1) {
require_once __DIR__ . '/tests/DataGenerator/_bootstrap.php';
$generator = new \MailPoet\Test\DataGenerator\DataGenerator(new \Codeception\Lib\Console\Output([]));

View File

@ -1,4 +1,4 @@
.mailpoet-automation-workflow-add-trigger {
.mailpoet-automation-add-trigger {
align-items: center;
border: 1px dashed #c3c4c7;
border-radius: 4px;

View File

@ -1,14 +1,14 @@
.mailpoet-automation-editor-workflow {
.mailpoet-automation-editor-automation {
background: #fbfbfb;
flex-grow: 1;
}
.mailpoet-automation-editor-workflow-wrapper {
.mailpoet-automation-editor-automation-wrapper {
display: grid;
padding: 50px 20px;
}
.mailpoet-automation-editor-workflow-end {
.mailpoet-automation-editor-automation-end {
background: #8c8f94;
border-radius: 999999px;
fill: white;

View File

@ -1,4 +1,4 @@
.mailpoet-automation-editor-empty-workflow {
.mailpoet-automation-editor-empty-automation {
align-items: center;
display: grid;
height: 100%;

View File

@ -30,3 +30,31 @@
outline: none;
}
}
.mailpoet-automation-field__error {
position: relative;
input,
select,
textarea {
background: right top/26px no-repeat url('../../img/icons/alert.svg');
padding-right: 26px;
}
select,
input[type=number] {
background-position-x: calc(100% - 26px);
padding-right: 8px !important;
}
.components-base-control__help,
.mailpoet-automation-field-message {
color: #d63638;
}
.components-button.mailpoet-automation-button-sidebar-primary,
.components-button.mailpoet-automation-button-sidebar-primary.has-text,
.components-button.mailpoet-automation-button-sidebar-primary.has-icon {
background: #d63638;
}
}

View File

@ -6,6 +6,12 @@
}
}
.mailpoet-automation-is-onboarding {
.notice {
display: none;
}
}
.mailpoet-automation-listing-heading {
margin-bottom: 16px;
}
@ -15,6 +21,21 @@
margin-bottom: 0;
}
.mailpoet-automation-listing-cell-name {
position: relative;
width: 100%;
> a:only-child {
bottom: 2px;
display: flex;
left: 0;
padding: 16px 24px;
position: absolute;
right: 0;
top: 0;
}
}
.mailpoet-filter-tab-panel {
background-color: #fff;
border: 1px solid #dcdcde;

View File

@ -0,0 +1,201 @@
@mixin full-width {
margin-left: -20px;
padding-left: 104px;
padding-right: 104px;
width: calc(100% + 60px);
@media screen and (max-width: 782px) {
margin-left: -10px;
width: calc(100% + 34px);
}
}
.mailpoet-automation-section {
@include full-width;
}
.mailpoet-automation-white-background {
background: #fff;
}
.mailpoet-automation-section-content {
display: block;
margin: auto;
max-width: 1072px;
padding: 65px 0;
h2 {
font-size: 23px;
font-weight: 400;
line-height: 32px;
margin: 0;
padding: 0 0 8px;
}
p {
font-size: 14px;
font-weight: 400;
line-height: 22px;
margin: 0;
padding: 0 0 40px;
}
}
.mailpoet-automation-section-hero {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: -20px;
h1 {
font-size: 32px;
font-weight: 400;
line-height: 40px;
}
p {
font-size: 14px;
line-height: 22px;
margin-bottom: 32px;
}
> div {
width: 400px;
}
img {
margin-top: 16px;
max-width: 100%;
width: 532px;
@media screen and (min-width: 1305px) {
height: 100%;
margin-top: 0;
max-height: 294px;
width: auto;
}
}
}
.mailpoet-automation-preheading {
display: block;
font-size: 11px;
letter-spacing: .2px;
line-height: 16px;
margin-bottom: 32px;
text-transform: uppercase;
}
.mailpoet-section-templates {
padding: 48px 0;
.components-button {
display: block;
font-size: 16px;
font-weight: 400;
line-height: 25px;
text-align: center;
text-underline-offset: 5px;
}
}
.mailpoet-section-template-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 40px;
> li {
flex-grow: 1;
margin-right: 8px;
max-width: 336px;
&:last-child {
margin-right: 0;
}
button {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 0;
color: #1d2327;
cursor: pointer;
padding: 24px;
text-align: left;
h3 {
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
}
}
}
.mailpoet-section-build-list-button {
background: transparent;
border: 0;
color: #000;
cursor: pointer;
font-size: 16px;
font-weight: 400;
line-height: 24px;
padding: 0;
text-align: left;
width: 100%;
}
.mailpoet-section-build-your-own {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
ol {
list-style: decimal-leading-zero inside;
margin: 0;
max-width: 373px;
padding: 0;
> li {
border-bottom: 1px solid #dcdcde;
display: grid;
grid-gap: 16px;
grid-template-columns: 16px auto;
margin-bottom: 16px;
padding-bottom: 16px;
&.open {
p {
display: block;
}
.mailpoet-section-build-list-button {
font-weight: 600;
}
}
&:last-of-type {
border: 0;
}
}
.marker {
color: #ff5301;
display: inline-block;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
p {
display: none;
padding: 0;
}
}
img {
height: auto;
max-width: 400px;
width: 100%;
}
}

View File

@ -0,0 +1,25 @@
.mailpoet-option-button {
display: flex;
margin-top: 8px;
position: relative;
}
.mailpoet-option-button-main {
border-radius: 2px 0 0 2px;
margin-right: 1px;
}
.mailpoet-option-button-opener {
background: var(--wp-admin-theme-color);
border-radius: 0 2px 2px 0;
color: white;
}
.mailpoet-option-button-opener svg {
fill: white;
}
.mailpoet-option-button-opener .is-opened svg {
transform: scale(-1, -1);
transform-origin: center 12.5px;
}

View File

@ -119,6 +119,10 @@
color: $color-stats-average;
}
.mailpoet-statistics-value-number-critical {
color: $color-stats-critical;
}
.mailpoet-statistics-value-number-excellent {
color: $color-stats-excellent;
}

View File

@ -63,6 +63,11 @@ $form-line-height: 1.4;
.mailpoet-has-font-size {
line-height: $form-line-height;
}
.mailpoet_submit {
white-space: normal;
word-wrap: break-word;
}
}
/* Reset fieldset styles in form for backward compatibility. */

View File

@ -41,6 +41,15 @@
}
}
.mailpoet-tag-critical {
border-color: $color-stats-critical;
color: $color-stats-critical;
&.mailpoet-tag-inverted {
background: $color-stats-critical;
}
}
.mailpoet-tag-good {
border-color: $color-stats-good;
color: $color-stats-good;

View File

@ -11,17 +11,17 @@
@import './components-automation-editor/add-step-button';
@import './components-automation-editor/add-trigger';
@import './components-automation-editor/automation';
@import './components-automation-editor/block-icon';
@import './components-automation-editor/chip';
@import './components-automation-editor/dropdown';
@import './components-automation-editor/empty-workflow';
@import './components-automation-editor/empty-automation';
@import './components-automation-editor/errors';
@import './components-automation-editor/panel';
@import './components-automation-editor/separator';
@import './components-automation-editor/status';
@import './components-automation-editor/step';
@import './components-automation-editor/step-card';
@import './components-automation-editor/workflow';
@import './components-automation-editor/notices';
@import './components-automation-editor/deactivate-modal';

View File

@ -13,18 +13,34 @@ ul.mailpoet-automation-templates {
margin: auto;
max-width: 982px;
padding: 48px 0;
}
.mailpoet-automation-template-list-item {
button.components-button {
align-content: baseline;
align-items: flex-start;
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
cursor: pointer;
display: block;
display: grid;
grid-template-rows: 40px auto auto;
height: 100%;
padding: 24px 24px 26px;
text-align: left;
width: 100%;
&:disabled,
&[aria-disabled='true'] {
color: #787c82;
cursor: not-allowed;
opacity: 1;
h2 {
color: #787c82;
}
}
&:hover {
background: #fff;
border: 1px solid #dcdcde;
@ -37,12 +53,15 @@ ul.mailpoet-automation-templates {
box-shadow: 0 3px 6px rgba(0, 0, 0, .15);
color: inherit;
}
>* {
width: 100%
}
}
h2 {
background: transparent;
border: none;
color: #2271b1;
font-size: 14px;
font-weight: 600;
line-height: 21px;
@ -54,7 +73,7 @@ ul.mailpoet-automation-templates {
margin: 8px 0 0;
}
.mailpoet-automation-from-scratch {
&.mailpoet-automation-from-scratch {
button {
align-content: center;
border: 2px dashed #dcdcde;
@ -71,4 +90,27 @@ ul.mailpoet-automation-templates {
fill: #dcdcde;
}
}
.badge {
text-align: right;
transform: translateX(24px);
span {
padding: 3px 8px;
}
}
}
.mailpoet-automation-template-list-item-coming-soon {
.badge span {
background: #ffe9cc;
color: #1d2327;
}
}
.mailpoet-automation-template-list-item-premium {
.badge span {
background: #ff5301;
color: #fff;
}
}

View File

@ -4,11 +4,14 @@
// automation components
@import './components-automation/statistics';
@import './components-automation/option-button';
// automation listing
@import './components-automation-listing/sections';
@import './components-automation-listing/listing';
@import './components-automation-listing/header';
@import './components-automation-listing/search';
@import './components-automation-listing/cells/actions';
@import './components-automation-listing/cells/status';
@import './mailpoet-automation-templates';

View File

@ -69,6 +69,7 @@ $color-badge-video-guide: #46b450;
$color-stats-average: #f559c3;
$color-stats-good: #ff9f00;
$color-stats-excellent: #7ed321;
$color-stats-critical: #f00;
$color-stats-unknown: $color-primary-inactive;
// Automation editor

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.75C7.99594 4.75 4.75 7.99594 4.75 12C4.75 16.0041 7.99594 19.25 12 19.25C16.0041 19.25 19.25 16.0041 19.25 12C19.25 7.99594 16.0041 4.75 12 4.75ZM3.25 12C3.25 7.16751 7.16751 3.25 12 3.25C16.8325 3.25 20.75 7.16751 20.75 12C20.75 16.8325 16.8325 20.75 12 20.75C7.16751 20.75 3.25 16.8325 3.25 12Z" fill="#d63638"/>
<path d="M13 7H11V13H13V7Z" fill="#d63638"/>
<path d="M13 15H11V17H13V15Z" fill="#d63638"/>
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@ -1,81 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '../config';
const API_URL = `${api.root}/mailpoet/v1/automation`;
export const request = (
path: string,
init?: RequestInit,
): ReturnType<typeof fetch> => fetch(`${API_URL}/${path}`, init);
type Error<T> = {
response?: Response;
data?: T;
};
type State<T> = {
data?: T;
loading: boolean;
error?: Error<T>;
};
type Result<T> = [(init?: RequestInit) => Promise<void>, State<T>];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Data = Record<string, any>;
export const useMutation = <T extends Data>(
path: string,
config?: RequestInit,
): Result<T> => {
const [state, setState] = useState<State<T>>({
data: undefined,
loading: false,
error: undefined,
});
const mutation = useCallback(
async (init?: RequestInit) => {
setState((prevState) => ({ ...prevState, loading: true }));
const response = await request(path, {
...config,
...init,
headers: {
'content-type': 'application/json',
...(init?.headers ?? {}),
'x-wp-nonce': api.nonce,
},
});
try {
const data = await response.json();
const error = response.ok ? null : { ...response, data };
setState((prevState) => ({ ...prevState, data, error }));
} catch (_) {
const error = { response };
setState((prevState) => ({ ...prevState, error }));
} finally {
setState((prevState) => ({ ...prevState, loading: false }));
}
},
[config, path],
);
return [mutation, state];
};
export const useQuery = <T extends Data>(
path: string,
init?: RequestInit,
): State<T> => {
const [mutation, result] = useMutation<T>(path, init);
useEffect(
() => {
void mutation();
},
[] /* eslint-disable-line react-hooks/exhaustive-deps -- request only on initial load */,
);
return result;
};

View File

@ -1,9 +1,7 @@
import apiFetch from '@wordpress/api-fetch';
import { api } from '../config';
export * from './hooks';
const apiUrl = `${api.root}/mailpoet/v1/automation/`;
const apiUrl = `${api.root}/mailpoet/v1/`;
export type ApiError = {
code?: string;

View File

@ -1,94 +1,70 @@
import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { TopBarWithBeamer } from 'common/top_bar/top_bar';
import { plusIcon } from 'common/button/icon/plus';
import { Button, Flex, Popover, SlotFillProvider } from '@wordpress/components';
import { Popover, SlotFillProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { initializeApi, useMutation } from './api';
import { initializeApi } from './api';
import { registerTranslations } from './i18n';
import { createStore, storeName } from './listing/store';
import { AutomationListing } from './listing';
import { AutomationListing, AutomationListingHeader } from './listing';
import { registerApiErrorHandler } from './listing/api-error-handler';
import { Notices } from './listing/components/notices';
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
import { Onboarding } from './onboarding';
import {
CreateEmptyWorkflowButton,
CreateWorkflowFromTemplateButton,
} from './testing';
import { BuildYourOwnSection, HeroSection, TemplatesSection } from './sections';
import { MailPoet } from '../mailpoet';
function Content(): JSX.Element {
const count = useSelect((select) => select(storeName).getWorkflowCount());
return count > 0 ? <AutomationListing /> : <Onboarding />;
}
const trackOpenEvent = () => {
MailPoet.trackEvent('Automations > Listing viewed');
};
function Workflows(): JSX.Element {
function Content(): JSX.Element {
const [isBooting, setIsBooting] = useState(true);
const count = useSelect((select) => select(storeName).getAutomationCount());
useEffect(() => {
if (!isBooting || count === 0) {
return;
}
trackOpenEvent();
setIsBooting(false);
}, [isBooting, count]);
const content =
count > 0 ? (
<>
<AutomationListingHeader />
<AutomationListing />
</>
) : (
<HeroSection />
);
// Hide notices on onboarding screen
useEffect(() => {
const onboardingClass = 'mailpoet-automation-is-onboarding';
const element = document.querySelector('body');
if (count === 0 && !element.classList.contains(onboardingClass)) {
element.classList.add(onboardingClass);
}
if (count > 0 && element.classList.contains(onboardingClass)) {
element.classList.remove(onboardingClass);
}
}, [count]);
return (
<>
<TopBarWithBeamer />
<Flex className="mailpoet-automation-listing-heading">
<h1 className="wp-heading-inline">Automations</h1>
<Button
href={MailPoet.urls.automationTemplates}
icon={plusIcon}
variant="primary"
className="mailpoet-add-new-button"
>
New automation
</Button>
</Flex>
<Notices />
<Content />
{content}
<TemplatesSection />
<BuildYourOwnSection />
</>
);
}
function RecreateSchemaButton(): JSX.Element {
const [createSchema, { loading, error }] = useMutation('system/database', {
method: 'POST',
});
function Automations(): JSX.Element {
return (
<div>
<WorkflowListingNotices />
<button
className="button button-link-delete"
type="button"
onClick={() => createSchema()}
disabled={loading}
>
Recreate DB schema (data will be lost)
</button>
{error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div>
);
}
function DeleteSchemaButton(): JSX.Element {
const [deleteSchema, { loading, error }] = useMutation('system/database', {
method: 'DELETE',
});
return (
<div>
<button
className="button button-link-delete"
type="button"
onClick={async () => {
await deleteSchema();
window.location.href =
'/wp-admin/admin.php?page=mailpoet-experimental';
}}
disabled={loading}
>
Delete DB schema & deactivate feature
</button>
{error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div>
<>
<TopBarWithBeamer />
<Notices />
<Content />
</>
);
}
@ -96,26 +72,8 @@ function App(): JSX.Element {
return (
<SlotFillProvider>
<BrowserRouter>
<div>
<Workflows />
<div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}>
<CreateEmptyWorkflowButton />
<CreateWorkflowFromTemplateButton slug="simple-welcome-email">
Create testing workflow from template (welcome email)
</CreateWorkflowFromTemplateButton>
<CreateWorkflowFromTemplateButton slug="welcome-email-sequence">
Create testing workflow from template (welcome sequence, only
premium)
</CreateWorkflowFromTemplateButton>
<CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence">
Create testing workflow from template (advanced welcome sequence,
only premium)
</CreateWorkflowFromTemplateButton>
<RecreateSchemaButton />
<DeleteSchemaButton />
</div>
<Popover.Slot />
</div>
<Automations />
<Popover.Slot />
</BrowserRouter>
</SlotFillProvider>
);
@ -126,6 +84,7 @@ window.addEventListener('DOMContentLoaded', () => {
const root = document.getElementById('mailpoet_automation');
if (root) {
registerTranslations();
registerApiErrorHandler();
initializeApi();
ReactDOM.render(<App />, root);

View File

@ -0,0 +1,44 @@
import { Button, DropdownMenu } from '@wordpress/components';
import { chevronDown } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { Fragment } from '@wordpress/element';
import { StepMoreControlsType } from '../../types/filters';
type OptionButtonPropType = {
variant: Button.ButtonVariant;
controls: StepMoreControlsType;
title: string;
onClick: () => void;
};
export function OptionButton({
controls,
title,
onClick,
variant,
}: OptionButtonPropType): JSX.Element {
const slots = Object.values(controls).filter((item) => item.slot);
return (
<div className="mailpoet-option-button">
<Button
variant={variant}
className="mailpoet-option-button-main"
onClick={onClick}
>
{title}
</Button>
{slots.length > 0 &&
slots.map(({ key, slot }) => (
<Fragment key={`slot-${key}`}>{slot}</Fragment>
))}
{Object.values(controls).length > 0 && (
<DropdownMenu
className="mailpoet-option-button-opener"
label={__('More', 'mailpoet')}
icon={chevronDown}
controls={Object.values(controls).map((item) => item.control)}
popoverProps={{ position: 'bottom left' }}
/>
)}
</div>
);
}

View File

@ -4,9 +4,9 @@ declare global {
root: string;
nonce: string;
};
mailpoet_workflow_count: number;
mailpoet_automation_count: number;
}
}
export const api = window.mailpoet_automation_api;
export const workflowCount = window.mailpoet_workflow_count;
export const automationCount = window.mailpoet_automation_count;

View File

@ -19,7 +19,7 @@ export const registerApiErrorHandler = (): void =>
const status = errorObject.data?.status;
const code = errorObject.code;
if (code === 'mailpoet_automation_workflow_not_valid') {
if (code === 'mailpoet_automation_not_valid') {
dispatch(storeName).setErrors({ steps: errorObject.data.errors });
return undefined;
}
@ -30,6 +30,7 @@ export const registerApiErrorHandler = (): void =>
message ?? __('An unknown error occurred.', 'mailpoet'),
{ explicitDismiss: true },
);
dispatch(storeName).setErrors({ steps: [] });
return undefined;
}

View File

@ -4,13 +4,14 @@ import {
Button,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { storeName } from '../../store';
export function TrashButton(): JSX.Element {
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { workflow } = useSelect(
const { automation } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
automation: select(storeName).getAutomationData(),
}),
[],
);
@ -20,8 +21,8 @@ export function TrashButton(): JSX.Element {
<>
<ConfirmDialog
isOpen={showConfirmDialog}
title="Delete workflow"
confirmButtonText="Yes, delete"
title={__('Delete automation', 'mailpoet')}
confirmButtonText={__('Yes, delete', 'mailpoet')}
onConfirm={async () => {
trash(() => {
setShowConfirmDialog(false);
@ -30,7 +31,12 @@ export function TrashButton(): JSX.Element {
onCancel={() => setShowConfirmDialog(false)}
__experimentalHideHeader={false}
>
You are about to delete the {workflow.name} workflow.
{sprintf(
__('You are about to delete the automation "%s".', 'mailpoet'),
automation.name,
)}
<br />
{__(' This will stop it for all subscribers immediately.', 'mailpoet')}
</ConfirmDialog>
<Button
@ -38,7 +44,7 @@ export function TrashButton(): JSX.Element {
isDestructive
onClick={() => setShowConfirmDialog(true)}
>
Move to Trash
{__('Move to Trash', 'mailpoet')}
</Button>
</>
);

View File

@ -1,7 +1,7 @@
import { useContext } from 'react';
import { __unstableCompositeItem as CompositeItem } from '@wordpress/components';
import { Icon, plus } from '@wordpress/icons';
import { WorkflowCompositeContext } from './context';
import { AutomationCompositeContext } from './context';
type Props = {
onClick?: (element: HTMLButtonElement) => void;
@ -9,7 +9,7 @@ type Props = {
};
export function AddStepButton({ onClick, previousStepId }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext);
const compositeState = useContext(AutomationCompositeContext);
return (
<CompositeItem
state={compositeState}

View File

@ -3,7 +3,7 @@ import { __unstableCompositeItem as CompositeItem } from '@wordpress/components'
import { Icon, plus } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { WorkflowCompositeContext } from './context';
import { AutomationCompositeContext } from './context';
import { Step } from './types';
import { storeName } from '../../store';
@ -12,14 +12,14 @@ type Props = {
};
export function AddTrigger({ step }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext);
const compositeState = useContext(AutomationCompositeContext);
const { setInserterPopover } = useDispatch(storeName);
return (
<CompositeItem
state={compositeState}
role="treeitem"
className="mailpoet-automation-workflow-add-trigger"
className="mailpoet-automation-add-trigger"
data-previous-step-id={step.id}
focusable
onClick={(event) => {

View File

@ -1,5 +1,5 @@
import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components';
import { createContext } from '@wordpress/element';
export const WorkflowCompositeContext =
export const AutomationCompositeContext =
createContext<ReturnType<typeof useCompositeState>>(undefined);

View File

@ -0,0 +1,9 @@
import { __ } from '@wordpress/i18n';
export function EmptyAutomation(): JSX.Element {
return (
<div className="mailpoet-automation-editor-empty-automation">
{__('No automation data.', 'mailpoet')}
</div>
);
}

View File

@ -7,8 +7,8 @@ import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Icon, check } from '@wordpress/icons';
import { Hooks } from 'wp-js-hooks';
import { WorkflowCompositeContext } from './context';
import { EmptyWorkflow } from './empty-workflow';
import { AutomationCompositeContext } from './context';
import { EmptyAutomation } from './empty-automation';
import { Separator } from './separator';
import { Step } from './step';
import { Step as StepData } from './types';
@ -16,11 +16,15 @@ import { InserterPopover } from '../inserter-popover';
import { storeName } from '../../store';
import { AddTrigger } from './add-trigger';
import { Statistics } from './statistics';
import {
RenderStepSeparatorType,
RenderStepType,
} from '../../../types/filters';
export function Workflow(): JSX.Element {
const { workflowData, selectedStep } = useSelect(
export function Automation(): JSX.Element {
const { automationData, selectedStep } = useSelect(
(select) => ({
workflowData: select(storeName).getWorkflowData(),
automationData: select(storeName).getAutomationData(),
selectedStep: select(storeName).getSelectedStep(),
}),
[],
@ -32,9 +36,9 @@ export function Workflow(): JSX.Element {
shift: true,
});
const stepMap = workflowData?.steps ?? undefined;
const stepMap = automationData?.steps ?? undefined;
// serialize steps (for now, we support only one trigger and linear workflows)
// serialize steps (for now, we support only one trigger and linear automations)
const steps = useMemo(() => {
const stepArray = [stepMap.root];
@ -50,9 +54,9 @@ export function Workflow(): JSX.Element {
}, [stepMap]);
const renderStep = useMemo(
() =>
(): RenderStepType =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step',
'mailpoet.automation.render_step',
(stepData: StepData) =>
stepData.type === 'root' ? (
<AddTrigger step={stepData} />
@ -67,9 +71,9 @@ export function Workflow(): JSX.Element {
);
const renderSeparator = useMemo(
() =>
(): RenderStepSeparatorType =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step_separator',
'mailpoet.automation.render_step_separator',
(previousStepData: StepData) => (
<Separator previousStepId={previousStepData.id} />
),
@ -77,20 +81,20 @@ export function Workflow(): JSX.Element {
[],
);
if (!workflowData) {
return <EmptyWorkflow />;
if (!automationData) {
return <EmptyAutomation />;
}
return (
<WorkflowCompositeContext.Provider value={compositeState}>
<AutomationCompositeContext.Provider value={compositeState}>
<Composite
state={compositeState}
role="tree"
aria-label={__('Workflow', 'mailpoet')}
aria-label={__('Automation', 'mailpoet')}
aria-orientation="vertical"
className="mailpoet-automation-editor-workflow"
className="mailpoet-automation-editor-automation"
>
<div className="mailpoet-automation-editor-workflow-wrapper">
<div className="mailpoet-automation-editor-automation-wrapper">
<Statistics />
{stepMap.root.next_steps.length === 0 ? (
<>
@ -115,13 +119,13 @@ export function Workflow(): JSX.Element {
</Fragment>
))}
<Icon
className="mailpoet-automation-editor-workflow-end"
className="mailpoet-automation-editor-automation-end"
icon={check}
/>
<div />
</div>
<InserterPopover />
</Composite>
</WorkflowCompositeContext.Provider>
</AutomationCompositeContext.Provider>
);
}

View File

@ -0,0 +1,40 @@
import { useSelect } from '@wordpress/data';
import { _x } from '@wordpress/i18n';
import { storeName } from '../../store';
import { Statistics as BaseStatistics } from '../../../components/statistics';
export function Statistics(): JSX.Element {
const { automation } = useSelect(
(select) => ({
automation: select(storeName).getAutomationData(),
}),
[],
);
return (
<div className="mailpoet-automation-editor-stats">
<BaseStatistics
items={[
{
key: 'entered',
// translators: Total number of subscribers who entered an automation
label: _x('Total Entered', 'automation stats', 'mailpoet'),
value: automation.stats.totals.entered,
},
{
key: 'processing',
// translators: Total number of subscribers who are being processed in an automation
label: _x('Total Processing', 'automation stats', 'mailpoet'),
value: automation.stats.totals.in_progress,
},
{
key: 'exited',
// translators: Total number of subscribers who exited an automation, no matter the result
label: _x('Total Exited', 'automation stats', 'mailpoet'),
value: automation.stats.totals.exited,
},
]}
/>
</div>
);
}

View File

@ -0,0 +1,77 @@
import { useState, Fragment } from 'react';
import { DropdownMenu } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { moreVertical, trash } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { Hooks } from 'wp-js-hooks';
import { PremiumModal } from 'common/premium_modal';
import { Step as StepData } from './types';
import { storeName } from '../../store';
import { StepMoreControlsType } from '../../../types/filters';
type Props = {
step: StepData;
};
export function StepMoreMenu({ step }: Props): JSX.Element {
const { stepType } = useSelect(
(select) => ({
stepType: select(storeName).getStepType(step.key),
}),
[step],
);
const [showModal, setShowModal] = useState(false);
const moreControls: StepMoreControlsType = Hooks.applyFilters(
'mailpoet.automation.step.more-controls',
{
delete: {
key: 'delete',
control: {
title: __('Delete step', 'mailpoet'),
icon: trash,
onClick: () => setShowModal(true),
},
slot: () => {
if (!showModal) {
return false;
}
return (
<PremiumModal
onRequestClose={() => {
setShowModal(false);
}}
tracking={{
utm_medium: 'upsell_modal',
utm_campaign: 'remove_automation_step',
}}
>
{__('You cannot remove a step from the automation.', 'mailpoet')}
</PremiumModal>
);
},
},
},
step,
stepType,
);
const slots = Object.values(moreControls).filter(
(item) => item.slot !== undefined,
);
const controls = Object.values(moreControls).map((item) => item.control);
return (
<div className="mailpoet-automation-step-more-menu">
{slots.map(({ key, slot }) => (
<Fragment key={key}>{slot()}</Fragment>
))}
<DropdownMenu
label={__('More', 'mailpoet')}
icon={moreVertical}
popoverProps={{ position: 'bottom right' }}
toggleProps={{ isSmall: true }}
controls={Object.values(controls)}
/>
</div>
);
}

View File

@ -3,8 +3,8 @@ import { useContext } from 'react';
import { __unstableCompositeItem as CompositeItem } from '@wordpress/components';
import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
import { blockMeta } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { WorkflowCompositeContext } from './context';
import { __, _x } from '@wordpress/i18n';
import { AutomationCompositeContext } from './context';
import { StepMoreMenu } from './step-more-menu';
import { Step as StepData } from './types';
import { Chip } from '../chip';
@ -48,7 +48,7 @@ export function Step({ step, isSelected }: Props): JSX.Element {
[step],
);
const { openSidebar, selectStep } = useDispatch(storeName);
const compositeState = useContext(WorkflowCompositeContext);
const compositeState = useContext(AutomationCompositeContext);
const { batch } = useRegistry();
const compositeItemId = `step-${step.id}`;
@ -90,7 +90,7 @@ export function Step({ step, isSelected }: Props): JSX.Element {
>
{step.type !== 'trigger'
? stepTypeData.title
: __('Trigger', 'mailpoet')}
: _x('Trigger', 'noun', 'mailpoet')}
</label>
<div className="mailpoet-automation-editor-step-subtitle">
{step.type !== 'trigger'

View File

@ -1,3 +1,5 @@
import { AutomationStatus } from '../../../listing/automation';
export type NextStep = {
id: string;
};
@ -10,10 +12,10 @@ export type Step = {
next_steps: NextStep[];
};
export type Workflow = {
export type Automation = {
id: number;
name: string;
status: 'active' | 'inactive' | 'draft' | 'trash';
status: AutomationStatus;
created_at: string;
updated_at: string;
activated_at: string;

View File

@ -1,16 +1,16 @@
import { ComponentProps, ComponentType, Ref } from 'react';
import {
Dropdown as WpDropdown,
Button,
VisuallyHidden,
__experimentalText as Text,
Button,
Dropdown as WpDropdown,
VisuallyHidden,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { chevronDown } from '@wordpress/icons';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { AutomationStatus } from '../../../listing/automation';
// See: https://github.com/WordPress/gutenberg/blob/eff0cab2b3181c004dbd15398e570ecec28a3726/packages/edit-site/src/components/header/document-actions/index.js
@ -22,10 +22,10 @@ const Dropdown: ComponentType<
> = WpDropdown;
export function DocumentActions({ children }): JSX.Element {
const { workflowName, workflowStatus, showIconLabels } = useSelect(
const { automationName, automationStatus, showIconLabels } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
workflowStatus: select(storeName).getWorkflowData().status,
automationName: select(storeName).getAutomationData().name,
automationStatus: select(storeName).getAutomationData().status,
showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
}),
[],
@ -36,9 +36,9 @@ export function DocumentActions({ children }): JSX.Element {
const titleRef = useRef();
let chipClass = 'mailpoet-automation-editor-chip-gray';
if (workflowStatus === WorkflowStatus.ACTIVE) {
if (automationStatus === AutomationStatus.ACTIVE) {
chipClass = 'mailpoet-automation-editor-chip-success';
} else if (workflowStatus === WorkflowStatus.INACTIVE) {
} else if (automationStatus === AutomationStatus.DEACTIVATING) {
chipClass = 'mailpoet-automation-editor-chip-danger';
}
@ -64,19 +64,21 @@ export function DocumentActions({ children }): JSX.Element {
as="h1"
>
<VisuallyHidden as="span">
{__('Editing workflow: ')}
{__('Editing automation:', 'mailpoet')}
</VisuallyHidden>
{workflowName}
{automationName}
</Text>
<Text
size="body"
className={`edit-site-document-actions__secondary-item ${chipClass}`}
>
{workflowStatus === WorkflowStatus.ACTIVE && __('Active')}
{workflowStatus === WorkflowStatus.INACTIVE &&
__('Inactive')}
{workflowStatus === WorkflowStatus.DRAFT && __('Draft')}
{automationStatus === AutomationStatus.ACTIVE &&
__('Active', 'mailpoet')}
{automationStatus === AutomationStatus.DEACTIVATING &&
__('Deactivating', 'mailpoet')}
{automationStatus === AutomationStatus.DRAFT &&
__('Draft', 'mailpoet')}
</Text>
</a>
<Button
@ -85,9 +87,9 @@ export function DocumentActions({ children }): JSX.Element {
aria-expanded={isOpen}
aria-haspopup="true"
onClick={onToggle}
label={__('Change workflow name')}
label={__('Change automation name', 'mailpoet')}
>
{showIconLabels && __('Rename')}
{showIconLabels && __('Rename', 'mailpoet')}
</Button>
</>
)}

View File

@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n';
import { Chip } from '../chip';
import { ColoredIcon } from '../icons';
import {
StepError as StepErrorType,
StepErrors as StepErrorType,
stepSidebarKey,
storeName,
} from '../../store';
@ -35,17 +35,17 @@ type StepErrorProps = {
function StepError({ stepId }: StepErrorProps): JSX.Element {
const compositeState = useContext(ErrorsCompositeContext);
const { steps, workflowData } = useSelect(
const { steps, automationData } = useSelect(
(select) => ({
steps: select(storeName).getSteps(),
workflowData: select(storeName).getWorkflowData(),
automationData: select(storeName).getAutomationData(),
}),
[],
);
const { openSidebar, selectStep } = useDispatch(storeName);
const stepData = workflowData.steps[stepId];
const stepData = automationData.steps[stepId];
const step = steps.find(({ key }) => key === stepData.key);
return (
@ -78,10 +78,10 @@ export function Errors(): JSX.Element | null {
shift: true,
});
const { errors, workflowData } = useSelect(
const { errors, automationData } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
workflowData: select(storeName).getWorkflowData(),
automationData: select(storeName).getAutomationData(),
}),
[],
);
@ -93,18 +93,18 @@ export function Errors(): JSX.Element | null {
}
const visited = new Map<string, StepErrorType | undefined>();
const ids = workflowData.steps.root.next_steps.map(({ id }) => id);
const ids = automationData.steps.root.next_steps.map(({ id }) => id);
while (ids.length > 0) {
const id = ids.shift();
if (!visited.has(id)) {
visited.set(id, errors.steps[id]);
workflowData.steps[id]?.next_steps?.forEach((step) =>
automationData.steps[id]?.next_steps?.forEach((step) =>
ids.push(step.id),
);
}
}
return [...visited.values()].filter((error) => !!error);
}, [errors, workflowData]);
}, [errors, automationData]);
// automatically open the popover when errors appear
const hasErrors = stepErrors.length > 0;
@ -151,11 +151,14 @@ export function Errors(): JSX.Element | null {
<Composite
state={compositeState}
role="list"
aria-label={__('Workflow errors', 'mailpoet')}
aria-label={__('Automation errors', 'mailpoet')}
className="mailpoet-automation-errors"
>
<div className="mailpoet-automation-errors-header">
{__('The following steps are not fully set:', 'mailpoet')}
{
// translators: Label for a list of automation steps that are incomplete or have errors
__('The following steps are not fully set:', 'mailpoet')
}
</div>
{stepErrors.map((error) => (
<StepError key={error.step_id} stepId={error.step_id} />

View File

@ -1,5 +1,10 @@
import { useState } from 'react';
import { Button, NavigableMenu, TextControl } from '@wordpress/components';
import {
Button,
NavigableMenu,
TextControl,
Tooltip,
} from '@wordpress/components';
import { dispatch, useDispatch, useSelect } from '@wordpress/data';
import { PinnedItems } from '@wordpress/interface';
import { __ } from '@wordpress/i18n';
@ -8,44 +13,100 @@ import { Errors } from './errors';
import { InserterToggle } from './inserter_toggle';
import { MoreMenu } from './more_menu';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { DeactivateModal } from '../modals/deactivate-modal';
import { AutomationStatus } from '../../../listing/automation';
import {
DeactivateImmediatelyModal,
DeactivateModal,
} from '../modals/deactivate-modal';
// See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/index.js
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
function ActivateButton({ onClick }): JSX.Element {
const { errors } = useSelect(
function ActivateButton({ label }): JSX.Element {
const { errors, isDeactivating } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
isDeactivating:
select(storeName).getAutomationData().status ===
AutomationStatus.DEACTIVATING,
}),
[],
);
const { openActivationPanel } = useDispatch(storeName);
return (
const button = (
<Button
variant="primary"
className="editor-post-publish-button"
onClick={onClick}
disabled={!!errors}
onClick={openActivationPanel}
disabled={isDeactivating || !!errors}
>
{__('Activate', 'mailpoet')}
{label}
</Button>
);
if (isDeactivating) {
return (
<Tooltip
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// The following error seems to be a mismatch. It claims the 'delay' prop does not exist, but it does.
delay={0}
text={__(
'Editing an active automation is temporarily unavailable. We are working on introducing this functionality.',
'mailpoet',
)}
>
{button}
</Tooltip>
);
}
return button;
}
function UpdateButton(): JSX.Element {
const { save } = useDispatch(storeName);
const { automation } = useSelect(
(select) => ({
automation: select(storeName).getAutomationData(),
}),
[],
);
if (automation.stats.totals.in_progress === 0) {
return (
<Button
variant="primary"
className="editor-post-publish-button"
onClick={save}
>
{__('Update', 'mailpoet')}
</Button>
);
}
return (
<Button
variant="primary"
className="editor-post-publish-button"
onClick={save}
<Tooltip
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// The following error seems to be a mismatch. It claims the 'delay' prop does not exist, but it does.
delay={0}
text={__(
'Editing an active automation is temporarily unavailable. We are working on introducing this functionality.',
'mailpoet',
)}
>
{__('Update', 'mailpoet')}
</Button>
<Button
variant="primary"
className="editor-post-publish-button"
onClick={save}
disabled
>
{__('Update', 'mailpoet')}
</Button>
</Tooltip>
);
}
@ -65,7 +126,7 @@ function DeactivateButton(): JSX.Element {
const { hasUsersInProgress } = useSelect(
(select) => ({
hasUsersInProgress:
select(storeName).getWorkflowData().stats.totals.in_progress > 0,
select(storeName).getAutomationData().stats.totals.in_progress > 0,
}),
[],
);
@ -99,20 +160,56 @@ function DeactivateButton(): JSX.Element {
);
}
function DeactivateNowButton(): JSX.Element {
const [showDeactivateModal, setShowDeactivateModal] = useState(false);
const [isBusy, setIsBusy] = useState(false);
const { hasUsersInProgress } = useSelect(
(select) => ({
hasUsersInProgress:
select(storeName).getAutomationData().stats.totals.in_progress > 0,
}),
[],
);
const deactivateOrShowModal = () => {
if (hasUsersInProgress) {
setShowDeactivateModal(true);
return;
}
setIsBusy(true);
void dispatch(storeName).deactivate();
};
return (
<>
{showDeactivateModal && (
<DeactivateImmediatelyModal
onClose={() => {
setShowDeactivateModal(false);
}}
/>
)}
<Button
isBusy={isBusy}
variant="tertiary"
onClick={deactivateOrShowModal}
>
{__('Deactivate now', 'mailpoet')}
</Button>
</>
);
}
type Props = {
showInserterToggle: boolean;
toggleActivatePanel: () => void;
};
export function Header({
showInserterToggle,
toggleActivatePanel,
}: Props): JSX.Element {
const { setWorkflowName } = useDispatch(storeName);
const { workflowName, workflowStatus } = useSelect(
export function Header({ showInserterToggle }: Props): JSX.Element {
const { setAutomationName } = useDispatch(storeName);
const { automationName, automationStatus } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
workflowStatus: select(storeName).getWorkflowData().status,
automationName: select(storeName).getAutomationData().name,
automationStatus: select(storeName).getAutomationData().status,
}),
[],
);
@ -137,8 +234,8 @@ export function Header({
{__('Automation name', 'mailpoet')}
</div>
<TextControl
value={workflowName}
onChange={(newName) => setWorkflowName(newName)}
value={automationName}
onChange={(newName) => setAutomationName(newName)}
help={__(
`Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`,
'mailpoet',
@ -152,18 +249,24 @@ export function Header({
<div className="edit-site-header_end">
<div className="edit-site-header__actions">
<Errors />
{workflowStatus !== WorkflowStatus.ACTIVE && (
{automationStatus === AutomationStatus.DRAFT && (
<>
<SaveDraftButton />
<ActivateButton onClick={toggleActivatePanel} />
<ActivateButton label={__('Activate', 'mailpoet')} />
</>
)}
{workflowStatus === WorkflowStatus.ACTIVE && (
{automationStatus === AutomationStatus.ACTIVE && (
<>
<DeactivateButton />
<UpdateButton />
</>
)}
{automationStatus === AutomationStatus.DEACTIVATING && (
<>
<DeactivateNowButton />
<ActivateButton label={__('Update & Activate', 'mailpoet')} />
</>
)}
<PinnedItems.Slot scope={storeName} />
<MoreMenu />
</div>

View File

@ -1,6 +1,6 @@
import { Button, ToolbarItem } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { __, _x } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { plus } from '@wordpress/icons';
import { storeName } from '../../store';
@ -28,13 +28,11 @@ export function InserterToggle(): JSX.Element {
onMouseDown={(event) => event.preventDefault()}
onClick={toggleInserterSidebar}
icon={plus}
label={_x(
'Toggle step inserter',
'Generic label for step inserter button',
)}
label={__('Toggle step inserter', 'mailpoet')}
showTooltip={!showIconLabels}
>
{showIconLabels && (!isInserterOpened ? __('Add') : __('Close'))}
{showIconLabels &&
(!isInserterOpened ? __('Add', 'mailpoet') : __('Close', 'mailpoet'))}
</ToolbarItem>
);
}

View File

@ -20,14 +20,14 @@ export function MoreMenu(): JSX.Element {
>
{() => (
<>
<MenuGroup label={_x('View', 'noun')}>
<MenuGroup label={_x('View', 'noun', 'mailpoet')}>
<PreferenceToggleMenuItem
scope={storeName}
name="fullscreenMode"
label={__('Fullscreen mode')}
info={__('Work without distraction')}
messageActivated={__('Fullscreen mode activated')}
messageDeactivated={__('Fullscreen mode deactivated')}
label={__('Fullscreen mode', 'mailpoet')}
info={__('Work without distraction', 'mailpoet')}
messageActivated={__('Fullscreen mode activated', 'mailpoet')}
messageDeactivated={__('Fullscreen mode deactivated', 'mailpoet')}
shortcut={displayShortcut.secondary('f')}
/>
</MenuGroup>

View File

@ -13,7 +13,10 @@ export const InserterListboxGroup = forwardRef<HTMLDivElement, Props>(
useEffect(() => {
if (shouldSpeak) {
speak(__('Use left and right arrow keys to move through blocks'));
speak(
// translators: Moving through automation step list using keyboard
__('Use left and right arrow keys to move through steps', 'mailpoet'),
);
}
}, [shouldSpeak]);

View File

@ -7,6 +7,7 @@ import { PremiumModal } from 'common/premium_modal';
import { Inserter } from '../inserter';
import { Item } from '../inserter/item';
import { storeName } from '../../store';
import { AddStepCallbackType } from '../../../types/filters';
export function InserterPopover(): JSX.Element | null {
const popoverRef = useRef<HTMLDivElement>();
@ -20,8 +21,8 @@ export function InserterPopover(): JSX.Element | null {
const { setInserterPopover } = useDispatch(storeName);
const onInsert = useCallback((item: Item) => {
const addStepCallback = Hooks.applyFilters(
'mailpoet.automation.workflow.add_step_callback',
const addStepCallback: AddStepCallbackType = Hooks.applyFilters(
'mailpoet.automation.add_step_callback',
() => {
setShowModal(true);
},

View File

@ -2,7 +2,7 @@ import { forwardRef, Fragment, useCallback, useMemo } from 'react';
import { SearchControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useRef, useImperativeHandle, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { __, _x } from '@wordpress/i18n';
import { blockDefault, Icon } from '@wordpress/icons';
import { Group } from './group';
import { Item } from './item';
@ -41,21 +41,26 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
{
type: 'triggers',
title: undefined,
label: __('Triggers', 'mailpoet'),
// translators: Label for a list of automation steps of type trigger
label: _x('Triggers', 'automation steps', 'mailpoet'),
items: steps.filter(({ group }) => group === 'triggers'),
},
]
: [
{
type: 'actions',
title: __('Actions', 'mailpoet'),
label: __('Actions', 'mailpoet'),
// translators: Label for a list of automation steps of type action
title: _x('Actions', 'automation steps', 'mailpoet'),
// translators: Label for a list of automation steps of type action
label: _x('Actions', 'automation steps', 'mailpoet'),
items: steps.filter(({ group }) => group === 'actions'),
},
{
type: 'logical',
title: __('Logical', 'mailpoet'),
label: __('Logical', 'mailpoet'),
// translators: Label for a list of logical automation steps (if/else, etc.)
title: _x('Logical', 'automation steps', 'mailpoet'),
// translators: Label for a list of logical automation steps (if/else, etc.)
label: _x('Logical', 'automation steps', 'mailpoet'),
items: steps.filter(({ group }) => group === 'logical'),
},
],
@ -96,8 +101,8 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
setFilterValue(value);
}}
value={filterValue}
label={__('Search for blocks and patterns')}
placeholder={__('Search')}
label={__('Search for automation steps', 'mailpoet')}
placeholder={__('Search', 'mailpoet')}
ref={searchRef}
/>
@ -135,7 +140,7 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
className="block-editor-inserter__no-results-icon"
icon={blockDefault}
/>
<p>{__('No results found.')}</p>
<p>{__('No results found.', 'mailpoet')}</p>
</div>
)}
</InserterListbox>

View File

@ -5,7 +5,7 @@ import {
store as keyboardShortcutsStore,
} from '@wordpress/keyboard-shortcuts';
import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/keyboard-shortcuts/index.js
@ -25,7 +25,7 @@ export function KeyboardShortcuts(): null {
void registerShortcut({
name: 'mailpoet/automation-editor/toggle-fullscreen',
category: 'global',
description: __('Toggle fullscreen mode.'),
description: __('Toggle fullscreen mode.', 'mailpoet'),
keyCombination: {
modifier: 'secondary',
character: 'f',
@ -35,7 +35,7 @@ export function KeyboardShortcuts(): null {
void registerShortcut({
name: 'mailpoet/automation-editor/toggle-sidebar',
category: 'global',
description: __('Show or hide the settings sidebar.'),
description: __('Show or hide the settings sidebar.', 'mailpoet'),
keyCombination: {
modifier: 'primaryShift',
character: ',',
@ -55,7 +55,7 @@ export function KeyboardShortcuts(): null {
} else {
const sidebarToOpen = selectedStep()
? stepSidebarKey
: workflowSidebarKey;
: automationSidebarKey;
openSidebar(sidebarToOpen);
}
});

View File

@ -3,23 +3,66 @@ import { Button, Modal } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { dispatch, useSelect } from '@wordpress/data';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { AutomationStatus } from '../../../listing/automation';
export function DeactivateModal({ onClose }): JSX.Element {
const { workflowName } = useSelect(
type DeactivateImmediatelyModalProps = {
onClose: () => void;
};
export function DeactivateImmediatelyModal({
onClose,
}: DeactivateImmediatelyModalProps): JSX.Element {
const [isBusy, setIsBusy] = useState<boolean>(false);
return (
<Modal
className="mailpoet-automatoin-deactivate-modal"
title={__('Stop automation for all subscribers?', 'mailpoet')}
onRequestClose={onClose}
>
<p>
{__(
'Are you sure you want to deactivate now? This would stop this automation for all subscribers immediately.',
'mailpoet',
)}
</p>
<Button
isBusy={isBusy}
variant="primary"
onClick={() => {
setIsBusy(true);
dispatch(storeName).deactivate(true);
}}
>
{__('Deactivate now', 'mailpoet')}
</Button>
<Button disabled={isBusy} variant="tertiary" onClick={onClose}>
{__('Cancel', 'mailpoet')}
</Button>
</Modal>
);
}
type DeactivateModalProps = {
onClose: () => void;
};
export function DeactivateModal({
onClose,
}: DeactivateModalProps): JSX.Element {
const { automationName } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
automationName: select(storeName).getAutomationData().name,
}),
[],
);
const [selected, setSelected] = useState<
WorkflowStatus.INACTIVE | WorkflowStatus.DEACTIVATING
>(WorkflowStatus.DEACTIVATING);
AutomationStatus.DRAFT | AutomationStatus.DEACTIVATING
>(AutomationStatus.DEACTIVATING);
const [isBusy, setIsBusy] = useState<boolean>(false);
// translators: %s is the name of the automation.
const title = sprintf(
__('Deactivate the "%s" automation?', 'mailpoet'),
workflowName,
automationName,
);
return (
@ -36,7 +79,7 @@ export function DeactivateModal({ onClose }): JSX.Element {
<li>
<label
className={
selected === WorkflowStatus.DEACTIVATING
selected === AutomationStatus.DEACTIVATING
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
@ -46,8 +89,8 @@ export function DeactivateModal({ onClose }): JSX.Element {
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.DEACTIVATING}
onChange={() => setSelected(WorkflowStatus.DEACTIVATING)}
checked={selected === AutomationStatus.DEACTIVATING}
onChange={() => setSelected(AutomationStatus.DEACTIVATING)}
/>
</span>
<span>
@ -64,7 +107,7 @@ export function DeactivateModal({ onClose }): JSX.Element {
<li>
<label
className={
selected === WorkflowStatus.INACTIVE
selected === AutomationStatus.DRAFT
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
@ -74,8 +117,8 @@ export function DeactivateModal({ onClose }): JSX.Element {
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.INACTIVE}
onChange={() => setSelected(WorkflowStatus.INACTIVE)}
checked={selected === AutomationStatus.DRAFT}
onChange={() => setSelected(AutomationStatus.DRAFT)}
/>
</span>
<span>
@ -96,12 +139,9 @@ export function DeactivateModal({ onClose }): JSX.Element {
variant="primary"
onClick={() => {
setIsBusy(true);
if (selected === WorkflowStatus.DEACTIVATING) {
// @ToDo Use the correct method provided in MAILPOET-4731
dispatch(storeName).deactivate();
return;
}
dispatch(storeName).deactivate();
dispatch(storeName).deactivate(
selected !== AutomationStatus.DEACTIVATING,
);
}}
>
{__('Deactivate automation', 'mailpoet')}

View File

@ -4,7 +4,7 @@ import { Button, Spinner } from '@wordpress/components';
import { closeSmall } from '@wordpress/icons';
import { __, sprintf } from '@wordpress/i18n';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { AutomationStatus } from '../../../listing/automation';
import { MailPoet } from '../../../../mailpoet';
function PreStep({ onClose }): JSX.Element {
@ -58,9 +58,9 @@ function PreStep({ onClose }): JSX.Element {
}
function PostStep({ onClose }): JSX.Element {
const { workflow } = useSelect(
const { automation } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
automation: select(storeName).getAutomationData(),
}),
[],
);
@ -81,10 +81,10 @@ function PostStep({ onClose }): JSX.Element {
<div className="mailpoet-automation-activate-panel__body">
<div className="mailpoet-automation-activate-panel__section">
{sprintf(__('"%s" is now live.', 'mailpoet'), workflow.name)}
{sprintf(__('"%s" is now live.', 'mailpoet'), automation.name)}
</div>
<p>
<strong>{__("What's next?", 'mailpoet')}</strong>
<strong>{__('Whats next?', 'mailpoet')}</strong>
</p>
<p>
{__(
@ -100,29 +100,31 @@ function PostStep({ onClose }): JSX.Element {
);
}
export function ActivatePanel({ onClose }): JSX.Element {
const { workflow, errors } = useSelect(
export function ActivatePanel(): JSX.Element {
const { automation, errors } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
workflow: select(storeName).getWorkflowData(),
automation: select(storeName).getAutomationData(),
}),
[],
);
const { closeActivationPanel } = useDispatch(storeName);
useEffect(() => {
if (errors) {
onClose();
closeActivationPanel();
}
}, [errors, onClose]);
}, [errors, closeActivationPanel]);
if (errors) {
return null;
}
const isActive = workflow.status === WorkflowStatus.ACTIVE;
const isActive = automation.status === AutomationStatus.ACTIVE;
return (
<div className="mailpoet-automation-activate-panel">
{isActive && <PostStep onClose={onClose} />}
{!isActive && <PreStep onClose={onClose} />}
{isActive && <PostStep onClose={closeActivationPanel} />}
{!isActive && <PreStep onClose={closeActivationPanel} />}
</div>
);
}

View File

@ -0,0 +1,24 @@
import { PanelBody as WpPanelBody } from '@wordpress/components';
import { useEffect, useState } from 'react';
type Props = WpPanelBody.Props & {
hasErrors?: boolean;
};
export function PanelBody({ hasErrors = false, ...props }: Props): JSX.Element {
const [isOpened, setIsOpened] = useState(props.initialOpen);
useEffect(() => {
if (hasErrors) {
setIsOpened(true);
}
}, [hasErrors]);
return (
<WpPanelBody
opened={isOpened}
onToggle={() => setIsOpened((prevState) => !prevState)}
{...props}
/>
);
}

View File

@ -1,4 +1,5 @@
import { Dropdown, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { edit, Icon } from '@wordpress/icons';
import { PlainBodyTitle } from './plain-body-title';
import { TitleActionButton } from './title-action-button';
@ -25,7 +26,7 @@ export function StepName({
<TitleActionButton
onClick={onToggle}
aria-expanded={isOpen}
aria-label="Edit step name"
aria-label={__('Edit step name', 'mailpoet')}
>
<Icon icon={edit} size={16} />
</TitleActionButton>
@ -33,13 +34,15 @@ export function StepName({
)}
renderContent={() => (
<TextControl
label="Step name"
label={__('Step name', 'mailpoet')}
className="mailpoet-step-name-input"
placeholder={defaultName}
value={currentName}
onChange={update}
help="Give the automation step a name that indicates its purpose. E.g
Abandoned cart recovery. This name will be displayed only to you and not to the clients."
help={__(
'Give the automation step a name that indicates its purpose. E.g "Abandoned cart recovery". This name will be displayed only to you and not to the clients.',
'mailpoet',
)}
/>
)}
/>

View File

@ -1,12 +1,13 @@
import { PanelBody, PanelRow } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../../store';
import { TrashButton } from '../../actions/trash-button';
export function WorkflowSidebar(): JSX.Element {
const { workflowData } = useSelect(
export function AutomationSidebar(): JSX.Element {
const { automationData } = useSelect(
(select) => ({
workflowData: select(storeName).getWorkflowData(),
automationData: select(storeName).getAutomationData(),
}),
[],
);
@ -18,33 +19,33 @@ export function WorkflowSidebar(): JSX.Element {
};
return (
<PanelBody title="Automation details" initialOpen>
<PanelBody title={__('Automation details', 'mailpoet')} initialOpen>
<PanelRow>
<strong>Date added</strong>{' '}
{new Date(Date.parse(workflowData.created_at)).toLocaleDateString(
{new Date(Date.parse(automationData.created_at)).toLocaleDateString(
undefined,
dateOptions,
)}
</PanelRow>
<PanelRow>
<strong>Activated</strong>{' '}
{workflowData.status === 'active' &&
new Date(Date.parse(workflowData.updated_at)).toLocaleDateString(
{automationData.status === 'active' &&
new Date(Date.parse(automationData.updated_at)).toLocaleDateString(
undefined,
dateOptions,
)}
{workflowData.status !== 'active' &&
workflowData.activated_at &&
new Date(Date.parse(workflowData.activated_at)).toLocaleDateString(
{automationData.status !== 'active' &&
automationData.activated_at &&
new Date(Date.parse(automationData.activated_at)).toLocaleDateString(
undefined,
dateOptions,
)}
{workflowData.status !== 'active' && !workflowData.activated_at && (
{automationData.status !== 'active' && !automationData.activated_at && (
<span className="mailpoet-deactive">Not activated yet.</span>
)}
</PanelRow>
<PanelRow>
<strong>Author</strong> {workflowData.author.name}
<strong>Author</strong> {automationData.author.name}
</PanelRow>
<PanelRow>
<TrashButton />

View File

@ -1,6 +1,7 @@
import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/sidebar/settings-header/index.js
@ -12,29 +13,29 @@ type Props = {
export function Header({ sidebarKey }: Props): JSX.Element {
const { openSidebar } = useDispatch(storeName);
const openWorkflowSettings = () => openSidebar(workflowSidebarKey);
const openAutomationSettings = () => openSidebar(automationSidebarKey);
const openStepSettings = () => openSidebar(stepSidebarKey);
const [workflowAriaLabel, workflowActiveClass] =
sidebarKey === workflowSidebarKey
? ['Workflow (selected)', 'is-active']
: ['Workflow', ''];
const [automationAriaLabel, automationActiveClass] =
sidebarKey === automationSidebarKey
? [__('Automation (selected)', 'mailpoet'), 'is-active']
: [__('Automation', 'mailpoet'), ''];
const [stepAriaLabel, stepActiveClass] =
sidebarKey === stepSidebarKey
? ['Step (selected)', 'is-active']
: ['Step', ''];
? [__('Step (selected)', 'mailpoet'), 'is-active']
: [__('Step', 'mailpoet'), ''];
return (
<ul>
<li>
<Button
onClick={openWorkflowSettings}
className={`edit-site-sidebar__panel-tab ${workflowActiveClass}`}
aria-label={workflowAriaLabel}
data-label="Workflow"
onClick={openAutomationSettings}
className={`edit-site-sidebar__panel-tab ${automationActiveClass}`}
aria-label={automationAriaLabel}
data-label={__('Automation', 'mailpoet')}
>
Workflow
{__('Automation', 'mailpoet')}
</Button>
</li>
<li>
@ -42,9 +43,9 @@ export function Header({ sidebarKey }: Props): JSX.Element {
onClick={openStepSettings}
className={`edit-site-sidebar__panel-tab ${stepActiveClass}`}
aria-label={stepAriaLabel}
data-label="Workflow"
data-label={__('Step', 'mailpoet')}
>
Step
{__('Step', 'mailpoet')}
</Button>
</li>
</ul>

View File

@ -10,8 +10,8 @@ import {
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { Header } from './header';
import { StepSidebar } from './step';
import { WorkflowSidebar } from './workflow';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
import { AutomationSidebar } from './automation';
import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See:
// https://github.com/WordPress/gutenberg/blob/5caeae34b3fb303761e3b9432311b26f4e5ea3a6/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js
@ -26,7 +26,7 @@ const sidebarActiveByDefault = Platform.select({
type Props = ComponentProps<typeof ComplementaryArea>;
export function Sidebar(props: Props): JSX.Element {
const { keyboardShortcut, sidebarKey, showIconLabels, workflowName } =
const { keyboardShortcut, sidebarKey, showIconLabels, automationName } =
useSelect(
(select) => ({
keyboardShortcut: select(
@ -36,9 +36,9 @@ export function Sidebar(props: Props): JSX.Element {
),
sidebarKey:
select(interfaceStore).getActiveComplementaryArea(storeName) ??
workflowSidebarKey,
automationSidebarKey,
showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
workflowName: select(storeName).getWorkflowData().name,
automationName: select(storeName).getAutomationData().name,
}),
[],
);
@ -47,20 +47,20 @@ export function Sidebar(props: Props): JSX.Element {
<ComplementaryArea
identifier={sidebarKey}
header={<Header sidebarKey={sidebarKey} />}
closeLabel={__('Close settings')}
closeLabel={__('Close settings', 'mailpoet')}
headerClassName="edit-site-sidebar__panel-tabs"
title={__('Settings')}
title={__('Settings', 'mailpoet')}
icon={cog}
className="edit-site-sidebar mailpoet-automation-sidebar"
panelClassName="edit-site-sidebar"
smallScreenTitle={workflowName || __('(no title)')}
smallScreenTitle={automationName || __('(no title)', 'mailpoet')}
scope={storeName}
toggleShortcut={keyboardShortcut}
isActiveByDefault={sidebarActiveByDefault}
showIconLabels={showIconLabels}
{...props}
>
{sidebarKey === workflowSidebarKey && <WorkflowSidebar />}
{sidebarKey === automationSidebarKey && <AutomationSidebar />}
{sidebarKey === stepSidebarKey && <StepSidebar />}
</ComplementaryArea>
);

View File

@ -30,22 +30,11 @@ export function StepSidebar(): JSX.Element {
icon={selectedStepType.icon}
/>
<Edit />
<PanelBody title="Debug info" initialOpen={false}>
<div>
<strong>ID:</strong> {selectedStep.id}
</div>
<div>
<strong>Type:</strong> {selectedStep.type}
</div>
<div>
<strong>Key:</strong> {selectedStep.key}
</div>
<div>
<strong>Args:</strong> {JSON.stringify(selectedStep.args)}
</div>
</PanelBody>
<Edit
// Force sidebar remount to avoid different steps mixing their data.
// This can happen e.g. when having "useState" or "useRef" internally.
key={selectedStep.id}
/>
</div>
);
}

View File

@ -1,7 +0,0 @@
export function EmptyWorkflow(): JSX.Element {
return (
<div className="mailpoet-automation-editor-empty-workflow">
No workflow data.
</div>
);
}

View File

@ -1,37 +0,0 @@
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../store';
import { Statistics as BaseStatistics } from '../../../components/statistics';
export function Statistics(): JSX.Element {
const { workflow } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
}),
[],
);
return (
<div className="mailpoet-automation-editor-stats">
<BaseStatistics
items={[
{
key: 'entered',
label: __('Total Entered', 'mailpoet'),
value: workflow.stats.totals.entered,
},
{
key: 'processing',
label: __('Total Processing', 'mailpoet'),
value: workflow.stats.totals.in_progress,
},
{
key: 'exited',
label: __('Total Exited', 'mailpoet'),
value: workflow.stats.totals.exited,
},
]}
/>
</div>
);
}

View File

@ -1,59 +0,0 @@
import { useCallback, useState } from 'react';
import { DropdownMenu } from '@wordpress/components';
import { moreVertical, trash } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { Hooks } from 'wp-js-hooks';
import { PremiumModal } from 'common/premium_modal';
import { Step as StepData } from './types';
type Props = {
step: StepData;
};
export function StepMoreMenu({ step }: Props): JSX.Element {
const [showModal, setShowModal] = useState(false);
const onDelete = useCallback((stepData: StepData) => {
const deleteStepCallback = Hooks.applyFilters(
'mailpoet.automation.workflow.delete_step_callback',
() => {
setShowModal(true);
},
);
deleteStepCallback(stepData);
}, []);
return (
<>
<div className="mailpoet-automation-step-more-menu">
<DropdownMenu
label={__('More', 'mailpoet')}
icon={moreVertical}
controls={[
{
title: __('Delete step', 'mailpoet'),
icon: trash,
onClick: () => onDelete(step),
},
]}
popoverProps={{ position: 'bottom right' }}
toggleProps={{ isSmall: true }}
/>
</div>
{showModal && (
<PremiumModal
onRequestClose={() => {
setShowModal(false);
}}
tracking={{
utm_medium: 'upsell_modal',
utm_campaign: 'remove_automation_step',
}}
>
{__('You cannot remove a new step from the automation.', 'mailpoet')}
</PremiumModal>
)}
</>
);
}

View File

@ -1,30 +1,39 @@
import classnames from 'classnames';
import ReactDOM from 'react-dom';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Button, Icon, Popover, SlotFillProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import {
dispatch,
select as globalSelect,
StoreDescriptor,
useSelect,
} from '@wordpress/data';
import { wordpress } from '@wordpress/icons';
import {
ComplementaryArea,
InterfaceSkeleton,
FullscreenMode,
InterfaceSkeleton,
} from '@wordpress/interface';
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import { Header } from './components/header';
import { InserterSidebar } from './components/inserter-sidebar';
import { KeyboardShortcuts } from './components/keyboard-shortcuts';
import { EditorNotices } from './components/notices';
import { Sidebar } from './components/sidebar';
import { Workflow } from './components/workflow';
import { Automation } from './components/automation';
import { createStore, storeName } from './store';
import { initializeApi } from '../api';
import { initialize as initializeCoreIntegration } from '../integrations/core';
import { initialize as initializeMailPoetIntegration } from '../integrations/mailpoet';
import { MailPoet } from '../../mailpoet';
import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices';
import { LISTING_NOTICE_PARAMETERS } from '../listing/automation-listing-notices';
import { registerApiErrorHandler } from './api-error-handler';
import { ActivatePanel } from './components/panel/activate-panel';
import { registerTranslations } from '../i18n';
import { AutomationStatus } from '../listing/automation';
// See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js
@ -33,41 +42,97 @@ import { ActivatePanel } from './components/panel/activate-panel';
// disable inserter sidebar until we implement drag & drop
const showInserterSidebar = false;
/**
* Show temporary message that active automations cant be updated
*
* see MAILPOET-4744
*/
function updatingActiveAutomationNotPossible() {
const automation = globalSelect(storeName).getAutomationData();
if (
![AutomationStatus.ACTIVE, AutomationStatus.DEACTIVATING].includes(
automation.status,
)
) {
return;
}
if (automation.stats.totals.in_progress === 0) {
return;
}
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
void createNotice(
'success',
__(
'Editing an active automation is temporarily unavailable. We are working on introducing this functionality.',
'mailpoet',
),
{
type: 'snackbar',
},
);
}
function onUnload(event) {
if (!globalSelect(storeName).getAutomationSaved()) {
// eslint-disable-next-line no-param-reassign
event.returnValue = __(
'There are unsaved changes that will be lost. Do you want to continue?',
'mailpoet',
);
return event.returnValue;
}
return '';
}
function useConfirmUnsaved() {
useEffect(() => {
window.addEventListener('beforeunload', onUnload);
return () => window.removeEventListener('beforeunload', onUnload);
}, []);
}
function Editor(): JSX.Element {
const {
isFullscreenActive,
isInserterOpened,
isActivationPanelOpened,
isSidebarOpened,
showIconLabels,
workflow,
automation,
} = useSelect(
(select) => ({
isFullscreenActive: select(storeName).isFeatureActive('fullscreenMode'),
isInserterOpened: select(storeName).isInserterSidebarOpened(),
isSidebarOpened: select(storeName).isSidebarOpened(),
isActivationPanelOpened: select(storeName).isActivationPanelOpened(),
showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
workflow: select(storeName).getWorkflowData(),
automation: select(storeName).getAutomationData(),
}),
[],
);
const [showActivatePanel, setShowActivatePanel] = useState(false);
const [isBooting, setIsBooting] = useState(true);
useConfirmUnsaved();
useEffect(() => {
if (!isBooting) {
return;
}
updatingActiveAutomationNotPossible();
setIsBooting(false);
}, [isBooting]);
const className = classnames('interface-interface-skeleton', {
'is-sidebar-opened': isSidebarOpened,
'show-icon-labels': showIconLabels,
});
if (workflow.status === 'trash') {
if (automation.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted]: workflow.id,
[LISTING_NOTICE_PARAMETERS.automationHadBeenDeleted]: automation.id,
});
return null;
}
const toggleActivatePanel = () => {
setShowActivatePanel(!showActivatePanel);
};
return (
<ShortcutProvider>
<SlotFillProvider>
@ -88,16 +153,11 @@ function Editor(): JSX.Element {
</div>
)
}
header={
<Header
showInserterToggle={showInserterSidebar}
toggleActivatePanel={toggleActivatePanel}
/>
}
header={<Header showInserterToggle={showInserterSidebar} />}
content={
<>
<EditorNotices />
<Workflow />
<Automation />
</>
}
sidebar={<ComplementaryArea.Slot scope={storeName} />}
@ -105,7 +165,7 @@ function Editor(): JSX.Element {
showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null
}
/>
{showActivatePanel && <ActivatePanel onClose={toggleActivatePanel} />}
{isActivationPanelOpened && <ActivatePanel />}
<Popover.Slot />
</SlotFillProvider>
</ShortcutProvider>
@ -117,6 +177,7 @@ window.addEventListener('DOMContentLoaded', () => {
const root = document.getElementById('mailpoet_automation_editor');
if (root) {
registerTranslations();
registerApiErrorHandler();
initializeApi();
initializeCoreIntegration();

View File

@ -7,14 +7,42 @@ import { store as preferencesStore } from '@wordpress/preferences';
import { addQueryArgs } from '@wordpress/url';
import { storeName } from './constants';
import { Feature, State } from './types';
import { LISTING_NOTICE_PARAMETERS } from '../../listing/workflow-listing-notices';
import { LISTING_NOTICE_PARAMETERS } from '../../listing/automation-listing-notices';
import { MailPoet } from '../../../mailpoet';
import { WorkflowStatus } from '../../listing/workflow';
import { AutomationStatus } from '../../listing/automation';
export const openSidebar =
(key) =>
({ registry }) =>
const trackErrors = (errors) => {
if (!errors?.steps) {
return;
}
const payload = Object.keys(errors.steps as object).map((stepId) => {
const error = errors.steps[stepId];
const stepKey = select(storeName).getStepById(stepId)?.key;
const fields = Object.keys(error.fields as object)
.map((field) => `${stepKey}/${field}`)
.reduce((prev, next) => prev.concat(next));
return fields;
});
MailPoet.trackEvent('Automations > Automation validation error', {
errors: payload,
});
};
export const openActivationPanel = () => ({
type: 'SET_ACTIVATION_PANEL_VISIBILITY',
value: true,
});
export const closeActivationPanel = () => ({
type: 'SET_ACTIVATION_PANEL_VISIBILITY',
value: false,
});
export const openSidebar = (key) => {
dispatch(storeName).closeActivationPanel();
return ({ registry }) =>
registry.dispatch(interfaceStore).enableComplementaryArea(storeName, key);
};
export const closeSidebar =
() =>
@ -46,44 +74,55 @@ export function selectStep(value) {
} as const;
}
export function setWorkflowName(name) {
const workflow = select(storeName).getWorkflowData();
export function setAutomationName(name) {
const automation = select(storeName).getAutomationData();
return {
type: 'UPDATE_WORKFLOW',
workflow: {
...workflow,
type: 'UPDATE_AUTOMATION',
automation: {
...automation,
name,
},
} as const;
}
export function* save() {
const workflow = select(storeName).getWorkflowData();
const automation = select(storeName).getAutomationData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
path: `/automations/${automation.id}`,
method: 'PUT',
data: { ...workflow },
data: { ...automation },
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data) {
void createNotice(
'success',
__('The automation has been saved.', 'mailpoet'),
{
type: 'snackbar',
},
);
}
return {
type: 'SAVE',
workflow: data?.data ?? workflow,
automation: data?.data ?? automation,
} as const;
}
export function* activate() {
const workflow = select(storeName).getWorkflowData();
const automation = select(storeName).getAutomationData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
path: `/automations/${automation.id}`,
method: 'PUT',
data: {
...workflow,
status: 'active',
...automation,
status: AutomationStatus.ACTIVE,
},
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data.status === WorkflowStatus.ACTIVE) {
if (data?.data.status === AutomationStatus.ACTIVE) {
void createNotice(
'success',
__('Well done! Automation is now activated!', 'mailpoet'),
@ -91,28 +130,33 @@ export function* activate() {
type: 'snackbar',
},
);
MailPoet.trackEvent('Automations > Automation activated');
}
return {
type: 'ACTIVATE',
workflow: data?.data ?? workflow,
automation: data?.data ?? automation,
} as const;
}
// @ToDo: Decide on best naming once MAILPOET-4731 decides about the "deactivating" status name
export function* deactivate() {
const workflow = select(storeName).getWorkflowData();
export function* deactivate(deactivateAutomationRuns = true) {
const automation = select(storeName).getAutomationData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
path: `/automations/${automation.id}`,
method: 'PUT',
data: {
...workflow,
status: 'inactive',
...automation,
status: deactivateAutomationRuns
? AutomationStatus.DRAFT
: AutomationStatus.DEACTIVATING,
},
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data.status === WorkflowStatus.INACTIVE) {
if (
deactivateAutomationRuns &&
data?.data.status === AutomationStatus.DRAFT
) {
void createNotice(
'success',
__('Automation is now deactivated!', 'mailpoet'),
@ -120,36 +164,58 @@ export function* deactivate() {
type: 'snackbar',
},
);
MailPoet.trackEvent('Automations > Automation deactivated', {
type: 'immediate',
});
}
if (
!deactivateAutomationRuns &&
data?.data.status === AutomationStatus.DEACTIVATING
) {
void createNotice(
'success',
__(
'Automation is deactivated. But recent users are still going through the flow.',
'mailpoet',
),
{
type: 'snackbar',
},
);
MailPoet.trackEvent('Automations > Automation deactivated', {
type: 'continuous',
});
}
return {
type: 'DEACTIVATE',
workflow: data?.data ?? workflow,
automation: data?.data ?? automation,
} as const;
}
export function* trash(onTrashed: () => void = undefined) {
const workflow = select(storeName).getWorkflowData();
const automation = select(storeName).getAutomationData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
path: `/automations/${automation.id}`,
method: 'PUT',
data: {
...workflow,
status: 'trash',
...automation,
status: AutomationStatus.TRASH,
},
});
onTrashed?.();
if (data?.status === 'trash') {
if (data?.status === AutomationStatus.TRASH) {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowDeleted]: workflow.id,
[LISTING_NOTICE_PARAMETERS.automationDeleted]: automation.id,
});
}
return {
type: 'TRASH',
workflow: data?.data ?? workflow,
automation: data?.data ?? automation,
} as const;
}
@ -170,6 +236,7 @@ export function updateStepArgs(stepId, name, value) {
}
export function setErrors(errors) {
trackErrors(errors);
return {
type: 'SET_ERRORS',
errors,

View File

@ -1,4 +1,4 @@
export const storeName = 'mailpoet/automation-editor';
export const workflowSidebarKey = 'mailpoet/automation-editor/workflow';
export const automationSidebarKey = 'mailpoet/automation-editor/automation';
export const stepSidebarKey = 'mailpoet/automation-editor/step';

View File

@ -5,12 +5,15 @@ declare let window: AutomationEditorWindow;
export const getInitialState = (): State => ({
context: { ...window.mailpoet_automation_context },
stepTypes: {},
workflowData: { ...window.mailpoet_automation_workflow },
workflowSaved: true,
automationData: { ...window.mailpoet_automation },
automationSaved: true,
selectedStep: undefined,
inserterSidebar: {
isOpened: false,
},
activationPanel: {
isOpened: false,
},
inserterPopover: undefined,
errors: undefined,
});

View File

@ -3,6 +3,14 @@ import { State } from './types';
export function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_ACTIVATION_PANEL_VISIBILITY':
return {
...state,
activationPanel: {
...state.activationPanel,
isOpened: action.value,
},
};
case 'TOGGLE_INSERTER_SIDEBAR':
return {
...state,
@ -21,35 +29,35 @@ export function reducer(state: State, action: Action): State {
...state,
selectedStep: action.value,
};
case 'UPDATE_WORKFLOW':
case 'UPDATE_AUTOMATION':
return {
...state,
workflowData: action.workflow,
workflowSaved: false,
automationData: action.automation,
automationSaved: false,
};
case 'SAVE':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
automationData: action.automation,
automationSaved: true,
};
case 'ACTIVATE':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
automationData: action.automation,
automationSaved: true,
};
case 'DEACTIVATE':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
automationData: action.automation,
automationSaved: true,
};
case 'TRASH':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
automationData: action.automation,
automationSaved: true,
};
case 'REGISTER_STEP_TYPE':
return {
@ -60,19 +68,21 @@ export function reducer(state: State, action: Action): State {
},
};
case 'UPDATE_STEP_ARGS': {
const prevArgs = state.workflowData.steps[action.stepId].args ?? {};
const prevArgs = state.automationData.steps[action.stepId].args ?? {};
const value =
typeof action.value === 'function'
? action.value(prevArgs[action.name] ?? undefined)
: action.value;
const args = {
...prevArgs,
[action.name]: value,
};
const args =
value === undefined
? Object.fromEntries(
Object.entries(prevArgs).filter(([name]) => name !== action.name),
)
: { ...prevArgs, [action.name]: value };
const step = { ...state.workflowData.steps[action.stepId], args };
const step = { ...state.automationData.steps[action.stepId], args };
const stepErrors = Object.values(state.errors?.steps ?? {}).filter(
({ step_id }) => step_id !== action.stepId,
@ -80,14 +90,14 @@ export function reducer(state: State, action: Action): State {
return {
...state,
workflowData: {
...state.workflowData,
automationData: {
...state.automationData,
steps: {
...state.workflowData.steps,
...state.automationData.steps,
[action.stepId]: step,
},
},
workflowSaved: false,
automationSaved: false,
selectedStep: step,
errors:
stepErrors.length > 0

View File

@ -2,9 +2,9 @@ import { createRegistrySelector } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface';
import { store as preferencesStore } from '@wordpress/preferences';
import { storeName } from './constants';
import { Context, Errors, Feature, State, StepError, StepType } from './types';
import { Context, Errors, Feature, State, StepErrors, StepType } from './types';
import { Item } from '../components/inserter/item';
import { Step, Workflow } from '../components/workflow/types';
import { Step, Automation } from '../components/automation/types';
export const isFeatureActive = createRegistrySelector(
(select) =>
@ -21,6 +21,10 @@ export function isInserterSidebarOpened(state: State): boolean {
return state.inserterSidebar.isOpened;
}
export function isActivationPanelOpened(state: State): boolean {
return state.activationPanel.isOpened;
}
export function getContext(state: State): Context {
return state.context;
}
@ -54,18 +58,22 @@ export function getInserterPopover(
return state.inserterPopover;
}
export function getWorkflowData(state: State): Workflow {
return state.workflowData;
export function getAutomationData(state: State): Automation {
return state.automationData;
}
export function getWorkflowSaved(state: State): boolean {
return state.workflowSaved;
export function getAutomationSaved(state: State): boolean {
return state.automationSaved;
}
export function getSelectedStep(state: State): Step | undefined {
return state.selectedStep;
}
export function getStepById(state: State, id: string): Step | undefined {
return state.automationData.steps[id] ?? undefined;
}
export function getStepType(state: State, key: string): StepType | undefined {
return state.stepTypes[key] ?? undefined;
}
@ -78,6 +86,6 @@ export function getErrors(state: State): Errors | undefined {
return state.errors;
}
export function getStepError(state: State, id: string): StepError | undefined {
export function getStepError(state: State, id: string): StepErrors | undefined {
return state.errors?.steps[id] ?? undefined;
}

View File

@ -1,9 +1,4 @@
import {
createReduxStore,
register,
StoreConfig,
StoreDescriptor,
} from '@wordpress/data';
import { createReduxStore, register, StoreDescriptor } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
import { Hooks } from 'wp-js-hooks';
import * as actions from './actions';
@ -13,6 +8,7 @@ import { reducer } from './reducer';
import * as selectors from './selectors';
import { State } from './types';
import { OmitFirstArgs } from '../../../types';
import { EditorStoreConfigType } from '../../types/filters';
type StoreType = Omit<StoreDescriptor, 'name'> & {
name: typeof storeName;
@ -28,8 +24,8 @@ export const createStore = (): StoreType => {
selectors,
reducer,
initialState: getInitialState(),
} as StoreConfig<State>,
) as StoreConfig<State>;
} as EditorStoreConfigType,
) as EditorStoreConfigType;
const store = createReduxStore<State>(storeName, storeConfig) as StoreType;
register(store);

View File

@ -1,9 +1,9 @@
import { ComponentType } from 'react';
import { Step, Workflow } from '../components/workflow/types';
import { Step, Automation } from '../components/automation/types';
export interface AutomationEditorWindow extends Window {
mailpoet_automation_context: Context;
mailpoet_automation_workflow: Workflow;
mailpoet_automation: Automation;
}
export type Context = {
@ -32,26 +32,31 @@ export type StepType = {
edit: ComponentType;
foreground: string;
background: string;
createStep?: (step: Step, state: State) => Step;
};
export type StepError = {
export type StepErrors = {
step_id: string;
message: string;
fields: Record<string, string>;
};
export type Errors = {
steps: Record<string, StepError>;
steps: Record<string, StepErrors>;
};
export type State = {
context: Context;
stepTypes: Record<string, StepType>;
workflowData: Workflow;
workflowSaved: boolean;
automationData: Automation;
automationSaved: boolean;
selectedStep: Step | undefined;
inserterSidebar: {
isOpened: boolean;
};
activationPanel: {
isOpened: boolean;
};
inserterPopover?: {
anchor: HTMLElement;
type: 'steps' | 'triggers';

View File

@ -0,0 +1,14 @@
import { getLocaleData, setLocaleData } from '@wordpress/i18n';
declare global {
interface Window {
wp: {
i18n: { getLocaleData: typeof getLocaleData };
};
}
}
// We are using "@wordpress/i18n" from our bundle while WordPress initializes
// translation data on the core one — we need to pass the data to our code.
export const registerTranslations = () =>
setLocaleData(window.wp.i18n.getLocaleData('mailpoet'), 'mailpoet');

View File

@ -12,25 +12,42 @@ import { storeName } from '../../../../editor/store';
import { DelayTypeOptions } from './types/delayTypes';
export function Edit(): JSX.Element {
const { selectedStep } = useSelect(
const { selectedStep, errors } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const errorFields = errors?.fields ?? {};
const delayErrorMessage = errorFields?.delay ?? '';
const delayTypeErrorMessage = errorFields?.delay_type ?? '';
const delayValueInputId = `delay-number-${selectedStep.id}`;
return (
<PanelBody opened>
<label htmlFor={delayValueInputId}>
<PlainBodyTitle title={__('Wait for', 'mailpoet')} />
<PlainBodyTitle
title={
// translators: A label for a wait delay time selection form field - time unit follows
__('Wait for', 'mailpoet')
}
/>
</label>
<Flex align="top">
<FlexItem style={{ flex: '1 1 0' }}>
<FlexItem
style={{ flex: '1 1 0' }}
className={
delayErrorMessage ? 'mailpoet-automation-field__error' : ''
}
>
<TextControl
id={delayValueInputId}
help={delayErrorMessage}
type="number"
placeholder="Number"
placeholder={__('Number', 'mailpoet')}
value={(selectedStep.args.delay as string) ?? ''}
onChange={(rawValue) => {
const value: number =
@ -45,9 +62,15 @@ export function Edit(): JSX.Element {
}}
/>
</FlexItem>
<FlexItem style={{ flex: '1 1 0' }}>
<FlexItem
style={{ flex: '1 1 0' }}
className={
delayTypeErrorMessage ? 'mailpoet-automation-field__error' : ''
}
>
<SelectControl
label=""
help={delayTypeErrorMessage}
value={(selectedStep.args.delay_type as string) ?? 'HOURS'}
options={DelayTypeOptions}
onChange={(value) =>

View File

@ -1,3 +1,4 @@
import { __, _x } from '@wordpress/i18n';
import { Icon } from './icon';
import { Edit } from './edit';
import { StepType } from '../../../../editor/store/types';
@ -14,13 +15,16 @@ const getDelayInformation = (delayTypeValue: string, value: number): string =>
export const step: StepType = {
key: 'core:delay',
group: 'actions',
title: 'Delay',
title: _x('Delay', 'noun', 'mailpoet'),
foreground: '#7F54B3',
background: '#f7edf7',
description: 'Wait some time before proceeding with the steps below',
description: __(
'Wait some time before proceeding with the steps below',
'mailpoet',
),
subtitle: (data): string => {
if (!data.args.delay || !data.args.delay_type) {
return 'Not set up yet.';
return __('Not set up yet.', 'mailpoet');
}
return getDelayInformation(

View File

@ -1,25 +1,44 @@
import { SelectControl } from '@wordpress/components';
import { __, _n, sprintf } from '@wordpress/i18n';
export type DelayTypes = SelectControl.Option & {
subtitle: (value: number) => string;
};
export const DelayTypeOptions: DelayTypes[] = [
{
label: 'Hours',
label: __('Minutes', 'mailpoet'),
subtitle: (value: number) =>
`Wait for ${value} ${value === 1 ? 'hour' : 'hours'}`,
sprintf(
_n('Wait for %d minute', 'Wait for %d minutes', value, 'mailpoet'),
value,
),
value: 'MINUTES',
},
{
label: __('Hours', 'mailpoet'),
subtitle: (value: number) =>
sprintf(
_n('Wait for %d hour', 'Wait for %d hours', value, 'mailpoet'),
value,
),
value: 'HOURS',
},
{
label: 'Days',
label: __('Days', 'mailpoet'),
subtitle: (value: number) =>
`Wait for ${value} ${value === 1 ? 'day' : 'days'}`,
sprintf(
_n('Wait for %d day', 'Wait for %d days', value, 'mailpoet'),
value,
),
value: 'DAYS',
},
{
label: 'Weeks',
label: __('Weeks', 'mailpoet'),
subtitle: (value: number) =>
`Wait for ${value} ${value === 1 ? 'week' : 'weeks'}`,
sprintf(
_n('Wait for %d week', 'Wait for %d weeks', value, 'mailpoet'),
value,
),
value: 'WEEKS',
},
];

View File

@ -2,9 +2,11 @@ import { registerStepType } from '../../editor/store';
import { step as SendEmailStep } from './steps/send_email';
import { step as SomeoneSubscribesTrigger } from './steps/someone-subscribes';
import { step as WpUserRegisteredTrigger } from './steps/wp-user-registered';
import { registerStepControls } from './step-controls';
export const initialize = (): void => {
registerStepType(SendEmailStep);
registerStepType(WpUserRegisteredTrigger);
registerStepType(SomeoneSubscribesTrigger);
registerStepControls();
};

View File

@ -0,0 +1,47 @@
import { __ } from '@wordpress/i18n';
import { chartBar } from '@wordpress/icons';
import { Hooks } from 'wp-js-hooks';
import { MoreControlType, StepMoreControlsType } from '../../../types/filters';
import { StepType } from '../../../editor/store';
import { Step } from '../../../editor/components/automation/types';
const emailStatisticsControl = (step: Step): MoreControlType => {
const hasEmail = step.args?.email_id > 0;
return {
key: 'statistics',
control: {
icon: chartBar,
title: __('Email statistics', 'mailpoet'),
isDisabled: !hasEmail,
onClick: () => {
window.open(
`admin.php?page=mailpoet-newsletters#/stats/${
step.args.email_id as string
}`,
'_blank',
);
},
},
slot: () => null,
};
};
export function registerStepControls() {
Hooks.addFilter(
'mailpoet.automation.step.more-controls',
'mailpoet',
(
controls: StepMoreControlsType,
step: Step,
stepType: StepType,
): StepMoreControlsType => {
if (stepType.key === 'mailpoet:send-email') {
return {
statistics: emailStatisticsControl(step),
...controls,
};
}
return controls;
},
);
}

View File

@ -1,25 +1,52 @@
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { plus } from '@wordpress/icons';
import { useCallback, useEffect, useState } from 'react';
import { Button } from '../../../components/button';
import { storeName } from '../../../../../editor/store';
import { MailPoet } from '../../../../../../mailpoet';
const emailPreviewLinkCache = {};
const retrievePreviewLink = async (emailId) => {
if (
emailPreviewLinkCache[emailId] &&
emailPreviewLinkCache[emailId].length > 0
) {
return emailPreviewLinkCache[emailId];
}
const response = await MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'get',
data: {
id: emailId,
},
});
emailPreviewLinkCache[emailId] = response?.meta?.preview_url ?? '';
return emailPreviewLinkCache[emailId];
};
export function EditNewsletter(): JSX.Element {
const [redirectToTemplateSelection, setRedirectToTemplateSelection] =
useState(false);
const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false);
const { selectedStep, workflowId, workflowSaved } = useSelect(
const { selectedStep, automationId, automationSaved, errors } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
workflowId: select(storeName).getWorkflowData().id,
workflowSaved: select(storeName).getWorkflowSaved(),
automationId: select(storeName).getAutomationData().id,
automationSaved: select(storeName).getAutomationSaved(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const emailId = selectedStep?.args?.email_id as number | undefined;
const workflowStepId = selectedStep.id;
const automationStepId = selectedStep.id;
const errorFields = errors?.fields ?? {};
const emailIdError = errorFields?.email_id ?? '';
const createEmail = useCallback(async () => {
setRedirectToTemplateSelection(true);
@ -31,41 +58,51 @@ export function EditNewsletter(): JSX.Element {
type: 'automation',
subject: '',
options: {
workflowId,
workflowStepId,
automationId,
automationStepId,
},
},
});
dispatch(storeName).updateStepArgs(
workflowStepId,
automationStepId,
'email_id',
parseInt(response.data.id as string, 10),
);
dispatch(storeName).save();
}, [workflowId, workflowStepId]);
}, [automationId, automationStepId]);
// This component is rendered only when no email ID is set. Once we have the ID
// and the workflow is saved, we can safely redirect to the email design flow.
// and the automation is saved, we can safely redirect to the email design flow.
useEffect(() => {
if (redirectToTemplateSelection && emailId && workflowSaved) {
if (redirectToTemplateSelection && emailId && automationSaved) {
window.location.href = `admin.php?page=mailpoet-newsletters#/template/${emailId}`;
}
}, [emailId, workflowSaved, redirectToTemplateSelection]);
}, [emailId, automationSaved, redirectToTemplateSelection]);
if (!emailId || redirectToTemplateSelection) {
return (
<Button
variant="sidebar-primary"
centered
icon={plus}
onClick={createEmail}
isBusy={redirectToTemplateSelection}
disabled={redirectToTemplateSelection}
>
Design email
</Button>
<div className={emailIdError ? 'mailpoet-automation-field__error' : ''}>
<Button
variant="sidebar-primary"
centered
icon={plus}
onClick={createEmail}
isBusy={redirectToTemplateSelection}
disabled={redirectToTemplateSelection}
>
{__('Design email', 'mailpoet')}
</Button>
{emailIdError && (
<span className="mailpoet-automation-field-message">
{__(
'You need to design an email before you can activate the automation',
'mailpoet',
)}
</span>
)}
</div>
);
}
@ -78,10 +115,21 @@ export function EditNewsletter(): JSX.Element {
selectedStep.args.email_id as string
}`}
>
Edit content
{__('Edit content', 'mailpoet')}
</Button>
<Button variant="secondary" centered>
Preview
<Button
variant="secondary"
centered
isBusy={fetchingPreviewLink}
disabled={fetchingPreviewLink}
onClick={async () => {
setFetchingPreviewLink(true);
const link = await retrievePreviewLink(emailId);
window.open(link as string, '_blank');
setFetchingPreviewLink(false);
}}
>
{__('Preview', 'mailpoet')}
</Button>
</div>
);

View File

@ -1,6 +1,7 @@
import { ComponentProps } from 'react';
import { PanelBody, TextareaControl, TextControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { ShortcodeHelpText } from './shortcode_help_text';
import { PlainBodyTitle } from '../../../../../editor/components/panel';
import { storeName } from '../../../../../editor/store';
@ -31,14 +32,21 @@ function SingleLineTextareaControl(
}
export function EmailPanel(): JSX.Element {
const { selectedStep, selectedStepType } = useSelect(
const { selectedStep, selectedStepType, errors } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
selectedStepType: select(storeName).getSelectedStepType(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const errorFields = errors?.fields ?? {};
const senderNameErrorMessage = errorFields?.sender_name ?? '';
const senderAddressErrorMessage = errorFields?.sender_address ?? '';
const subjectErrorMessage = errorFields?.subject ?? '';
return (
<PanelBody opened>
<StepName
@ -49,8 +57,15 @@ export function EmailPanel(): JSX.Element {
}}
/>
<TextControl
label="“From” name"
placeholder="John Doe"
className={
senderNameErrorMessage ? 'mailpoet-automation-field__error' : ''
}
help={senderNameErrorMessage}
label={__('"From" name', 'mailpoet')}
placeholder={
// translators: A placeholder for a person's name
__('John Doe', 'mailpoet')
}
value={(selectedStep.args.sender_name as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
@ -61,9 +76,16 @@ export function EmailPanel(): JSX.Element {
}
/>
<TextControl
className={
senderAddressErrorMessage ? 'mailpoet-automation-field__error' : ''
}
help={senderAddressErrorMessage}
type="email"
label="“From email address"
placeholder="you@domain.com"
label={__('"From" email address', 'mailpoet')}
placeholder={
// translators: A placeholder for an email
__('you@domain.com', 'mailpoet')
}
value={(selectedStep.args.sender_address as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
@ -74,17 +96,25 @@ export function EmailPanel(): JSX.Element {
}
/>
<SingleLineTextareaControl
label="Subject"
placeholder="Type in subject…"
className={
subjectErrorMessage ? 'mailpoet-automation-field__error' : ''
}
label={__('Subject', 'mailpoet')}
placeholder={__('Type in subject…', 'mailpoet')}
value={(selectedStep.args.subject as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(selectedStep.id, 'subject', value)
}
help={<ShortcodeHelpText />}
help={
<>
{`${subjectErrorMessage} `}
<ShortcodeHelpText />
</>
}
/>
<SingleLineTextareaControl
label="Preheader"
placeholder="Type in preheader…"
label={__('Preheader', 'mailpoet')}
placeholder={__('Type in preheader…', 'mailpoet')}
value={(selectedStep.args.preheader as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
@ -97,7 +127,7 @@ export function EmailPanel(): JSX.Element {
/>
<div className="mailpoet-automation-email-content-separator" />
<PlainBodyTitle title="Email" />
<PlainBodyTitle title={__('Email', 'mailpoet')} />
<EditNewsletter />
</PanelBody>
);

View File

@ -1,27 +1,36 @@
import { PanelBody, ToggleControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { useState } from 'react';
import { ToggleControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { PremiumModal } from 'common/premium_modal';
import { Hooks } from 'wp-js-hooks';
import { storeName } from '../../../../../editor/store';
import { GoogleAnalyticsPanelBodyType } from '../../../types/filters';
import { PanelBody } from '../../../../../editor/components/panel/panel-body';
export function GoogleAnalyticsPanel(): JSX.Element {
const { selectedStep } = useSelect(
(select) => ({ selectedStep: select(storeName).getSelectedStep() }),
const { selectedStep, errors } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
)?.fields?.ga_campaign,
}),
[],
);
const enabled = typeof selectedStep.args?.ga_campaign !== 'undefined';
const panelBody = Hooks.applyFilters(
const { updateStepArgs } = useDispatch(storeName);
const hasValue = typeof selectedStep.args?.ga_campaign !== 'undefined';
const [enabled, setEnabled] = useState(hasValue);
const panelBody: GoogleAnalyticsPanelBodyType = Hooks.applyFilters(
'mailpoet.automation.send_email.google_analytics_panel',
<PremiumModal
onRequestClose={() =>
dispatch(storeName).updateStepArgs(
selectedStep.id,
'ga_campaign',
undefined,
)
}
onRequestClose={() => {
setEnabled(false);
updateStepArgs(selectedStep.id, 'ga_campaign', undefined);
}}
>
{__(
'Google Analytics tracking is not available in the free version of the MailPoet plugin.',
@ -31,17 +40,20 @@ export function GoogleAnalyticsPanel(): JSX.Element {
);
return (
<PanelBody title="Google analytics" initialOpen={false}>
<PanelBody
title={__('Google Analytics', 'mailpoet')}
initialOpen={false}
hasErrors={!!errors}
>
<ToggleControl
label="Enable custom GA tracking"
label={__('Enable custom GA tracking', 'mailpoet')}
checked={enabled}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
selectedStep.id,
'ga_campaign',
value ? '' : undefined,
)
}
onChange={(value) => {
setEnabled(value);
if (!value) {
updateStepArgs(selectedStep.id, 'ga_campaign', undefined);
}
}}
/>
{enabled && panelBody}

View File

@ -1,67 +1,113 @@
import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { useRef, useState } from 'react';
import { TextControl, ToggleControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../../../../editor/store';
import { PanelBody } from '../../../../../editor/components/panel/panel-body';
type ReplyToArgs = {
reply_to_name?: string;
reply_to_address?: string;
};
export function ReplyToPanel(): JSX.Element {
const { selectedStep } = useSelect(
const { context, selectedStep, errors } = useSelect(
(select) => ({
context: select(storeName).getContext(),
selectedStep: select(storeName).getSelectedStep(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const replyToName = selectedStep.args.reply_to_name as string | undefined;
const replyToAddress = selectedStep.args.reply_to_address as
| string
| undefined;
const { updateStepArgs } = useDispatch(storeName);
const enabled =
typeof replyToName !== 'undefined' || typeof replyToAddress !== 'undefined';
const args = selectedStep.args as ReplyToArgs;
const hasValue = !!args.reply_to_name || !!args.reply_to_address;
const [expanded, setExpanded] = useState(hasValue);
const prevValue = useRef<{ name?: string; address?: string }>();
// defaults
const argsContext =
context.steps['mailpoet:send-email']?.args_schema?.properties ?? {};
const defaultName = argsContext.reply_to_name?.default;
const defaultAddress = argsContext.reply_to_address?.default;
const errorFields = errors?.fields ?? {};
const replyToNameError = errorFields?.reply_to_name ?? '';
const replyToAddressError = errorFields?.reply_to_address ?? '';
return (
<PanelBody title="Reply to" initialOpen={false}>
<PanelBody
title={__('Reply to', 'mailpoet')}
initialOpen={false}
hasErrors={!!replyToNameError || !!replyToAddressError}
>
<ToggleControl
label="Use different email address for getting replies to the email"
checked={enabled}
label={__(
'Use different email address for getting replies to the email',
'mailpoet',
)}
checked={expanded}
onChange={(value) => {
dispatch(storeName).updateStepArgs(
selectedStep.id,
'reply_to_name',
value ? '' : undefined,
);
dispatch(storeName).updateStepArgs(
selectedStep.id,
'reply_to_address',
value ? '' : undefined,
);
setExpanded(value);
const stepId = selectedStep.id;
if (value) {
const name = prevValue.current?.name ?? defaultName;
const address = prevValue.current?.address ?? defaultAddress;
updateStepArgs(stepId, 'reply_to_name', name);
updateStepArgs(stepId, 'reply_to_address', address);
} else {
prevValue.current = {
name: args.reply_to_name,
address: args.reply_to_address,
};
updateStepArgs(stepId, 'reply_to_name', undefined);
updateStepArgs(stepId, 'reply_to_address', undefined);
}
}}
/>
{enabled && (
{expanded && (
<>
<TextControl
label="“Reply to” name"
placeholder="John Doe"
value={replyToName ?? ''}
className={
replyToNameError ? 'mailpoet-automation-field__error' : ''
}
help={replyToNameError}
label={__('"Reply to" name', 'mailpoet')}
placeholder={
// translators: A placeholder for a person's name
__('John Doe', 'mailpoet')
}
value={args.reply_to_name ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
updateStepArgs(
selectedStep.id,
'reply_to_name',
value,
value || undefined,
)
}
/>
<TextControl
className={
replyToAddressError ? 'mailpoet-automation-field__error' : ''
}
help={replyToAddressError}
type="email"
label="“Reply to email address"
placeholder="you@domain.com"
value={replyToAddress ?? ''}
label={__('"Reply to" email address', 'mailpoet')}
placeholder={
// translators: A placeholder for an email
__('you@domain.com', 'mailpoet')
}
value={args.reply_to_address ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
updateStepArgs(
selectedStep.id,
'reply_to_address',
value,
value || undefined,
)
}
/>

View File

@ -1,3 +1,5 @@
import { __ } from '@wordpress/i18n';
export function ShortcodeHelpText(): JSX.Element {
return (
<span className="mailpoet-shortcode-selector">
@ -6,9 +8,8 @@ export function ShortcodeHelpText(): JSX.Element {
href="https://kb.mailpoet.com/article/215-personalize-newsletter-with-shortcodes"
target="_blank"
rel="noopener noreferrer"
data-beacon-article="59d662ef042863379ddc6faa"
>
MailPoet shortcodes
{__('MailPoet shortcodes', 'mailpoet')}
</a>
</span>
);

View File

@ -1,5 +1,6 @@
import { ComponentProps, ComponentType, useEffect, useState } from 'react';
import { Spinner as WpSpinner } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { MailPoetAjax } from '../../../../../../ajax';
// @types/wordpress__components don't define "className", which is supported
@ -39,7 +40,7 @@ export function Thumbnail({ emailId }: Props): JSX.Element {
<img
className="mailpoet-automation-thumbnail-image"
src={thumbnailUrl}
alt="Email thumbnail"
alt={__('Email thumbnail', 'mailpoet')}
/>
</div>
) : (

View File

@ -1,15 +1,25 @@
import { __ } from '@wordpress/i18n';
import { Hooks } from 'wp-js-hooks';
import { Icon } from './icon';
import { Edit } from './edit';
import { StepType } from '../../../../editor/store/types';
import { State, StepType } from '../../../../editor/store/types';
import { Step } from '../../../../editor/components/automation/types';
export const step: StepType = {
key: 'mailpoet:send-email',
group: 'actions',
title: 'Send email',
description: 'An email will be sent to subscriber',
subtitle: (data) => (data.args.name as string) ?? 'Send email',
title: __('Send email', 'mailpoet'),
description: __('An email will be sent to subscriber', 'mailpoet'),
subtitle: (data) =>
(data.args.name as string) ?? __('Send email', 'mailpoet'),
foreground: '#996800',
background: '#FCF9E8',
icon: Icon,
edit: Edit,
createStep: (stepData: Step, state: State) =>
Hooks.applyFilters(
'mailpoet.automation.send_email.create_step',
stepData,
state.automationData.id,
),
} as const;

View File

@ -30,7 +30,7 @@ export function ListPanel(): JSX.Element {
<FormTokenField
label={__(
'When someone subscribers to the following list(s):',
'When someone subscribes to the following lists:',
'mailpoet',
)}
placeholder={__('Any list', 'mailpoet')}

View File

@ -1,4 +1,4 @@
import { __ } from '@wordpress/i18n';
import { __, _x } from '@wordpress/i18n';
import { commentAuthorAvatar } from '@wordpress/icons';
import { StepType } from '../../../../editor/store';
import { Edit } from './edit';
@ -13,7 +13,7 @@ export const step: StepType = {
'Starts the automation when a new subscriber is added to MailPoet.',
'mailpoet',
),
subtitle: () => __('Trigger', 'mailpoet'),
subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
icon: () => (
<div style={{ width: '100%', height: '100%', scale: '1.4' }}>
{commentAuthorAvatar}

View File

@ -1,11 +1,31 @@
import { PanelBody } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import ReactStringReplace from 'react-string-replace';
import { storeName } from '../../../../../editor/store';
import { PlainBodyTitle } from '../../../../../editor/components/panel';
import { userRoles } from './role';
import { FormTokenField } from '../../../components/form-token-field';
function SettingsInfoText(): JSX.Element {
return (
<p>
{ReactStringReplace(
__(
'[link]Subscribe in registration form[/link] setting must be enabled.',
'mailpoet',
),
/\[link\](.*?)\[\/link\]/g,
(match) => (
<a href="admin.php?page=mailpoet-settings#/basics" target="_blank">
{match}
</a>
),
)}
</p>
);
}
export function RolePanel(): JSX.Element {
const { selectedStep } = useSelect(
(select) => ({
@ -20,9 +40,11 @@ export function RolePanel(): JSX.Element {
const selected = userRoles.filter((role): boolean =>
rawSelected.includes(role.id as string),
);
return (
<PanelBody opened>
<PlainBodyTitle title={__('Trigger settings', 'mailpoet')} />
<SettingsInfoText />
<FormTokenField
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

@ -1,4 +1,4 @@
import { __ } from '@wordpress/i18n';
import { __, _x } from '@wordpress/i18n';
import { wordpress } from '@wordpress/icons';
import { StepType } from '../../../../editor/store';
import { Edit } from './edit';
@ -13,7 +13,7 @@ export const step: StepType = {
'Starts the automation when a new user registered in WordPress.',
'mailpoet',
),
subtitle: () => __('Trigger', 'mailpoet'),
subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
icon: () => (
<div style={{ width: '100%', height: '100%', scale: '1.12' }}>
{wordpress}

View File

@ -0,0 +1,14 @@
/**
* The types in this file document the expected return types of specific
* filters.
*/
import { Step } from '../../../editor/components/automation/types';
// mailpoet.automation.send_email.create_step
export type SendEmailCreateStepType = (
step: Step,
automationId: number,
) => Step;
// mailpoet.automation.send_email.google_analytics_panel
export type GoogleAnalyticsPanelBodyType = JSX.Element;

View File

@ -3,27 +3,30 @@ import { __ } from '@wordpress/i18n';
import { Notice } from '../../notices/notice';
export const LISTING_NOTICE_PARAMETERS = {
workflowHadBeenDeleted: 'mailpoet-had-been-deleted',
workflowDeleted: 'mailpoet-workflow-deleted',
automationHadBeenDeleted: 'mailpoet-had-been-deleted',
automationDeleted: 'mailpoet-automation-deleted',
};
export function WorkflowListingNotices(): JSX.Element {
const workflowHadBeenDeleted = parseInt(
export function AutomationListingNotices(): JSX.Element {
const automationHadBeenDeleted = parseInt(
getQueryArg(
window.location.href,
LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted,
LISTING_NOTICE_PARAMETERS.automationHadBeenDeleted,
) as string,
10,
);
const workflowDeleted = parseInt(
const automationDeleted = parseInt(
getQueryArg(
window.location.href,
LISTING_NOTICE_PARAMETERS.workflowDeleted,
LISTING_NOTICE_PARAMETERS.automationDeleted,
) as string,
10,
);
if (Number.isNaN(workflowHadBeenDeleted) && Number.isNaN(workflowDeleted)) {
if (
Number.isNaN(automationHadBeenDeleted) &&
Number.isNaN(automationDeleted)
) {
return null;
}
@ -32,7 +35,7 @@ export function WorkflowListingNotices(): JSX.Element {
...Object.values(LISTING_NOTICE_PARAMETERS),
);
window.history.pushState('', '', urlWithoutNotices);
if (workflowHadBeenDeleted) {
if (automationHadBeenDeleted) {
return (
<Notice type="error" closable timeout={false}>
<p>
@ -44,10 +47,10 @@ export function WorkflowListingNotices(): JSX.Element {
</Notice>
);
}
if (workflowDeleted) {
if (automationDeleted) {
return (
<Notice type="success" closable timeout={false}>
<p>{__('1 workflow moved to the Trash.', 'mailpoet')}</p>
<p>{__('1 automation moved to the Trash.', 'mailpoet')}</p>
</Notice>
);
}

View File

@ -1,16 +1,14 @@
export enum WorkflowStatus {
export enum AutomationStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
DRAFT = 'draft',
TRASH = 'trash',
// @ToDo: Needs to be aligned with MAILPOET-4731
DEACTIVATING = 'deactivating',
}
export type Workflow = {
export type Automation = {
id: number;
name: string;
status: WorkflowStatus;
status: AutomationStatus;
stats: {
totals: {
entered: number;

View File

@ -1,19 +1,19 @@
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import { Workflow } from '../../workflow';
import { Automation } from '../../automation';
import { MailPoet } from '../../../../mailpoet';
type Props = {
workflow: Workflow;
automation: Automation;
label?: string;
};
export function EditWorkflow({ workflow, label }: Props): JSX.Element {
export function EditAutomation({ automation, label }: Props): JSX.Element {
return (
<Button
variant="link"
href={addQueryArgs(MailPoet.urls.automationEditor, { id: workflow.id })}
href={addQueryArgs(MailPoet.urls.automationEditor, { id: automation.id })}
>
{label ?? __('Edit', 'mailpoet')}
</Button>

View File

@ -1,2 +1,2 @@
export * from './edit-workflow';
export * from './edit-automation';
export * from './undo-trash';

View File

@ -2,23 +2,23 @@ import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../store/constants';
import { Workflow, WorkflowStatus } from '../../workflow';
import { Automation, AutomationStatus } from '../../automation';
type Props = {
workflow: Workflow;
previousStatus: WorkflowStatus;
automation: Automation;
previousStatus: AutomationStatus;
};
export function UndoTrashButton({
workflow,
automation,
previousStatus,
}: Props): JSX.Element {
const { restoreWorkflow } = useDispatch(storeName);
const { restoreAutomation } = useDispatch(storeName);
return (
<Button
variant="link"
onClick={() => restoreWorkflow(workflow, previousStatus)}
onClick={() => restoreAutomation(automation, previousStatus)}
>
{__('Undo', 'mailpoet')}
</Button>

View File

@ -2,32 +2,26 @@ import { Fragment } from 'react';
import { __ } from '@wordpress/i18n';
import { DropdownMenu } from '@wordpress/components';
import { moreVertical } from '@wordpress/icons';
import {
useDeleteButton,
useDuplicateButton,
useRestoreButton,
useTrashButton,
} from '../menu';
import { Workflow } from '../../workflow';
import { EditWorkflow } from '../actions';
import { useDeleteButton, useRestoreButton, useTrashButton } from '../menu';
import { Automation } from '../../automation';
import { EditAutomation } from '../actions';
type Props = {
workflow: Workflow;
automation: Automation;
};
export function Actions({ workflow }: Props): JSX.Element {
export function Actions({ automation }: Props): JSX.Element {
// Menu items are using custom hooks because the "DropdownMenu" component uses the "controls"
// attribute rather than child components, but we need to render modal confirmation dialogs.
const duplicate = useDuplicateButton(workflow);
const trash = useTrashButton(workflow);
const restore = useRestoreButton(workflow);
const del = useDeleteButton(workflow);
const trash = useTrashButton(automation);
const restore = useRestoreButton(automation);
const del = useDeleteButton(automation);
const menuItems = [duplicate, trash, restore, del].filter((item) => item);
const menuItems = [trash, restore, del].filter((item) => item);
return (
<div className="mailpoet-automation-listing-cell-actions">
<EditWorkflow workflow={workflow} />
<EditAutomation automation={automation} />
{menuItems.map(({ control, slot }) => (
<Fragment key={control.title}>{slot}</Fragment>
))}

View File

@ -1,10 +1,10 @@
import { EditWorkflow } from '../actions';
import { Workflow } from '../../workflow';
import { EditAutomation } from '../actions';
import { Automation } from '../../automation';
type Props = {
workflow: Workflow;
automation: Automation;
};
export function Name({ workflow }: Props): JSX.Element {
return <EditWorkflow workflow={workflow} label={workflow.name} />;
export function Name({ automation }: Props): JSX.Element {
return <EditAutomation automation={automation} label={automation.name} />;
}

View File

@ -1,16 +1,16 @@
import { __ } from '@wordpress/i18n';
import { Workflow, WorkflowStatus } from '../../workflow';
import { Automation, AutomationStatus } from '../../automation';
type Props = {
workflow: Workflow;
automation: Automation;
};
export function Status({ workflow }: Props): JSX.Element {
export function Status({ automation }: Props): JSX.Element {
return (
<div className="mailpoet-automation-listing-cell-status">
{workflow.status === WorkflowStatus.ACTIVE
{automation.status === AutomationStatus.ACTIVE
? __('Active', 'mailpoet')
: __('Not active', 'mailpoet')}
: __('Draft', 'mailpoet')}
</div>
);
}

View File

@ -1,30 +1,33 @@
import { __ } from '@wordpress/i18n';
import { Workflow } from '../../workflow';
import { _x } from '@wordpress/i18n';
import { Automation } from '../../automation';
import { Statistics } from '../../../components/statistics';
type Props = {
workflow: Workflow;
automation: Automation;
};
export function Subscribers({ workflow }: Props): JSX.Element {
export function Subscribers({ automation }: Props): JSX.Element {
return (
<Statistics
labelPosition="after"
items={[
{
key: 'entered',
label: __('Entered', 'mailpoet'),
value: workflow.stats.totals.entered,
// translators: Total number of subscribers who entered an automation
label: _x('Entered', 'automation stats', 'mailpoet'),
value: automation.stats.totals.entered,
},
{
key: 'processing',
label: __('Processing', 'mailpoet'),
value: workflow.stats.totals.in_progress,
// translators: Total number of subscribers who are being processed in an automation
label: _x('Processing', 'automation stats', 'mailpoet'),
value: automation.stats.totals.in_progress,
},
{
key: 'exited',
label: __('Exited', 'mailpoet'),
value: workflow.stats.totals.exited,
// translators: Total number of subscribers who exited an automation, no matter the result
label: _x('Exited', 'automation stats', 'mailpoet'),
value: automation.stats.totals.exited,
},
]}
/>

View File

@ -4,13 +4,13 @@ import { useDispatch } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
import { Automation, AutomationStatus } from '../../automation';
export const useDeleteButton = (workflow: Workflow): Item | undefined => {
export const useDeleteButton = (automation: Automation): Item | undefined => {
const [showDialog, setShowDialog] = useState(false);
const { deleteWorkflow } = useDispatch(storeName);
const { deleteAutomation } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
if (automation.status !== AutomationStatus.TRASH) {
return undefined;
}
@ -27,16 +27,16 @@ export const useDeleteButton = (workflow: Workflow): Item | undefined => {
title={__('Permanently delete automation', 'mailpoet')}
confirmButtonText={__('Yes, permanently delete', 'mailpoet')}
__experimentalHideHeader={false}
onConfirm={() => deleteWorkflow(workflow)}
onConfirm={() => deleteAutomation(automation)}
onCancel={() => setShowDialog(false)}
>
{sprintf(
// translators: %s is the workflow name
// translators: %s is the automation name
__(
'Are you sure you want to permanently delete "%s" and all associated data? This cannot be undone!',
'mailpoet',
),
workflow.name,
automation.name,
)}
</ConfirmDialog>
),

View File

@ -2,12 +2,14 @@ import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
import { Automation, AutomationStatus } from '../../automation';
export const useDuplicateButton = (workflow: Workflow): Item | undefined => {
const { duplicateWorkflow } = useDispatch(storeName);
export const useDuplicateButton = (
automation: Automation,
): Item | undefined => {
const { duplicateAutomation } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) {
if (automation.status === AutomationStatus.TRASH) {
return undefined;
}
@ -16,7 +18,7 @@ export const useDuplicateButton = (workflow: Workflow): Item | undefined => {
control: {
title: __('Duplicate', 'mailpoet'),
icon: null,
onClick: () => duplicateWorkflow(workflow),
onClick: () => duplicateAutomation(automation),
},
};
};

View File

@ -2,12 +2,12 @@ import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
import { Automation, AutomationStatus } from '../../automation';
export const useRestoreButton = (workflow: Workflow): Item | undefined => {
const { restoreWorkflow } = useDispatch(storeName);
export const useRestoreButton = (automation: Automation): Item | undefined => {
const { restoreAutomation } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
if (automation.status !== AutomationStatus.TRASH) {
return undefined;
}
@ -16,7 +16,7 @@ export const useRestoreButton = (workflow: Workflow): Item | undefined => {
control: {
title: __('Restore', 'mailpoet'),
icon: null,
onClick: () => restoreWorkflow(workflow, WorkflowStatus.DRAFT),
onClick: () => restoreAutomation(automation, AutomationStatus.DRAFT),
},
};
};

View File

@ -1,42 +1,42 @@
import { useState } from 'react';
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { __, _x, sprintf } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
import { Automation, AutomationStatus } from '../../automation';
export const useTrashButton = (workflow: Workflow): Item | undefined => {
export const useTrashButton = (automation: Automation): Item | undefined => {
const [showDialog, setShowDialog] = useState(false);
const { trashWorkflow } = useDispatch(storeName);
const { trashAutomation } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) {
if (automation.status === AutomationStatus.TRASH) {
return undefined;
}
return {
key: 'trash',
control: {
title: __('Trash', 'mailpoet'),
title: _x('Trash', 'verb', 'mailpoet'),
icon: null,
onClick: () => setShowDialog(true),
},
slot: (
<ConfirmDialog
isOpen={showDialog}
title={__('Trash workflow', 'mailpoet')}
title={__('Trash automation', 'mailpoet')}
confirmButtonText={__('Yes, move to trash', 'mailpoet')}
__experimentalHideHeader={false}
onConfirm={() => trashWorkflow(workflow)}
onConfirm={() => trashAutomation(automation)}
onCancel={() => setShowDialog(false)}
>
{sprintf(
// translators: %s is the workflow name
// translators: %s is the automation name
__(
'Are you sure you want to move the workflow "%s" to the Trash?',
'Are you sure you want to move the automation "%s" to the Trash?',
'mailpoet',
),
workflow.name,
automation.name,
)}
</ConfirmDialog>
),

View File

@ -1,27 +1,27 @@
import { Workflow } from './workflow';
import { Automation } from './automation';
import { Actions, Name, Status, Subscribers } from './components/cells';
export function getRow(workflow: Workflow): object[] {
export function getRow(automation: Automation): object[] {
return [
{
id: workflow.id,
value: workflow.name,
display: <Name workflow={workflow} />,
id: automation.id,
value: automation.name,
display: <Name automation={automation} />,
},
{
id: workflow.id,
id: automation.id,
value: null,
display: <Subscribers workflow={workflow} />,
display: <Subscribers automation={automation} />,
},
{
id: workflow.id,
value: workflow.status,
display: <Status workflow={workflow} />,
id: automation.id,
value: automation.status,
display: <Status automation={automation} />,
},
{
id: workflow.id,
id: automation.id,
value: null,
display: <Actions workflow={workflow} />,
display: <Actions automation={automation} />,
},
];
}

View File

@ -1,48 +1,65 @@
import { Search, TableCard } from '@woocommerce/components/build';
import { TabPanel } from '@wordpress/components';
import { TableCard } from '@woocommerce/components/build';
import { Button, Flex, TabPanel } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { __, _x } from '@wordpress/i18n';
import { useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { plusIcon } from 'common/button/icon/plus';
import { getRow } from './get-row';
import { storeName } from './store/constants';
import { Workflow, WorkflowStatus } from './workflow';
import { storeName } from './store';
import { Automation, AutomationStatus } from './automation';
import { MailPoet } from '../../mailpoet';
const tabConfig = [
{
name: 'all',
title: 'All',
title: __('All', 'mailpoet'),
className: 'mailpoet-tab-all',
},
{
name: WorkflowStatus.ACTIVE,
title: 'Active',
name: AutomationStatus.ACTIVE,
title: __('Active', 'mailpoet'),
className: 'mailpoet-tab-active',
},
{
name: WorkflowStatus.INACTIVE,
title: 'Inactive',
className: 'mailpoet-tab-inactive',
},
{
name: WorkflowStatus.DRAFT,
title: 'Draft',
name: AutomationStatus.DRAFT,
title: _x('Draft', 'noun', 'mailpoet'),
className: 'mailpoet-tab-draft',
},
{
name: WorkflowStatus.TRASH,
title: 'Trash',
name: AutomationStatus.TRASH,
title: _x('Trash', 'noun', 'mailpoet'),
className: 'mailpoet-tab-trash',
},
] as const;
const tableHeaders = [
{ key: 'name', label: __('Name', 'mailpoet') },
{
key: 'name',
label: __('Name', 'mailpoet'),
cellClassName: 'mailpoet-automation-listing-cell-name',
},
{ key: 'subscribers', label: __('Subscribers', 'mailpoet') },
{ key: 'status', label: __('Status', 'mailpoet') },
{ key: 'actions' },
] as const;
export function AutomationListingHeader(): JSX.Element {
return (
<Flex className="mailpoet-automation-listing-heading">
<h1 className="wp-heading-inline">{__('Automations', 'mailpoet')}</h1>
<Button
href={MailPoet.urls.automationTemplates}
icon={plusIcon}
variant="primary"
className="mailpoet-add-new-button"
>
{__('New automation', 'mailpoet')}
</Button>
</Flex>
);
}
export function AutomationListing(): JSX.Element {
const history = useHistory();
const location = useLocation();
@ -51,14 +68,14 @@ export function AutomationListing(): JSX.Element {
[location],
);
const workflows = useSelect((select) => select(storeName).getWorkflows());
const { loadWorkflows } = useDispatch(storeName);
const automations = useSelect((select) => select(storeName).getAutomations());
const { loadAutomations } = useDispatch(storeName);
const status = pageSearch.get('status');
useEffect(() => {
loadWorkflows();
}, [loadWorkflows]);
loadAutomations();
}, [loadAutomations]);
// focus tab button on status change (needed due to the force re-mount below)
useLayoutEffect(() => {
@ -86,24 +103,24 @@ export function AutomationListing(): JSX.Element {
[pageSearch, history],
);
const groupedWorkflows = useMemo<Record<string, Workflow[]>>(() => {
const groupedAutomations = useMemo<Record<string, Automation[]>>(() => {
const grouped = { all: [] };
(workflows ?? []).forEach((workflow) => {
if (!grouped[workflow.status]) {
grouped[workflow.status] = [];
(automations ?? []).forEach((automation) => {
if (!grouped[automation.status]) {
grouped[automation.status] = [];
}
grouped[workflow.status].push(workflow);
if (workflow.status !== WorkflowStatus.TRASH) {
grouped.all.push(workflow);
grouped[automation.status].push(automation);
if (automation.status !== AutomationStatus.TRASH) {
grouped.all.push(automation);
}
});
return grouped;
}, [workflows]);
}, [automations]);
const tabs = useMemo(
() =>
tabConfig.map((tab) => {
const count = (groupedWorkflows[tab.name] ?? []).length;
const count = (groupedAutomations[tab.name] ?? []).length;
return {
name: tab.name,
title: (
@ -115,50 +132,39 @@ export function AutomationListing(): JSX.Element {
className: tab.className,
};
}),
[groupedWorkflows],
[groupedAutomations],
);
const renderTabs = useCallback(
(tab) => {
const filteredWorkflows: Workflow[] = groupedWorkflows[tab.name] ?? [];
const filteredAutomations: Automation[] =
groupedAutomations[tab.name] ?? [];
const rowsPerPage = parseInt(pageSearch.get('per_page') ?? '25', 10);
const currentPage = parseInt(pageSearch.get('paged') ?? '1', 10);
const start = (currentPage - 1) * rowsPerPage;
const rows = filteredWorkflows
.map((workflow) => getRow(workflow))
const rows = filteredAutomations
.map((automation) => getRow(automation))
.slice(start, start + rowsPerPage);
return (
<TableCard
className="mailpoet-automation-listing"
title=""
isLoading={!workflows}
isLoading={!automations}
headers={tableHeaders}
rows={rows}
rowKey={(_, i) => filteredWorkflows[i].id}
rowKey={(_, i) => filteredAutomations[i].id}
rowsPerPage={rowsPerPage}
onQueryChange={(key) => (value) => {
updateUrlSearchString({ [key]: value });
}}
totalRows={filteredWorkflows.length}
totalRows={filteredAutomations.length}
query={Object.fromEntries(pageSearch)}
hasSearch
showMenu={false}
actions={[
<Search
className="mailpoet-automation-listing-search"
allowFreeTextSearch
inlineTags
key="search"
type="custom"
disabled={!workflows}
autocompleter={{}}
/>,
]}
/>
);
},
[workflows, groupedWorkflows, pageSearch, updateUrlSearchString],
[automations, groupedAutomations, pageSearch, updateUrlSearchString],
);
return (

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