Compare commits

...

184 Commits

Author SHA1 Message Date
1fa19974cf Release 3.102.1 2022-11-03 17:41:02 +03:00
0e2e2d50de 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 17:41:02 +03:00
4f16f246b8 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 17:41:02 +03:00
9ca1cabb67 Update php-stubs/woocommerce-stubs to the latest version
[MAILPOET-4774]
2022-11-03 17:41:02 +03:00
a990250072 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 17:41:02 +03:00
5fbe658920 Prefix polyfilled normalizer functions
[MAILPOET-4770]
2022-11-03 17:41:02 +03: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
bbdbf6a52d Fix WooCommerce setup in the welcome wizard
The component that requires the global mailpoet_show_customers_import
variable is used also on the wizard page.
This commit ensures that it is set on the page.
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
56ed9f4ece Update acceptance tests for Woo setup page
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
e6188f5cef Update WooCommerce cookie tracking info in Wizard
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
a37e5bbe74 Hide WooCommerce initial import from wizard when there are no customers
[MAILPOET-4672]
2022-10-24 15:16:04 +02:00
9df99b1a00 Add method for getting customers count to WooCommerce helper
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
5ff543e62d Unify default values of mailpoet_subscribe_old_woocommerce_customers
The value is used in case the setting is not defined. Normally this shouldn't
happen because we set the value in the wizard.
The default value used in PHP is false, but in settings in admin was true.
See b0aacdd4ef/mailpoet/lib/Segments/WooCommerce.php (L528)
This commit unifies it.

[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
8f14c5ac53 Fix issue caused by merge conflict
[MAILPOET-4591]
2022-10-24 14:05:41 +02:00
4e0aa3c56d Remove redundant code which was selectively disabling validation
We no longer need selective validation disabling since we're
disabling all validations when saving a draft

[MAILPOET-4591]
2022-10-24 14:05:41 +02:00
545c6bfa5e Disable all validations when saving a draft
[MAILPOET-4591]
2022-10-24 14:05:41 +02:00
cd3652eaa6 Fix canceling multiple automatic emails
When we deleted sending queue using SQL it remained in the entity manager
and subsequent flush (not the first one) triggered the error, because it didn't know the ScheduledTask entity attached
to the orphaned SendingQueue entity.

This commit fixes this by refactoring deletion of sending queue using standard repository method.
After fixing the issue for sending queue there was another issue with SchedulesTaskSubscriberEntity that remained in memory.
I fixed that by detaching those. Theoretically there might be many SchedulesTaskSubscriberEntities for an Automatic email so
I consider still safer to delete using SQL and if there are some loaded (in this case there is one) detach them.
[MAILPOET-4741]
2022-10-24 14:03:54 +02:00
655641737b Do not use toggleModal()
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
152794720a Add DeactivateButton and logic
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
cb294fb303 Style DeactivateModal component
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
98744a53c1 Add Deactivate Modal
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
430fcc20a8 Add new store action to deactivate a workflow
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
1df78f22d3 Make the message better
[MAILPOET-4740]
2022-10-24 10:18:28 +02:00
2f823f5606 On settings page, Remove the dependency between loading 3rd-party libraries and tracking (data sharing). Allow user choose toggle values independently
MAILPOET-4657
2022-10-20 10:49:48 +02:00
086d6dce7e On WelcomeWizardUsageTrackingStep, Remove the dependency between loading 3rd-party libraries and tracking (data sharing). Allow user choose toggle values independently
MAILPOET-4657
2022-10-20 10:49:48 +02:00
879cca9fb3 On WelcomeWizardUsageTrackingStep, update section wordings and re-order items
MAILPOET-4657
2022-10-20 10:49:48 +02:00
3434fbe3b5 Add not-allowed cursor to disabled form fields
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
d4f9ccca5e Add disabled class to the wrapper of a disabled field
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
4a74c3e6fd Open premium modal if the premium plugin is not active
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
60d9a3f1ac Add posibility to add wrapper click handler to form fields
In some cases we need to disable the form field but add a click
handler to the field, but to make it work accross browsers the
click handler should get added to the wrapper and not the disabled
form field itself

[PREMIUM-200]
2022-10-20 10:30:30 +02:00
b64fbbdb7f Remove ga field default value when disabled
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
12e8d44a43 Disable ga field when premium plugin is not active
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
667658ae2f Show 'is now activated' snackbar only when status of workflow is active
[MAILPOET-4737]
2022-10-20 10:24:06 +02:00
9d2624163c Close ActivatePanel when there are errors
[MAILPOET-4737]
2022-10-20 10:24:06 +02:00
8f6688eba7 Check for CircleCI results file existence 2022-10-20 09:31:58 +02:00
b360d9a2cf Set focus on new activate button
[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
11384bbf6a Fix bug where the type of $step was Data\Step instead of Integration\Step
[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
6235944442 Fix bug where an empty array would generate a malformed SQL query
[MAILPOET-4732]
2022-10-19 13:34:45 +02:00
83573b0d43 Use labels to make editor more accessible
[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
bd87c09e1a Add acceptance test for automation workflow
* Creates an simple welcome email workflow
* Activates it
* Subscribes and checks whether the email arrives in the inbox

[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
1010b64c05 Update updateNames() to work with Woo Custom Order Tables
[MAILPOET-4711]
2022-10-19 11:32:41 +02:00
dd1fcd5100 Add helper method to get the name of the WooCommerce order addresses table
[MAILPOET-4711]
2022-10-19 11:32:41 +02:00
75706c9e8b Store success message of activation in activate action
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
61e1dd6a83 Add an activating state
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
8d5af952f6 Use sprintf instead of replace
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
2751a4bf3a End the sentence with a period
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
ed297dd68d Show snackbar notice
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
17c7d42ede Remove activate toggle
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
1741b39375 Style ActivatePanel
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
800432ab54 Introduce ActivatePanel
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
f169c8f8ac Use save method in Update button
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
f92a12db30 Make strings translateable
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
3ea730a0b6 Fix tab button focus styles
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
e3c19fa306 Improve listing styles
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
f2c4890def Avoid loosing tab focus on navigation
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
5bc2f62d98 Improve namin & add small simplifications to listing
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
c954603bfc Improve popover rendering
1) Rendering it in a slot (under slot fill provider) avoids inheriting deeply nested styles.
2) Bottom-left is a nicer default as it's the rightmost button in the table rows.

[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
bf552801ec Fix editor stats label color
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
6bb1af0a18 Simplify and fix stats rendering using CSS grid
Fixed separator rendering, simplified CSS (grid can do this), removed
had extra paddings that were causing problems on the listing page.

[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
48dca5e298 Reuse stats component in workflow listing
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
c08813be1d Extract workflow stats to atomation root component level
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
1cc6d93717 Do not wrap or shrink status cells
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
18a071fd7b Render "edit" and "more" actions in the same column
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
32d310d999 Add listing api error handler
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
c76d18e647 Add edit workflow link
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
865706c112 Extract edit workflow from cell to a generic action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
3238049828 Add undo trash button
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
a41de82030 Add workflow duplicate/trash/restore/delete success notices
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
ccec1faeb1 Add notices component for automation listing
This component combines data store from @wordpress/notices together with
MailPoet's notice rendering that is suitable for listing.

[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
b19887add0 Remove duplicate notices div
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
ba2cb75877 Add delete confirmation dialog
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
b89905aa80 Add trash confirmation dialog
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
a25e879cac Add delete workflow action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
0106c5123d Add restore workflow action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
2cb20a8f63 Add trash workflow action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
fe1a994442 Add duplicate workflow action, use dropdown from @wordpress/components
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
70889ab06d Load workflows list from store
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
aab6865f50 Initialize automation listing store
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
efd32043e3 Add workflow count to automation listing page
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
3eaa13a421 Add store for automation listing
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
80d2ab44a3 Release 3.101.0 2022-10-18 14:42:07 +02:00
215 changed files with 4108 additions and 4099 deletions

View File

@ -410,7 +410,9 @@ jobs:
name: Group acceptance tests name: Group acceptance tests
command: | command: |
# Convert test result filename values to be relative paths because the circleci CLI's split command requires exact matches # Convert test result filename values to be relative paths because the circleci CLI's split command requires exact matches
sed -i.bak 's#/wp-core/wp-content/plugins/mailpoet/##g' $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json if [ -e $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json ]; then
sed -i.bak 's#/wp-core/wp-content/plugins/mailpoet/##g' $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json
fi
# `circleci tests split` returns different values based on the container it's run on # `circleci tests split` returns different values based on the container it's run on
# in case group is defined find only tests containing the group # in case group is defined find only tests containing the group
if [[ -n '<< parameters.group >>' ]]; then if [[ -n '<< parameters.group >>' ]]; then

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. Filter argument supports following array keys.
| Key | Type | Description | | Key | Type | Description |
| -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------- | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------- |
| status | string | Specific status of subscribers. One of values: `unconfirmed`, `subscribed`, `unsubscribed`, `bounced`, `inactive` | | status | string | Specific status of subscribers. One of values: `unconfirmed`, `subscribed`, `unsubscribed`, `bounced`, `inactive` |
| list_id | int | List id or dynamic segment id | | listId | int | List id or dynamic segment id |
| min_updated_at | DateTime\int | DateTime object or timestamp of the minimal last update of subscribers | | minUpdatedAt | DateTime\int | DateTime object or timestamp of the minimal last update of subscribers |

View File

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

View File

@ -117,11 +117,45 @@ class RoboFile extends \Robo\Tasks {
} }
public function translationsBuild() { 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() $this->collectionBuilder()
->taskExec('mkdir -p ' . __DIR__ . '/lang') ->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' // HTML, HBS
)->run(); ->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() { public function translationsGetPotFileFromBuild() {

View File

@ -0,0 +1,44 @@
.mailpoet-automatoin-deactivate-modal {
color: #1d2327;
font-size: 13px;
line-height: 21px;
max-width: 480px;
.mailpoet-automation-options {
li {
margin-bottom: 12px;
}
}
.mailpoet-automation-option {
border: 2px solid #dcdcde;
border-radius: 4px;
color: #646970;
display: grid;
font-size: 12px;
grid-gap: 8px;
grid-template-columns: 20px auto;
line-height: 16px;
padding: 8px;
&.active {
border-color: #2271b1;
}
strong {
color: #1d2327;
display: block;
font-size: 13px;
font-weight: normal;
line-height: 21px;
}
}
.components-button {
float: right;
&.is-tertiary {
margin-right: 12px;
}
}
}

View File

@ -30,3 +30,31 @@
outline: none; 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

@ -12,6 +12,10 @@
font-weight: 500; font-weight: 500;
line-height: normal; line-height: normal;
padding: 16px 48px 16px 16px; padding: 16px 48px 16px 16px;
label & {
padding: 0;
}
} }
.mailpoet-automation-panel-plain-body-title-action { .mailpoet-automation-panel-plain-body-title-action {
@ -57,3 +61,78 @@
color: #757575; color: #757575;
font-style: italic; font-style: italic;
} }
.mailpoet-automation-activate-panel {
animation: mailpoet-automation-activate-panel-animation .1s forwards;
background: #fff;
border-left: 1px solid #ddd;
bottom: 0;
height: 100%;
left: auto;
overflow: auto;
position: fixed;
right: 0;
top: 0;
transform: translateX(100%);
width: 281px;
z-index: 999999;
button {
justify-content: center;
width: 100%;
}
}
.mailpoet-automation-activate-panel__header {
align-content: space-between;
align-items: center;
display: flex;
height: 61px;
.has-icon {
margin-left: auto;
width: auto;
}
}
.mailpoet-automation-activate-panel__header,
.mailpoet-automation-activate-panel__section {
border-bottom: 1px solid #ddd;
}
.mailpoet-automation-activate-panel__header,
.mailpoet-automation-activate-panel__body {
padding-left: 16px;
padding-right: 16px;
.components-spinner {
display: block;
margin: 100px auto 0;
}
}
.mailpoet-automation-activate-panel__section {
margin-left: -16px;
margin-right: -16px;
padding: 16px;
}
.mailpoet-automation-activate-panel__header-activate-button,
.mailpoet-automation-activate-panel__header-cancel-button {
flex-grow: 1;
max-width: 160px;
}
.mailpoet-automation-activate-panel__header-activate-button {
padding-right: 4px;
}
.mailpoet-automation-activate-panel__header-cancel-button {
padding-left: 4px;
}
@keyframes mailpoet-automation-activate-panel-animation {
100% {
transform: translateX(0);
}
}

View File

@ -17,3 +17,21 @@
padding: 3px; padding: 3px;
width: 18px; width: 18px;
} }
.mailpoet-automation-editor-stats {
margin: 0 auto 32px;
max-width: 480px;
width: 100%;
.mailpoet-automation-stats-item {
line-height: 22px;
}
.mailpoet-automation-stats-label {
color: #787c82;
}
.mailpoet-automation-stats-value {
font-size: 14px;
}
}

View File

@ -4,18 +4,43 @@
.mailpoet-add-new-button { .mailpoet-add-new-button {
padding-right: 12px; padding-right: 12px;
} }
}
.mailpoet-automation-listing { .mailpoet-automation-listing-heading {
/* Prevent border radius beneath tabs */ margin-bottom: 16px;
border-radius: 0 0 1px 1px; }
}
.mailpoet-automation-listing {
box-shadow: none;
margin-bottom: 0;
} }
.mailpoet-filter-tab-panel { .mailpoet-filter-tab-panel {
background-color: #fff; background-color: #fff;
border-radius: 1px; border: 1px solid #dcdcde;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1); border-radius: 2px;
outline: none;
.components-tab-panel__tabs {
box-shadow: inset 0 -1px 0 0 #dcdcde;
}
.components-tab-panel__tabs-item:focus {
box-shadow: none;
}
.components-tab-panel__tabs-item.is-active {
box-shadow: inset 0 -4px 0 0 var(--wp-admin-theme-color);
}
.components-tab-panel__tabs-item:focus-visible {
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
}
.components-tab-panel__tabs-item.is-active:focus-visible {
box-shadow:
inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),
inset 0 -4px 0 0 var(--wp-admin-theme-color);
}
.count { .count {
background-color: #f0f0f1; background-color: #f0f0f1;
@ -27,6 +52,13 @@
} }
} }
.mailpoet-automation-listing-heading { .mailpoet-automation-listing-more-button button.components-button {
margin-bottom: 16px; height: 36px;
padding: 0;
width: 36px;
svg {
height: 28px;
width: 28px;
}
} }

View File

@ -0,0 +1,7 @@
.mailpoet-automation-listing-cell-actions {
align-items: center;
display: grid;
gap: 8px;
grid-auto-flow: column;
white-space: nowrap;
}

View File

@ -1,6 +1,8 @@
.mailpoet-automation-listing-cell-status { .mailpoet-automation-listing-cell-status {
align-items: center; align-items: center;
display: flex; display: grid;
grid-auto-flow: column;
white-space: nowrap;
> div.components-base-control > div.components-base-control__field { > div.components-base-control > div.components-base-control__field {
margin-bottom: 0; margin-bottom: 0;

View File

@ -0,0 +1,32 @@
.mailpoet-automation-stats {
display: grid;
grid-auto-flow: column;
justify-content: space-between;
}
.mailpoet-automation-stats-item {
color: $color-wordpress-heading;
display: grid;
font-size: 12px;
line-height: 16px;
text-align: center;
}
.mailpoet-automation-stats-label {
color: #646970;
display: block;
&.display-after {
order: 1;
}
}
.mailpoet-automation-stats-value {
font-weight: 600;
}
.mailpoet-automation-stats-item-separator {
color: #a7aaad;
font-size: 20px;
margin: 0 16px;
}

View File

@ -232,3 +232,7 @@ progress::-moz-progress-bar {
.mailpoet-form-field-tags label.components-form-token-field__label { .mailpoet-form-field-tags label.components-form-token-field__label {
display: none; display: none;
} }
.mailpoet-form-field-disabled {
cursor: not-allowed;
}

View File

@ -1,63 +0,0 @@
.mailpoet-automation-stats {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
margin: auto;
@at-root #mailpoet_automation_editor #{&} {
justify-content: center;
margin-bottom: 32px;
max-width: 560px;
}
.mailpoet-automation-stats-item {
color: $color-wordpress-heading;
font-size: 12px;
font-weight: 600;
line-height: 16px;
padding: 0 16px;
position: relative;
text-align: center;
@at-root #mailpoet_automation_editor #{&} {
flex-grow: 1;
}
&:first-of-type {
@at-root #mailpoet_automation #{&} {
padding-left: 0;
}
}
@at-root #mailpoet_automation_editor #{&} {
font-size: 14px;
line-height: 22px;
}
&:after {
align-items: center;
color: #a7aaad;
content: '';
display: flex;
font-size: 20px;
font-weight: normal;
height: 100%;
justify-content: center;
position: absolute;
right: 0;
top: 0;
}
&:last-of-type:after {
content: '';
}
.mailpoet-automation-stats-label {
color: #646970;
display: block;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
}
}

View File

@ -2,6 +2,13 @@
@import '../../../node_modules/@wordpress/edit-site/build-style/style'; @import '../../../node_modules/@wordpress/edit-site/build-style/style';
@import '../../../node_modules/@wordpress/block-editor/build-style/style'; // for inserter styles @import '../../../node_modules/@wordpress/block-editor/build-style/style'; // for inserter styles
@import 'settings/colors'; @import 'settings/colors';
// automation components
@import './components-automation/statistics';
// automation editor
@import './components-automation-editor/add-step-button'; @import './components-automation-editor/add-step-button';
@import './components-automation-editor/add-trigger'; @import './components-automation-editor/add-trigger';
@import './components-automation-editor/block-icon'; @import './components-automation-editor/block-icon';
@ -16,8 +23,8 @@
@import './components-automation-editor/step-card'; @import './components-automation-editor/step-card';
@import './components-automation-editor/workflow'; @import './components-automation-editor/workflow';
@import './components-automation-editor/notices'; @import './components-automation-editor/notices';
@import './components-automation-editor/deactivate-modal';
// integrations // integrations
@import './components-automation-integrations/mailpoet'; @import './components-automation-integrations/mailpoet';
@import './components/automation_statistics';

View File

@ -1,7 +1,14 @@
@import '../../../node_modules/@woocommerce/components/build-style/style'; @import '../../../node_modules/@woocommerce/components/build-style/style';
@import 'settings/colors'; @import 'settings/colors';
// automation components
@import './components-automation/statistics';
// automation listing
@import './components-automation-listing/listing'; @import './components-automation-listing/listing';
@import './components-automation-listing/header'; @import './components-automation-listing/header';
@import './components-automation-listing/search'; @import './components-automation-listing/search';
@import './components-automation-listing/cells/actions';
@import './components-automation-listing/cells/status'; @import './components-automation-listing/cells/status';
@import './components/automation_statistics';

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

@ -2,35 +2,26 @@ import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { TopBarWithBeamer } from 'common/top_bar/top_bar'; import { TopBarWithBeamer } from 'common/top_bar/top_bar';
import { plusIcon } from 'common/button/icon/plus'; import { plusIcon } from 'common/button/icon/plus';
import { Button, Flex } from '@wordpress/components'; import { Button, Flex, Popover, SlotFillProvider } from '@wordpress/components';
import { Workflow } from './listing/workflow'; import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { initializeApi, useMutation } from './api';
import { registerTranslations } from './i18n';
import { createStore, storeName } from './listing/store';
import { AutomationListing } from './listing'; import { AutomationListing } 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 { Onboarding } from './onboarding';
import { import {
CreateEmptyWorkflowButton, CreateEmptyWorkflowButton,
CreateWorkflowFromTemplateButton, CreateWorkflowFromTemplateButton,
} from './testing'; } from './testing';
import { useMutation, useQuery } from './api';
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
import { MailPoet } from '../mailpoet'; import { MailPoet } from '../mailpoet';
function Content(): JSX.Element { function Content(): JSX.Element {
const { data, loading, error } = useQuery<{ data: Workflow[] }>('workflows'); const count = useSelect((select) => select(storeName).getWorkflowCount());
return count > 0 ? <AutomationListing /> : <Onboarding />;
if (error) {
return <div>Error: {error}</div>;
}
if (loading) {
return <div>Loading workflows...</div>;
}
const workflows = data?.data ?? [];
return workflows.length > 0 ? (
<AutomationListing workflows={workflows} loading={loading} />
) : (
<Onboarding />
);
} }
function Workflows(): JSX.Element { function Workflows(): JSX.Element {
@ -38,16 +29,17 @@ function Workflows(): JSX.Element {
<> <>
<TopBarWithBeamer /> <TopBarWithBeamer />
<Flex className="mailpoet-automation-listing-heading"> <Flex className="mailpoet-automation-listing-heading">
<h1 className="wp-heading-inline">Automations</h1> <h1 className="wp-heading-inline">{__('Automations', 'mailpoet')}</h1>
<Button <Button
href={MailPoet.urls.automationTemplates} href={MailPoet.urls.automationTemplates}
icon={plusIcon} icon={plusIcon}
variant="primary" variant="primary"
className="mailpoet-add-new-button" className="mailpoet-add-new-button"
> >
New automation {__('New automation', 'mailpoet')}
</Button> </Button>
</Flex> </Flex>
<Notices />
<Content /> <Content />
</> </>
); );
@ -104,33 +96,41 @@ function DeleteSchemaButton(): JSX.Element {
function App(): JSX.Element { function App(): JSX.Element {
return ( return (
<BrowserRouter> <SlotFillProvider>
<div> <BrowserRouter>
<Workflows /> <div>
<div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}> <Workflows />
<CreateEmptyWorkflowButton /> <div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}>
<CreateWorkflowFromTemplateButton slug="simple-welcome-email"> <CreateEmptyWorkflowButton />
Create testing workflow from template (welcome email) <CreateWorkflowFromTemplateButton slug="simple-welcome-email">
</CreateWorkflowFromTemplateButton> Create testing workflow from template (welcome email)
<CreateWorkflowFromTemplateButton slug="welcome-email-sequence"> </CreateWorkflowFromTemplateButton>
Create testing workflow from template (welcome sequence, only <CreateWorkflowFromTemplateButton slug="welcome-email-sequence">
premium) Create testing workflow from template (welcome sequence, only
</CreateWorkflowFromTemplateButton> premium)
<CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence"> </CreateWorkflowFromTemplateButton>
Create testing workflow from template (advanced welcome sequence, <CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence">
only premium) Create testing workflow from template (advanced welcome sequence,
</CreateWorkflowFromTemplateButton> only premium)
<RecreateSchemaButton /> </CreateWorkflowFromTemplateButton>
<DeleteSchemaButton /> <RecreateSchemaButton />
<DeleteSchemaButton />
</div>
<Popover.Slot />
</div> </div>
</div> </BrowserRouter>
</BrowserRouter> </SlotFillProvider>
); );
} }
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
createStore();
const root = document.getElementById('mailpoet_automation'); const root = document.getElementById('mailpoet_automation');
if (root) { if (root) {
registerTranslations();
registerApiErrorHandler();
initializeApi();
ReactDOM.render(<App />, root); ReactDOM.render(<App />, root);
} }
}); });

View File

@ -0,0 +1,41 @@
import { Fragment } from '@wordpress/element';
type Item = {
key: string;
label: string;
value: number;
};
type Props = {
items: Item[];
labelPosition?: 'before' | 'after';
};
export function Statistics({
items,
labelPosition = 'before',
}: Props): JSX.Element {
const intl = new Intl.NumberFormat();
return (
<div className="mailpoet-automation-stats">
{items.map((item, i) => (
<Fragment key={item.key}>
<div key={item.key} className="mailpoet-automation-stats-item">
<span
className={`mailpoet-automation-stats-label display-${labelPosition}`}
>
{item.label}
</span>
<span className="mailpoet-automation-stats-value">
{intl.format(item.value)}
</span>
</div>
{i < items.length - 1 && (
<div className="mailpoet-automation-stats-item-separator"></div>
)}
</Fragment>
))}
</div>
);
}

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { ComponentProps, ComponentType, Ref } from 'react'; import { ComponentProps, ComponentType, Ref } from 'react';
import { import {
Dropdown as WpDropdown,
Button,
VisuallyHidden,
__experimentalText as Text, __experimentalText as Text,
Button,
Dropdown as WpDropdown,
VisuallyHidden,
} from '@wordpress/components'; } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { useRef } from '@wordpress/element'; import { useRef } from '@wordpress/element';
@ -38,7 +38,11 @@ export function DocumentActions({ children }): JSX.Element {
let chipClass = 'mailpoet-automation-editor-chip-gray'; let chipClass = 'mailpoet-automation-editor-chip-gray';
if (workflowStatus === WorkflowStatus.ACTIVE) { if (workflowStatus === WorkflowStatus.ACTIVE) {
chipClass = 'mailpoet-automation-editor-chip-success'; chipClass = 'mailpoet-automation-editor-chip-success';
} else if (workflowStatus === WorkflowStatus.INACTIVE) { } else if (
[WorkflowStatus.INACTIVE, WorkflowStatus.DEACTIVATING].includes(
workflowStatus,
)
) {
chipClass = 'mailpoet-automation-editor-chip-danger'; chipClass = 'mailpoet-automation-editor-chip-danger';
} }
@ -64,7 +68,7 @@ export function DocumentActions({ children }): JSX.Element {
as="h1" as="h1"
> >
<VisuallyHidden as="span"> <VisuallyHidden as="span">
{__('Editing workflow: ')} {__('Editing workflow: ', 'mailpoet')}
</VisuallyHidden> </VisuallyHidden>
{workflowName} {workflowName}
</Text> </Text>
@ -73,10 +77,14 @@ export function DocumentActions({ children }): JSX.Element {
size="body" size="body"
className={`edit-site-document-actions__secondary-item ${chipClass}`} className={`edit-site-document-actions__secondary-item ${chipClass}`}
> >
{workflowStatus === WorkflowStatus.ACTIVE && __('Active')} {workflowStatus === WorkflowStatus.ACTIVE &&
__('Active', 'mailpoet')}
{workflowStatus === WorkflowStatus.INACTIVE && {workflowStatus === WorkflowStatus.INACTIVE &&
__('Inactive')} __('Inactive', 'mailpoet')}
{workflowStatus === WorkflowStatus.DRAFT && __('Draft')} {workflowStatus === WorkflowStatus.DEACTIVATING &&
__('Deactivating', 'mailpoet')}
{workflowStatus === WorkflowStatus.DRAFT &&
__('Draft', 'mailpoet')}
</Text> </Text>
</a> </a>
<Button <Button
@ -85,9 +93,9 @@ export function DocumentActions({ children }): JSX.Element {
aria-expanded={isOpen} aria-expanded={isOpen}
aria-haspopup="true" aria-haspopup="true"
onClick={onToggle} onClick={onToggle}
label={__('Change workflow name')} label={__('Change workflow name', 'mailpoet')}
> >
{showIconLabels && __('Rename')} {showIconLabels && __('Rename', 'mailpoet')}
</Button> </Button>
</> </>
)} )}

View File

@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n';
import { Chip } from '../chip'; import { Chip } from '../chip';
import { ColoredIcon } from '../icons'; import { ColoredIcon } from '../icons';
import { import {
StepError as StepErrorType, StepErrors as StepErrorType,
stepSidebarKey, stepSidebarKey,
storeName, storeName,
} from '../../store'; } from '../../store';
@ -155,7 +155,10 @@ export function Errors(): JSX.Element | null {
className="mailpoet-automation-errors" className="mailpoet-automation-errors"
> >
<div className="mailpoet-automation-errors-header"> <div className="mailpoet-automation-errors-header">
{__('The following steps are not fully set:', 'mailpoet')} {
// translators: Label for a list of automation workflow steps that are incomplete or have errors
__('The following steps are not fully set:', 'mailpoet')
}
</div> </div>
{stepErrors.map((error) => ( {stepErrors.map((error) => (
<StepError key={error.step_id} stepId={error.step_id} /> <StepError key={error.step_id} stepId={error.step_id} />

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
import { Button, NavigableMenu, TextControl } from '@wordpress/components'; import { Button, NavigableMenu, TextControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data'; import { dispatch, useDispatch, useSelect } from '@wordpress/data';
import { PinnedItems } from '@wordpress/interface'; import { PinnedItems } from '@wordpress/interface';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { DocumentActions } from './document_actions'; import { DocumentActions } from './document_actions';
@ -8,12 +9,16 @@ import { InserterToggle } from './inserter_toggle';
import { MoreMenu } from './more_menu'; import { MoreMenu } from './more_menu';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow'; import { WorkflowStatus } from '../../../listing/workflow';
import {
DeactivateImmediatelyModal,
DeactivateModal,
} from '../modals/deactivate-modal';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/index.js // 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 // https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
function ActivateButton(): JSX.Element { function ActivateButton({ onClick, label }): JSX.Element {
const { errors } = useSelect( const { errors } = useSelect(
(select) => ({ (select) => ({
errors: select(storeName).getErrors(), errors: select(storeName).getErrors(),
@ -21,30 +26,28 @@ function ActivateButton(): JSX.Element {
[], [],
); );
const { activate } = useDispatch(storeName);
return ( return (
<Button <Button
variant="primary" variant="primary"
className="editor-post-publish-button" className="editor-post-publish-button"
onClick={activate} onClick={onClick}
disabled={!!errors} disabled={!!errors}
> >
Activate {label}
</Button> </Button>
); );
} }
function UpdateButton(): JSX.Element { function UpdateButton(): JSX.Element {
const { activate } = useDispatch(storeName); const { save } = useDispatch(storeName);
return ( return (
<Button <Button
variant="primary" variant="primary"
className="editor-post-publish-button" className="editor-post-publish-button"
onClick={activate} onClick={save}
> >
Update {__('Update', 'mailpoet')}
</Button> </Button>
); );
} }
@ -54,16 +57,100 @@ function SaveDraftButton(): JSX.Element {
return ( return (
<Button variant="tertiary" onClick={save}> <Button variant="tertiary" onClick={save}>
{__('Save draft')} {__('Save draft', 'mailpoet')}
</Button> </Button>
); );
} }
function DeactivateButton(): JSX.Element {
const [showDeactivateModal, setShowDeactivateModal] = useState(false);
const [isBusy, setIsBusy] = useState(false);
const { hasUsersInProgress } = useSelect(
(select) => ({
hasUsersInProgress:
select(storeName).getWorkflowData().stats.totals.in_progress > 0,
}),
[],
);
const deactivateOrShowModal = () => {
if (hasUsersInProgress) {
setShowDeactivateModal(true);
return;
}
setIsBusy(true);
void dispatch(storeName).deactivate();
};
return (
<>
{showDeactivateModal && (
<DeactivateModal
onClose={() => {
setShowDeactivateModal(false);
}}
/>
)}
<Button
isBusy={isBusy}
variant="tertiary"
onClick={deactivateOrShowModal}
>
{__('Deactivate', 'mailpoet')}
</Button>
</>
);
}
function DeactivateNowButton(): JSX.Element {
const [showDeactivateModal, setShowDeactivateModal] = useState(false);
const [isBusy, setIsBusy] = useState(false);
const { hasUsersInProgress } = useSelect(
(select) => ({
hasUsersInProgress:
select(storeName).getWorkflowData().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 = { type Props = {
showInserterToggle: boolean; showInserterToggle: boolean;
toggleActivatePanel: () => void;
}; };
export function Header({ showInserterToggle }: Props): JSX.Element { export function Header({
showInserterToggle,
toggleActivatePanel,
}: Props): JSX.Element {
const { setWorkflowName } = useDispatch(storeName); const { setWorkflowName } = useDispatch(storeName);
const { workflowName, workflowStatus } = useSelect( const { workflowName, workflowStatus } = useSelect(
(select) => ({ (select) => ({
@ -90,13 +177,14 @@ export function Header({ showInserterToggle }: Props): JSX.Element {
{() => ( {() => (
<div className="mailpoet-automation-editor-dropdown-name-edit"> <div className="mailpoet-automation-editor-dropdown-name-edit">
<div className="mailpoet-automation-editor-dropdown-name-edit-title"> <div className="mailpoet-automation-editor-dropdown-name-edit-title">
{__('Automation name')} {__('Automation name', 'mailpoet')}
</div> </div>
<TextControl <TextControl
value={workflowName} value={workflowName}
onChange={(newName) => setWorkflowName(newName)} onChange={(newName) => setWorkflowName(newName)}
help={__( help={__(
`Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`, `Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`,
'mailpoet',
)} )}
/> />
</div> </div>
@ -107,9 +195,36 @@ export function Header({ showInserterToggle }: Props): JSX.Element {
<div className="edit-site-header_end"> <div className="edit-site-header_end">
<div className="edit-site-header__actions"> <div className="edit-site-header__actions">
<Errors /> <Errors />
<SaveDraftButton /> {workflowStatus === WorkflowStatus.DRAFT && (
{workflowStatus !== WorkflowStatus.ACTIVE && <ActivateButton />} <>
{workflowStatus === WorkflowStatus.ACTIVE && <UpdateButton />} <SaveDraftButton />
<ActivateButton
onClick={toggleActivatePanel}
label={__('Activate', 'mailpoet')}
/>
</>
)}
{workflowStatus === WorkflowStatus.ACTIVE && (
<>
<DeactivateButton />
<UpdateButton />
</>
)}
{workflowStatus === WorkflowStatus.DEACTIVATING && (
<>
<DeactivateNowButton />
<ActivateButton
onClick={toggleActivatePanel}
label={__('Update & Activate', 'mailpoet')}
/>
</>
)}
{workflowStatus === WorkflowStatus.INACTIVE && (
<ActivateButton
onClick={toggleActivatePanel}
label={__('Update & Activate', 'mailpoet')}
/>
)}
<PinnedItems.Slot scope={storeName} /> <PinnedItems.Slot scope={storeName} />
<MoreMenu /> <MoreMenu />
</div> </div>

View File

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

View File

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

View File

@ -13,7 +13,10 @@ export const InserterListboxGroup = forwardRef<HTMLDivElement, Props>(
useEffect(() => { useEffect(() => {
if (shouldSpeak) { 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]); }, [shouldSpeak]);

View File

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

View File

@ -25,7 +25,7 @@ export function KeyboardShortcuts(): null {
void registerShortcut({ void registerShortcut({
name: 'mailpoet/automation-editor/toggle-fullscreen', name: 'mailpoet/automation-editor/toggle-fullscreen',
category: 'global', category: 'global',
description: __('Toggle fullscreen mode.'), description: __('Toggle fullscreen mode.', 'mailpoet'),
keyCombination: { keyCombination: {
modifier: 'secondary', modifier: 'secondary',
character: 'f', character: 'f',
@ -35,7 +35,7 @@ export function KeyboardShortcuts(): null {
void registerShortcut({ void registerShortcut({
name: 'mailpoet/automation-editor/toggle-sidebar', name: 'mailpoet/automation-editor/toggle-sidebar',
category: 'global', category: 'global',
description: __('Show or hide the settings sidebar.'), description: __('Show or hide the settings sidebar.', 'mailpoet'),
keyCombination: { keyCombination: {
modifier: 'primaryShift', modifier: 'primaryShift',
character: ',', character: ',',

View File

@ -0,0 +1,155 @@
import { useState } from 'react';
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';
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 automatoin 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 { workflowName } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
}),
[],
);
const [selected, setSelected] = useState<
WorkflowStatus.INACTIVE | WorkflowStatus.DEACTIVATING
>(WorkflowStatus.DEACTIVATING);
const [isBusy, setIsBusy] = useState<boolean>(false);
// translators: %s is the name of the automation.
const title = sprintf(
__('Deactivate the "%s" automation?', 'mailpoet'),
workflowName,
);
return (
<Modal
className="mailpoet-automatoin-deactivate-modal"
title={title}
onRequestClose={onClose}
>
{__(
"Some subscribers entered but have not finished the flow. Let's decide what to do in this case.",
'mailpoet',
)}
<ul className="mailpoet-automation-options">
<li>
<label
className={
selected === WorkflowStatus.DEACTIVATING
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
>
<span>
<input
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.DEACTIVATING}
onChange={() => setSelected(WorkflowStatus.DEACTIVATING)}
/>
</span>
<span>
<strong>
{__('Let entered subscribers finish the flow', 'mailpoet')}
</strong>
{__(
"New subscribers won't enter, but recently entered could proceed.",
'mailpoet',
)}
</span>
</label>
</li>
<li>
<label
className={
selected === WorkflowStatus.INACTIVE
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
>
<span>
<input
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.INACTIVE}
onChange={() => setSelected(WorkflowStatus.INACTIVE)}
/>
</span>
<span>
<strong>
{__('Stop automation for all subscribers', 'mailpoet')}
</strong>
{__(
'Automation will be deactivated for all the subscribers immediately.',
'mailpoet',
)}
</span>
</label>
</li>
</ul>
<Button
isBusy={isBusy}
variant="primary"
onClick={() => {
setIsBusy(true);
dispatch(storeName).deactivate(
selected !== WorkflowStatus.DEACTIVATING,
);
}}
>
{__('Deactivate automation', 'mailpoet')}
</Button>
<Button disabled={isBusy} variant="tertiary" onClick={onClose}>
{__('Cancel', 'mailpoet')}
</Button>
</Modal>
);
}

View File

@ -0,0 +1,128 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelect } from '@wordpress/data';
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 { MailPoet } from '../../../../mailpoet';
function PreStep({ onClose }): JSX.Element {
const [isActivating, setIsActivating] = useState(false);
const { activate } = useDispatch(storeName);
return (
<>
<div className="mailpoet-automation-activate-panel__header">
<div className="mailpoet-automation-activate-panel__header-activate-button">
<Button
variant="primary"
disabled={isActivating}
isBusy={isActivating}
autoFocus={!isActivating}
onClick={() => {
setIsActivating(true);
activate();
}}
>
{isActivating && __('Activating…', 'mailpoet')}
{!isActivating && __('Activate', 'mailpoet')}
</Button>
</div>
<div className="mailpoet-automation-activate-panel__header-cancel-button">
<Button variant="secondary" onClick={onClose} disabled={isActivating}>
{__('Cancel', 'mailpoet')}
</Button>
</div>
</div>
{isActivating && (
<div className="mailpoet-automation-activate-panel__body">
<Spinner />
</div>
)}
{!isActivating && (
<div className="mailpoet-automation-activate-panel__body">
<p>
<strong>{__('Are you ready to activate?', 'mailpoet')}</strong>
</p>
<p>
{__('Double-check your settings before activating.', 'mailpoet')}
</p>
</div>
)}
</>
);
}
function PostStep({ onClose }): JSX.Element {
const { workflow } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
}),
[],
);
const goToListings = () => {
window.location.href = MailPoet.urls.automationListing;
};
return (
<>
<div className="mailpoet-automation-activate-panel__header">
<Button
icon={closeSmall}
onClick={onClose}
label={__('Close', 'mailpoet')}
/>
</div>
<div className="mailpoet-automation-activate-panel__body">
<div className="mailpoet-automation-activate-panel__section">
{sprintf(__('"%s" is now live.', 'mailpoet'), workflow.name)}
</div>
<p>
<strong>{__('Whats next?', 'mailpoet')}</strong>
</p>
<p>
{__(
'View all your automations to track statistics and create new ones.',
'mailpoet',
)}
</p>
<Button variant="secondary" onClick={goToListings}>
{__('View all automations', 'mailpoet')}
</Button>
</div>
</>
);
}
export function ActivatePanel({ onClose }): JSX.Element {
const { workflow, errors } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
workflow: select(storeName).getWorkflowData(),
}),
[],
);
useEffect(() => {
if (errors) {
onClose();
}
}, [errors, onClose]);
if (errors) {
return null;
}
const isActive = workflow.status === WorkflowStatus.ACTIVE;
return (
<div className="mailpoet-automation-activate-panel">
{isActive && <PostStep onClose={onClose} />}
{!isActive && <PreStep onClose={onClose} />}
</div>
);
}

View File

@ -1,4 +1,5 @@
import { Dropdown, TextControl } from '@wordpress/components'; import { Dropdown, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { edit, Icon } from '@wordpress/icons'; import { edit, Icon } from '@wordpress/icons';
import { PlainBodyTitle } from './plain-body-title'; import { PlainBodyTitle } from './plain-body-title';
import { TitleActionButton } from './title-action-button'; import { TitleActionButton } from './title-action-button';
@ -25,7 +26,7 @@ export function StepName({
<TitleActionButton <TitleActionButton
onClick={onToggle} onClick={onToggle}
aria-expanded={isOpen} aria-expanded={isOpen}
aria-label="Edit step name" aria-label={__('Edit step name', 'mailpoet')}
> >
<Icon icon={edit} size={16} /> <Icon icon={edit} size={16} />
</TitleActionButton> </TitleActionButton>
@ -33,13 +34,15 @@ export function StepName({
)} )}
renderContent={() => ( renderContent={() => (
<TextControl <TextControl
label="Step name" label={__('Step name', 'mailpoet')}
className="mailpoet-step-name-input" className="mailpoet-step-name-input"
placeholder={defaultName} placeholder={defaultName}
value={currentName} value={currentName}
onChange={update} onChange={update}
help="Give the automation step a name that indicates its purpose. E.g help={__(
Abandoned cart recovery. This name will be displayed only to you and not to the clients." '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,5 +1,6 @@
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data'; import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store'; import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
// See: // See:
@ -17,12 +18,12 @@ export function Header({ sidebarKey }: Props): JSX.Element {
const [workflowAriaLabel, workflowActiveClass] = const [workflowAriaLabel, workflowActiveClass] =
sidebarKey === workflowSidebarKey sidebarKey === workflowSidebarKey
? ['Workflow (selected)', 'is-active'] ? [__('Workflow (selected)', 'mailpoet'), 'is-active']
: ['Workflow', '']; : ['Workflow', ''];
const [stepAriaLabel, stepActiveClass] = const [stepAriaLabel, stepActiveClass] =
sidebarKey === stepSidebarKey sidebarKey === stepSidebarKey
? ['Step (selected)', 'is-active'] ? [__('Step (selected)', 'mailpoet'), 'is-active']
: ['Step', '']; : ['Step', ''];
return ( return (
@ -32,9 +33,9 @@ export function Header({ sidebarKey }: Props): JSX.Element {
onClick={openWorkflowSettings} onClick={openWorkflowSettings}
className={`edit-site-sidebar__panel-tab ${workflowActiveClass}`} className={`edit-site-sidebar__panel-tab ${workflowActiveClass}`}
aria-label={workflowAriaLabel} aria-label={workflowAriaLabel}
data-label="Workflow" data-label={__('Workflow', 'mailpoet')}
> >
Workflow {__('Workflow', 'mailpoet')}
</Button> </Button>
</li> </li>
<li> <li>
@ -42,9 +43,9 @@ export function Header({ sidebarKey }: Props): JSX.Element {
onClick={openStepSettings} onClick={openStepSettings}
className={`edit-site-sidebar__panel-tab ${stepActiveClass}`} className={`edit-site-sidebar__panel-tab ${stepActiveClass}`}
aria-label={stepAriaLabel} aria-label={stepAriaLabel}
data-label="Workflow" data-label={__('Step', 'mailpoet')}
> >
Step {__('Step', 'mailpoet')}
</Button> </Button>
</li> </li>
</ul> </ul>

View File

@ -47,13 +47,13 @@ export function Sidebar(props: Props): JSX.Element {
<ComplementaryArea <ComplementaryArea
identifier={sidebarKey} identifier={sidebarKey}
header={<Header sidebarKey={sidebarKey} />} header={<Header sidebarKey={sidebarKey} />}
closeLabel={__('Close settings')} closeLabel={__('Close settings', 'mailpoet')}
headerClassName="edit-site-sidebar__panel-tabs" headerClassName="edit-site-sidebar__panel-tabs"
title={__('Settings')} title={__('Settings', 'mailpoet')}
icon={cog} icon={cog}
className="edit-site-sidebar mailpoet-automation-sidebar" className="edit-site-sidebar mailpoet-automation-sidebar"
panelClassName="edit-site-sidebar" panelClassName="edit-site-sidebar"
smallScreenTitle={workflowName || __('(no title)')} smallScreenTitle={workflowName || __('(no title)', 'mailpoet')}
scope={storeName} scope={storeName}
toggleShortcut={keyboardShortcut} toggleShortcut={keyboardShortcut}
isActiveByDefault={sidebarActiveByDefault} isActiveByDefault={sidebarActiveByDefault}

View File

@ -1,5 +1,6 @@
import { PanelBody } from '@wordpress/components'; import { PanelBody } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../../store'; import { storeName } from '../../../store';
import { StepCard } from '../../step-card'; import { StepCard } from '../../step-card';
@ -32,7 +33,7 @@ export function StepSidebar(): JSX.Element {
<Edit /> <Edit />
<PanelBody title="Debug info" initialOpen={false}> <PanelBody title={__('Debug info', 'mailpoet')} initialOpen={false}>
<div> <div>
<strong>ID:</strong> {selectedStep.id} <strong>ID:</strong> {selectedStep.id}
</div> </div>

View File

@ -1,5 +1,6 @@
import { PanelBody, PanelRow } from '@wordpress/components'; import { PanelBody, PanelRow } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../../store'; import { storeName } from '../../../store';
import { TrashButton } from '../../actions/trash-button'; import { TrashButton } from '../../actions/trash-button';
@ -18,7 +19,7 @@ export function WorkflowSidebar(): JSX.Element {
}; };
return ( return (
<PanelBody title="Automation details" initialOpen> <PanelBody title={__('Automation details', 'mailpoet')} initialOpen>
<PanelRow> <PanelRow>
<strong>Date added</strong>{' '} <strong>Date added</strong>{' '}
{new Date(Date.parse(workflowData.created_at)).toLocaleDateString( {new Date(Date.parse(workflowData.created_at)).toLocaleDateString(

View File

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

View File

@ -1,6 +1,7 @@
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { _x } from '@wordpress/i18n';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { Statistics as BaseStatistics } from '../../../components/statistics';
export function Statistics(): JSX.Element { export function Statistics(): JSX.Element {
const { workflow } = useSelect( const { workflow } = useSelect(
@ -11,27 +12,29 @@ export function Statistics(): JSX.Element {
); );
return ( return (
<div> <div className="mailpoet-automation-editor-stats">
<ul className="mailpoet-automation-stats"> <BaseStatistics
<li className="mailpoet-automation-stats-item"> items={[
<span className="mailpoet-automation-stats-label"> {
{__('Total Entered', 'mailpoet')} key: 'entered',
</span> // translators: Total number of subscribers who entered an automation workflow
{new Intl.NumberFormat().format(workflow.stats.totals.entered)} label: _x('Total Entered', 'automation stats', 'mailpoet'),
</li> value: workflow.stats.totals.entered,
<li className="mailpoet-automation-stats-item"> },
<span className="mailpoet-automation-stats-label"> {
{__('Total Processing', 'mailpoet')} key: 'processing',
</span> // translators: Total number of subscribers who are being processed in an automation workflow
{new Intl.NumberFormat().format(workflow.stats.totals.in_progress)} label: _x('Total Processing', 'automation stats', 'mailpoet'),
</li> value: workflow.stats.totals.in_progress,
<li className="mailpoet-automation-stats-item"> },
<span className="mailpoet-automation-stats-label"> {
{__('Total Exited', 'mailpoet')} key: 'exited',
</span> // translators: Total number of subscribers who exited an automation workflow, no matter the result
{new Intl.NumberFormat().format(workflow.stats.totals.exited)} label: _x('Total Exited', 'automation stats', 'mailpoet'),
</li> value: workflow.stats.totals.exited,
</ul> },
]}
/>
</div> </div>
); );
} }

View File

@ -51,7 +51,7 @@ export function StepMoreMenu({ step }: Props): JSX.Element {
utm_campaign: 'remove_automation_step', utm_campaign: 'remove_automation_step',
}} }}
> >
{__('You cannot remove a new step from the automation.', 'mailpoet')} {__('You cannot remove a step from the automation.', 'mailpoet')}
</PremiumModal> </PremiumModal>
)} )}
</> </>

View File

@ -3,7 +3,7 @@ import { useContext } from 'react';
import { __unstableCompositeItem as CompositeItem } from '@wordpress/components'; import { __unstableCompositeItem as CompositeItem } from '@wordpress/components';
import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
import { blockMeta } from '@wordpress/icons'; import { blockMeta } from '@wordpress/icons';
import { __ } from '@wordpress/i18n'; import { __, _x } from '@wordpress/i18n';
import { WorkflowCompositeContext } from './context'; import { WorkflowCompositeContext } from './context';
import { StepMoreMenu } from './step-more-menu'; import { StepMoreMenu } from './step-more-menu';
import { Step as StepData } from './types'; import { Step as StepData } from './types';
@ -51,6 +51,7 @@ export function Step({ step, isSelected }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext); const compositeState = useContext(WorkflowCompositeContext);
const { batch } = useRegistry(); const { batch } = useRegistry();
const compositeItemId = `step-${step.id}`;
const stepTypeData = stepType ?? getUnknownStepType(step); const stepTypeData = stepType ?? getUnknownStepType(step);
return ( return (
<div className="mailpoet-automation-editor-step-wrapper"> <div className="mailpoet-automation-editor-step-wrapper">
@ -63,6 +64,7 @@ export function Step({ step, isSelected }: Props): JSX.Element {
'is-selected-step': isSelected, 'is-selected-step': isSelected,
'is-unknown-step': !stepType, 'is-unknown-step': !stepType,
})} })}
id={compositeItemId}
key={step.id} key={step.id}
focusable focusable
onClick={() => onClick={() =>
@ -82,11 +84,14 @@ export function Step({ step, isSelected }: Props): JSX.Element {
/> />
</div> </div>
<div> <div>
<div className="mailpoet-automation-editor-step-title"> <label
htmlFor={compositeItemId}
className="mailpoet-automation-editor-step-title"
>
{step.type !== 'trigger' {step.type !== 'trigger'
? stepTypeData.title ? stepTypeData.title
: __('Trigger', 'mailpoet')} : _x('Trigger', 'noun', 'mailpoet')}
</div> </label>
<div className="mailpoet-automation-editor-step-subtitle"> <div className="mailpoet-automation-editor-step-subtitle">
{step.type !== 'trigger' {step.type !== 'trigger'
? stepTypeData.subtitle(step) ? stepTypeData.subtitle(step)

View File

@ -1,3 +1,5 @@
import { WorkflowStatus } from '../../../listing/workflow';
export type NextStep = { export type NextStep = {
id: string; id: string;
}; };
@ -13,7 +15,7 @@ export type Step = {
export type Workflow = { export type Workflow = {
id: number; id: number;
name: string; name: string;
status: 'active' | 'inactive' | 'draft' | 'trash'; status: WorkflowStatus;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
activated_at: string; activated_at: string;

View File

@ -1,5 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useState } from 'react';
import { Button, Icon, Popover, SlotFillProvider } from '@wordpress/components'; import { Button, Icon, Popover, SlotFillProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { wordpress } from '@wordpress/icons'; import { wordpress } from '@wordpress/icons';
@ -23,6 +24,8 @@ import { initialize as initializeMailPoetIntegration } from '../integrations/mai
import { MailPoet } from '../../mailpoet'; import { MailPoet } from '../../mailpoet';
import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices'; import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices';
import { registerApiErrorHandler } from './api-error-handler'; import { registerApiErrorHandler } from './api-error-handler';
import { ActivatePanel } from './components/panel/activate-panel';
import { registerTranslations } from '../i18n';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js
@ -48,6 +51,7 @@ function Editor(): JSX.Element {
}), }),
[], [],
); );
const [showActivatePanel, setShowActivatePanel] = useState(false);
const className = classnames('interface-interface-skeleton', { const className = classnames('interface-interface-skeleton', {
'is-sidebar-opened': isSidebarOpened, 'is-sidebar-opened': isSidebarOpened,
@ -60,6 +64,11 @@ function Editor(): JSX.Element {
}); });
return null; return null;
} }
const toggleActivatePanel = () => {
setShowActivatePanel(!showActivatePanel);
};
return ( return (
<ShortcutProvider> <ShortcutProvider>
<SlotFillProvider> <SlotFillProvider>
@ -80,7 +89,12 @@ function Editor(): JSX.Element {
</div> </div>
) )
} }
header={<Header showInserterToggle={showInserterSidebar} />} header={
<Header
showInserterToggle={showInserterSidebar}
toggleActivatePanel={toggleActivatePanel}
/>
}
content={ content={
<> <>
<EditorNotices /> <EditorNotices />
@ -92,6 +106,7 @@ function Editor(): JSX.Element {
showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null
} }
/> />
{showActivatePanel && <ActivatePanel onClose={toggleActivatePanel} />}
<Popover.Slot /> <Popover.Slot />
</SlotFillProvider> </SlotFillProvider>
</ShortcutProvider> </ShortcutProvider>
@ -103,6 +118,7 @@ window.addEventListener('DOMContentLoaded', () => {
const root = document.getElementById('mailpoet_automation_editor'); const root = document.getElementById('mailpoet_automation_editor');
if (root) { if (root) {
registerTranslations();
registerApiErrorHandler(); registerApiErrorHandler();
initializeApi(); initializeApi();
initializeCoreIntegration(); initializeCoreIntegration();

View File

@ -1,5 +1,7 @@
import { select } from '@wordpress/data'; import { dispatch, select, StoreDescriptor } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls'; import { apiFetch } from '@wordpress/data-controls';
import { store as noticesStore } from '@wordpress/notices';
import { __ } from '@wordpress/i18n';
import { store as interfaceStore } from '@wordpress/interface'; import { store as interfaceStore } from '@wordpress/interface';
import { store as preferencesStore } from '@wordpress/preferences'; import { store as preferencesStore } from '@wordpress/preferences';
import { addQueryArgs } from '@wordpress/url'; import { addQueryArgs } from '@wordpress/url';
@ -7,6 +9,7 @@ import { storeName } from './constants';
import { Feature, State } from './types'; import { Feature, State } from './types';
import { LISTING_NOTICE_PARAMETERS } from '../../listing/workflow-listing-notices'; import { LISTING_NOTICE_PARAMETERS } from '../../listing/workflow-listing-notices';
import { MailPoet } from '../../../mailpoet'; import { MailPoet } from '../../../mailpoet';
import { WorkflowStatus } from '../../listing/workflow';
export const openSidebar = export const openSidebar =
(key) => (key) =>
@ -62,6 +65,17 @@ export function* save() {
data: { ...workflow }, data: { ...workflow },
}); });
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data) {
void createNotice(
'success',
__('The automation has been saved.', 'mailpoet'),
{
type: 'snackbar',
},
);
}
return { return {
type: 'SAVE', type: 'SAVE',
workflow: data?.data ?? workflow, workflow: data?.data ?? workflow,
@ -75,16 +89,72 @@ export function* activate() {
method: 'PUT', method: 'PUT',
data: { data: {
...workflow, ...workflow,
status: 'active', status: WorkflowStatus.ACTIVE,
}, },
}); });
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data.status === WorkflowStatus.ACTIVE) {
void createNotice(
'success',
__('Well done! Automation is now activated!', 'mailpoet'),
{
type: 'snackbar',
},
);
}
return { return {
type: 'ACTIVATE', type: 'ACTIVATE',
workflow: data?.data ?? workflow, workflow: data?.data ?? workflow,
} as const; } as const;
} }
export function* deactivate(deactivateWorkflowRuns = true) {
const workflow = select(storeName).getWorkflowData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
...workflow,
status: deactivateWorkflowRuns
? WorkflowStatus.INACTIVE
: WorkflowStatus.DEACTIVATING,
},
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (deactivateWorkflowRuns && data?.data.status === WorkflowStatus.INACTIVE) {
void createNotice(
'success',
__('Automation is now deactivated!', 'mailpoet'),
{
type: 'snackbar',
},
);
}
if (
!deactivateWorkflowRuns &&
data?.data.status === WorkflowStatus.DEACTIVATING
) {
void createNotice(
'success',
__(
'Automation is deactivated. But recent users are still going through the flow.',
'mailpoet',
),
{
type: 'snackbar',
},
);
}
return {
type: 'DEACTIVATE',
workflow: data?.data ?? workflow,
} as const;
}
export function* trash(onTrashed: () => void = undefined) { export function* trash(onTrashed: () => void = undefined) {
const workflow = select(storeName).getWorkflowData(); const workflow = select(storeName).getWorkflowData();
const data = yield apiFetch({ const data = yield apiFetch({
@ -92,13 +162,13 @@ export function* trash(onTrashed: () => void = undefined) {
method: 'PUT', method: 'PUT',
data: { data: {
...workflow, ...workflow,
status: 'trash', status: WorkflowStatus.TRASH,
}, },
}); });
onTrashed?.(); onTrashed?.();
if (data?.status === 'trash') { if (data?.status === WorkflowStatus.TRASH) {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, { window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowDeleted]: workflow.id, [LISTING_NOTICE_PARAMETERS.workflowDeleted]: workflow.id,
}); });

View File

@ -39,6 +39,12 @@ export function reducer(state: State, action: Action): State {
workflowData: action.workflow, workflowData: action.workflow,
workflowSaved: true, workflowSaved: true,
}; };
case 'DEACTIVATE':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
};
case 'TRASH': case 'TRASH':
return { return {
...state, ...state,

View File

@ -2,7 +2,7 @@ import { createRegistrySelector } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface'; import { store as interfaceStore } from '@wordpress/interface';
import { store as preferencesStore } from '@wordpress/preferences'; import { store as preferencesStore } from '@wordpress/preferences';
import { storeName } from './constants'; 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 { Item } from '../components/inserter/item';
import { Step, Workflow } from '../components/workflow/types'; import { Step, Workflow } from '../components/workflow/types';
@ -78,6 +78,6 @@ export function getErrors(state: State): Errors | undefined {
return state.errors; 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; return state.errors?.steps[id] ?? undefined;
} }

View File

@ -34,13 +34,14 @@ export type StepType = {
background: string; background: string;
}; };
export type StepError = { export type StepErrors = {
step_id: string; step_id: string;
message: string; message: string;
fields: Record<string, string>;
}; };
export type Errors = { export type Errors = {
steps: Record<string, StepError>; steps: Record<string, StepErrors>;
}; };
export type State = { export type State = {

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

@ -6,27 +6,48 @@ import {
FlexItem, FlexItem,
} from '@wordpress/components'; } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data'; import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { PlainBodyTitle } from '../../../../editor/components/panel'; import { PlainBodyTitle } from '../../../../editor/components/panel';
import { storeName } from '../../../../editor/store'; import { storeName } from '../../../../editor/store';
import { DelayTypeOptions } from './types/delayTypes'; import { DelayTypeOptions } from './types/delayTypes';
export function Edit(): JSX.Element { export function Edit(): JSX.Element {
const { selectedStep } = useSelect( const { selectedStep, errors } = useSelect(
(select) => ({ (select) => ({
selectedStep: select(storeName).getSelectedStep(), 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 ( return (
<PanelBody opened> <PanelBody opened>
<PlainBodyTitle title="Wait for" /> <label htmlFor={delayValueInputId}>
<PlainBodyTitle
title={
// translators: A label for a wait delay time selection form field - time unit follows
__('Wait for', 'mailpoet')
}
/>
</label>
<Flex align="top"> <Flex align="top">
<FlexItem style={{ flex: '1 1 0' }}> <FlexItem
style={{ flex: '1 1 0' }}
className={
delayErrorMessage ? 'mailpoet-automation-field__error' : ''
}
>
<TextControl <TextControl
label="" id={delayValueInputId}
help={delayErrorMessage}
type="number" type="number"
placeholder="Number" placeholder={__('Number', 'mailpoet')}
value={(selectedStep.args.delay as string) ?? ''} value={(selectedStep.args.delay as string) ?? ''}
onChange={(rawValue) => { onChange={(rawValue) => {
const value: number = const value: number =
@ -41,9 +62,15 @@ export function Edit(): JSX.Element {
}} }}
/> />
</FlexItem> </FlexItem>
<FlexItem style={{ flex: '1 1 0' }}> <FlexItem
style={{ flex: '1 1 0' }}
className={
delayTypeErrorMessage ? 'mailpoet-automation-field__error' : ''
}
>
<SelectControl <SelectControl
label="" label=""
help={delayTypeErrorMessage}
value={(selectedStep.args.delay_type as string) ?? 'HOURS'} value={(selectedStep.args.delay_type as string) ?? 'HOURS'}
options={DelayTypeOptions} options={DelayTypeOptions}
onChange={(value) => onChange={(value) =>

View File

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

View File

@ -1,25 +1,35 @@
import { SelectControl } from '@wordpress/components'; import { SelectControl } from '@wordpress/components';
import { __, _n, sprintf } from '@wordpress/i18n';
export type DelayTypes = SelectControl.Option & { export type DelayTypes = SelectControl.Option & {
subtitle: (value: number) => string; subtitle: (value: number) => string;
}; };
export const DelayTypeOptions: DelayTypes[] = [ export const DelayTypeOptions: DelayTypes[] = [
{ {
label: 'Hours', label: __('Hours', 'mailpoet'),
subtitle: (value: number) => subtitle: (value: number) =>
`Wait for ${value} ${value === 1 ? 'hour' : 'hours'}`, sprintf(
_n('Wait for %d hour', 'Wait for %d hours', value, 'mailpoet'),
value,
),
value: 'HOURS', value: 'HOURS',
}, },
{ {
label: 'Days', label: __('Days', 'mailpoet'),
subtitle: (value: number) => 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', value: 'DAYS',
}, },
{ {
label: 'Weeks', label: __('Weeks', 'mailpoet'),
subtitle: (value: number) => 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', value: 'WEEKS',
}, },
]; ];

View File

@ -1,25 +1,52 @@
import { dispatch, useSelect } from '@wordpress/data'; import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { plus } from '@wordpress/icons'; import { plus } from '@wordpress/icons';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Button } from '../../../components/button'; import { Button } from '../../../components/button';
import { storeName } from '../../../../../editor/store'; import { storeName } from '../../../../../editor/store';
import { MailPoet } from '../../../../../../mailpoet'; 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 { export function EditNewsletter(): JSX.Element {
const [redirectToTemplateSelection, setRedirectToTemplateSelection] = const [redirectToTemplateSelection, setRedirectToTemplateSelection] =
useState(false); useState(false);
const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false);
const { selectedStep, workflowId, workflowSaved } = useSelect( const { selectedStep, workflowId, workflowSaved, errors } = useSelect(
(select) => ({ (select) => ({
selectedStep: select(storeName).getSelectedStep(), selectedStep: select(storeName).getSelectedStep(),
workflowId: select(storeName).getWorkflowData().id, workflowId: select(storeName).getWorkflowData().id,
workflowSaved: select(storeName).getWorkflowSaved(), workflowSaved: select(storeName).getWorkflowSaved(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}), }),
[], [],
); );
const emailId = selectedStep?.args?.email_id as number | undefined; const emailId = selectedStep?.args?.email_id as number | undefined;
const workflowStepId = selectedStep.id; const workflowStepId = selectedStep.id;
const errorFields = errors?.fields ?? {};
const emailIdError = errorFields?.email_id ?? '';
const createEmail = useCallback(async () => { const createEmail = useCallback(async () => {
setRedirectToTemplateSelection(true); setRedirectToTemplateSelection(true);
@ -56,16 +83,26 @@ export function EditNewsletter(): JSX.Element {
if (!emailId || redirectToTemplateSelection) { if (!emailId || redirectToTemplateSelection) {
return ( return (
<Button <div className={emailIdError ? 'mailpoet-automation-field__error' : ''}>
variant="sidebar-primary" <Button
centered variant="sidebar-primary"
icon={plus} centered
onClick={createEmail} icon={plus}
isBusy={redirectToTemplateSelection} onClick={createEmail}
disabled={redirectToTemplateSelection} isBusy={redirectToTemplateSelection}
> disabled={redirectToTemplateSelection}
Design email >
</Button> {__('Design email', 'mailpoet')}
</Button>
{emailIdError && (
<span className="mailpoet-automation-field-message">
{__(
'You need to design an email before you can activate the workflow',
'mailpoet',
)}
</span>
)}
</div>
); );
} }
@ -78,10 +115,21 @@ export function EditNewsletter(): JSX.Element {
selectedStep.args.email_id as string selectedStep.args.email_id as string
}`} }`}
> >
Edit content {__('Edit content', 'mailpoet')}
</Button> </Button>
<Button variant="secondary" centered> <Button
Preview 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> </Button>
</div> </div>
); );

View File

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

View File

@ -31,9 +31,9 @@ export function GoogleAnalyticsPanel(): JSX.Element {
); );
return ( return (
<PanelBody title="Google analytics" initialOpen={false}> <PanelBody title={__('Google analytics', 'mailpoet')} initialOpen={false}>
<ToggleControl <ToggleControl
label="Enable custom GA tracking" label={__('Enable custom GA tracking', 'mailpoet')}
checked={enabled} checked={enabled}
onChange={(value) => onChange={(value) =>
dispatch(storeName).updateStepArgs( dispatch(storeName).updateStepArgs(

View File

@ -1,11 +1,15 @@
import { PanelBody, TextControl, ToggleControl } from '@wordpress/components'; import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data'; import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../../../../editor/store'; import { storeName } from '../../../../../editor/store';
export function ReplyToPanel(): JSX.Element { export function ReplyToPanel(): JSX.Element {
const { selectedStep } = useSelect( const { selectedStep, errors } = useSelect(
(select) => ({ (select) => ({
selectedStep: select(storeName).getSelectedStep(), selectedStep: select(storeName).getSelectedStep(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}), }),
[], [],
); );
@ -18,10 +22,16 @@ export function ReplyToPanel(): JSX.Element {
const enabled = const enabled =
typeof replyToName !== 'undefined' || typeof replyToAddress !== 'undefined'; typeof replyToName !== 'undefined' || typeof replyToAddress !== 'undefined';
const errorFields = errors?.fields ?? {};
const replyToNameError = errorFields?.reply_to_name ?? '';
const replyToAddressError = errorFields?.reply_to_address ?? '';
return ( return (
<PanelBody title="Reply to" initialOpen={false}> <PanelBody title={__('Reply to', 'mailpoet')} initialOpen={false}>
<ToggleControl <ToggleControl
label="Use different email address for getting replies to the email" label={__(
'Use different email address for getting replies to the email',
'mailpoet',
)}
checked={enabled} checked={enabled}
onChange={(value) => { onChange={(value) => {
dispatch(storeName).updateStepArgs( dispatch(storeName).updateStepArgs(
@ -40,8 +50,15 @@ export function ReplyToPanel(): JSX.Element {
{enabled && ( {enabled && (
<> <>
<TextControl <TextControl
label="“Reply to” name" className={
placeholder="John Doe" 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={replyToName ?? ''} value={replyToName ?? ''}
onChange={(value) => onChange={(value) =>
dispatch(storeName).updateStepArgs( dispatch(storeName).updateStepArgs(
@ -53,9 +70,16 @@ export function ReplyToPanel(): JSX.Element {
/> />
<TextControl <TextControl
className={
replyToAddressError ? 'mailpoet-automation-field__error' : ''
}
help={replyToAddressError}
type="email" type="email"
label="“Reply to email address" label={__('"Reply to" email address', 'mailpoet')}
placeholder="you@domain.com" placeholder={
// translators: A placeholder for an email
__('you@domain.com', 'mailpoet')
}
value={replyToAddress ?? ''} value={replyToAddress ?? ''}
onChange={(value) => onChange={(value) =>
dispatch(storeName).updateStepArgs( dispatch(storeName).updateStepArgs(

View File

@ -1,3 +1,5 @@
import { __ } from '@wordpress/i18n';
export function ShortcodeHelpText(): JSX.Element { export function ShortcodeHelpText(): JSX.Element {
return ( return (
<span className="mailpoet-shortcode-selector"> <span className="mailpoet-shortcode-selector">
@ -8,7 +10,7 @@ export function ShortcodeHelpText(): JSX.Element {
rel="noopener noreferrer" rel="noopener noreferrer"
data-beacon-article="59d662ef042863379ddc6faa" data-beacon-article="59d662ef042863379ddc6faa"
> >
MailPoet shortcodes {__('MailPoet shortcodes', 'mailpoet')}
</a> </a>
</span> </span>
); );

View File

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

View File

@ -1,3 +1,4 @@
import { __ } from '@wordpress/i18n';
import { Icon } from './icon'; import { Icon } from './icon';
import { Edit } from './edit'; import { Edit } from './edit';
import { StepType } from '../../../../editor/store/types'; import { StepType } from '../../../../editor/store/types';
@ -5,9 +6,10 @@ import { StepType } from '../../../../editor/store/types';
export const step: StepType = { export const step: StepType = {
key: 'mailpoet:send-email', key: 'mailpoet:send-email',
group: 'actions', group: 'actions',
title: 'Send email', title: __('Send email', 'mailpoet'),
description: 'An email will be sent to subscriber', description: __('An email will be sent to subscriber', 'mailpoet'),
subtitle: (data) => (data.args.name as string) ?? 'Send email', subtitle: (data) =>
(data.args.name as string) ?? __('Send email', 'mailpoet'),
foreground: '#996800', foreground: '#996800',
background: '#FCF9E8', background: '#FCF9E8',
icon: Icon, icon: Icon,

View File

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

View File

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

View File

@ -0,0 +1,36 @@
import apiFetch, { APIFetchOptions } from '@wordpress/api-fetch';
import { dispatch, StoreDescriptor } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { ApiError } from '../api';
export const registerApiErrorHandler = (): void =>
apiFetch.use(
async (
options: APIFetchOptions,
next: (nextOptions: APIFetchOptions) => Promise<unknown>,
) => {
try {
const result = await next(options);
return result;
} catch (error) {
const errorObject = error as ApiError;
const status = errorObject.data?.status;
if (status && status >= 400 && status < 500) {
const message = errorObject.message;
void dispatch(noticesStore as StoreDescriptor).createErrorNotice(
message ?? __('An unknown error occurred.', 'mailpoet'),
{ explicitDismiss: true },
);
return undefined;
}
void dispatch(noticesStore as StoreDescriptor).createErrorNotice(
__('An unknown error occurred.', 'mailpoet'),
{ explicitDismiss: true },
);
throw error;
}
},
);

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
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';
type Props = {
workflow: Workflow;
};
export function Actions({ workflow }: 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 menuItems = [duplicate, trash, restore, del].filter((item) => item);
return (
<div className="mailpoet-automation-listing-cell-actions">
<EditWorkflow workflow={workflow} />
{menuItems.map(({ control, slot }) => (
<Fragment key={control.title}>{slot}</Fragment>
))}
<DropdownMenu
className="mailpoet-automation-listing-more-button"
label={__('More', 'mailpoet')}
icon={moreVertical}
controls={menuItems.map(({ control }) => control)}
popoverProps={{ position: 'bottom left' }}
/>
</div>
);
}

View File

@ -1,15 +0,0 @@
import { __ } from '@wordpress/i18n';
import { Workflow } from '../../workflow';
type Props = {
workflow: Workflow;
label?: string;
};
export function Edit({ workflow, label }: Props): JSX.Element {
return (
<a href={`admin.php?page=mailpoet-automation-editor&id=${workflow.id}`}>
{label ?? __('Edit', 'mailpoet')}
</a>
);
}

View File

@ -1,5 +1,4 @@
export * from './edit'; export * from './actions';
export * from './more';
export * from './name'; export * from './name';
export * from './status'; export * from './status';
export * from './subscribers'; export * from './subscribers';

View File

@ -1,25 +0,0 @@
import { EllipsisMenu, MenuItem } from '@woocommerce/components/build';
import { __ } from '@wordpress/i18n';
import { Workflow } from '../../workflow';
type Props = {
workflow: Workflow;
};
export function More({ workflow }: Props): JSX.Element {
return (
<EllipsisMenu
label={`Actions for ${workflow.name}`}
renderContent={() => (
<div>
<MenuItem onInvoke={() => {}}>
<p>{__('Duplicate', 'mailpoet')}</p>
</MenuItem>
<MenuItem onInvoke={() => {}}>
<p>{__('Move to trash', 'mailpoet')}</p>
</MenuItem>
</div>
)}
/>
);
}

View File

@ -1,4 +1,4 @@
import { Edit } from './edit'; import { EditWorkflow } from '../actions';
import { Workflow } from '../../workflow'; import { Workflow } from '../../workflow';
type Props = { type Props = {
@ -6,5 +6,5 @@ type Props = {
}; };
export function Name({ workflow }: Props): JSX.Element { export function Name({ workflow }: Props): JSX.Element {
return <Edit workflow={workflow} label={workflow.name} />; return <EditWorkflow workflow={workflow} label={workflow.name} />;
} }

View File

@ -1,6 +1,4 @@
import { useState } from 'react';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { ToggleControl } from '@wordpress/components';
import { Workflow, WorkflowStatus } from '../../workflow'; import { Workflow, WorkflowStatus } from '../../workflow';
type Props = { type Props = {
@ -8,14 +6,8 @@ type Props = {
}; };
export function Status({ workflow }: Props): JSX.Element { export function Status({ workflow }: Props): JSX.Element {
const [isActive, setIsActive] = useState(workflow.status === 'active');
return ( return (
<div className="mailpoet-automation-listing-cell-status"> <div className="mailpoet-automation-listing-cell-status">
<ToggleControl
checked={isActive}
onChange={(active) => setIsActive(active)}
/>
{workflow.status === WorkflowStatus.ACTIVE {workflow.status === WorkflowStatus.ACTIVE
? __('Active', 'mailpoet') ? __('Active', 'mailpoet')
: __('Not active', 'mailpoet')} : __('Not active', 'mailpoet')}

View File

@ -1,5 +1,6 @@
import { __ } from '@wordpress/i18n'; import { _x } from '@wordpress/i18n';
import { Workflow } from '../../workflow'; import { Workflow } from '../../workflow';
import { Statistics } from '../../../components/statistics';
type Props = { type Props = {
workflow: Workflow; workflow: Workflow;
@ -7,25 +8,28 @@ type Props = {
export function Subscribers({ workflow }: Props): JSX.Element { export function Subscribers({ workflow }: Props): JSX.Element {
return ( return (
<ul className="mailpoet-automation-stats"> <Statistics
<li className="mailpoet-automation-stats-item"> labelPosition="after"
{new Intl.NumberFormat().format(workflow.stats.totals.entered)} items={[
<span className="mailpoet-automation-stats-label"> {
{__('Entered', 'mailpoet')} key: 'entered',
</span> // translators: Total number of subscribers who entered an automation workflow
</li> label: _x('Entered', 'automation stats', 'mailpoet'),
<li className="mailpoet-automation-stats-item"> value: workflow.stats.totals.entered,
{new Intl.NumberFormat().format(workflow.stats.totals.in_progress)} },
<span className="mailpoet-automation-stats-label"> {
{__('Processing', 'mailpoet')} key: 'processing',
</span> // translators: Total number of subscribers who are being processed in an automation workflow
</li> label: _x('Processing', 'automation stats', 'mailpoet'),
<li className="mailpoet-automation-stats-item"> value: workflow.stats.totals.in_progress,
{new Intl.NumberFormat().format(workflow.stats.totals.exited)} },
<span className="mailpoet-automation-stats-label"> {
{__('Exited', 'mailpoet')} key: 'exited',
</span> // translators: Total number of subscribers who exited an automation workflow, no matter the result
</li> label: _x('Exited', 'automation stats', 'mailpoet'),
</ul> value: workflow.stats.totals.exited,
},
]}
/>
); );
} }

View File

@ -0,0 +1,44 @@
import { useState } from 'react';
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
export const useDeleteButton = (workflow: Workflow): Item | undefined => {
const [showDialog, setShowDialog] = useState(false);
const { deleteWorkflow } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
return undefined;
}
return {
key: 'delete',
control: {
title: __('Delete permanently', 'mailpoet'),
icon: null,
onClick: () => setShowDialog(true),
},
slot: (
<ConfirmDialog
isOpen={showDialog}
title={__('Permanently delete automation', 'mailpoet')}
confirmButtonText={__('Yes, permanently delete', 'mailpoet')}
__experimentalHideHeader={false}
onConfirm={() => deleteWorkflow(workflow)}
onCancel={() => setShowDialog(false)}
>
{sprintf(
// translators: %s is the workflow name
__(
'Are you sure you want to permanently delete "%s" and all associated data? This cannot be undone!',
'mailpoet',
),
workflow.name,
)}
</ConfirmDialog>
),
};
};

View File

@ -0,0 +1,22 @@
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
export const useDuplicateButton = (workflow: Workflow): Item | undefined => {
const { duplicateWorkflow } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) {
return undefined;
}
return {
key: 'duplicate',
control: {
title: __('Duplicate', 'mailpoet'),
icon: null,
onClick: () => duplicateWorkflow(workflow),
},
};
};

View File

@ -0,0 +1,4 @@
export * from './delete';
export * from './duplicate';
export * from './restore';
export * from './trash';

View File

@ -0,0 +1,9 @@
import { DropdownMenu } from '@wordpress/components';
import Control = DropdownMenu.Control;
export type Item = {
key: string;
control: Control;
slot?: JSX.Element;
};

View File

@ -0,0 +1,22 @@
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
export const useRestoreButton = (workflow: Workflow): Item | undefined => {
const { restoreWorkflow } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
return undefined;
}
return {
key: 'restore',
control: {
title: __('Restore', 'mailpoet'),
icon: null,
onClick: () => restoreWorkflow(workflow, WorkflowStatus.DRAFT),
},
};
};

View File

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

View File

@ -0,0 +1,49 @@
import { StoreDescriptor, useSelect, useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { Notice } from '../../../../notices/notice';
export function Notices(): JSX.Element {
const { notices } = useSelect(
(select) => ({
notices: select(noticesStore as StoreDescriptor).getNotices(),
}),
[],
);
const { removeNotice } = useDispatch(noticesStore as StoreDescriptor);
const dismissibleNotices = notices.filter(
({ isDismissible, type }) => isDismissible && type === 'default',
);
const nonDismissibleNotices = notices.filter(
({ isDismissible, type }) => !isDismissible && type === 'default',
);
return (
<>
{nonDismissibleNotices
.reverse()
.map(({ id, status, content, __unstableHTML }) => (
<Notice key={id} renderInPlace type={status} timeout={false}>
{__unstableHTML ?? <p>{content}</p>}
</Notice>
))}
{dismissibleNotices
.reverse()
.map(({ id, status, content, __unstableHTML }) => (
<Notice
key={id}
type={status}
renderInPlace
timeout={false}
closable
onClose={removeNotice}
>
{__unstableHTML ?? <p>{content}</p>}
</Notice>
))}
</>
);
}

View File

@ -1,5 +1,5 @@
import { Workflow } from './workflow'; import { Workflow } from './workflow';
import { Edit, More, Name, Status, Subscribers } from './components/cells'; import { Actions, Name, Status, Subscribers } from './components/cells';
export function getRow(workflow: Workflow): object[] { export function getRow(workflow: Workflow): object[] {
return [ return [
@ -21,12 +21,7 @@ export function getRow(workflow: Workflow): object[] {
{ {
id: workflow.id, id: workflow.id,
value: null, value: null,
display: <Edit workflow={workflow} />, display: <Actions workflow={workflow} />,
},
{
id: workflow.id,
value: null,
display: <More workflow={workflow} />,
}, },
]; ];
} }

View File

@ -1,40 +1,37 @@
import { Search, TableCard } from '@woocommerce/components/build'; import { Search, TableCard } from '@woocommerce/components/build';
import { TabPanel } from '@wordpress/components'; import { TabPanel } from '@wordpress/components';
import { __ } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useMemo } from 'react'; import { __, _x } from '@wordpress/i18n';
import { useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { getRow } from './get-row'; import { getRow } from './get-row';
import { storeName } from './store/constants';
import { Workflow, WorkflowStatus } from './workflow'; import { Workflow, WorkflowStatus } from './workflow';
type Props = { const tabConfig = [
workflows: Workflow[];
loading: boolean;
};
const filterTabs = [
{ {
name: 'all', name: 'all',
title: 'All', title: __('All', 'mailpoet'),
className: 'mailpoet-tab-all', className: 'mailpoet-tab-all',
}, },
{ {
name: WorkflowStatus.ACTIVE, name: WorkflowStatus.ACTIVE,
title: 'Active', title: __('Active', 'mailpoet'),
className: 'mailpoet-tab-active', className: 'mailpoet-tab-active',
}, },
{ {
name: WorkflowStatus.INACTIVE, name: WorkflowStatus.INACTIVE,
title: 'Inactive', title: __('Inactive', 'mailpoet'),
className: 'mailpoet-tab-inactive', className: 'mailpoet-tab-inactive',
}, },
{ {
name: WorkflowStatus.DRAFT, name: WorkflowStatus.DRAFT,
title: 'Draft', title: _x('Draft', 'noun', 'mailpoet'),
className: 'mailpoet-tab-draft', className: 'mailpoet-tab-draft',
}, },
{ {
name: WorkflowStatus.TRASH, name: WorkflowStatus.TRASH,
title: 'Trash', title: _x('Trash', 'noun', 'mailpoet'),
className: 'mailpoet-tab-trash', className: 'mailpoet-tab-trash',
}, },
] as const; ] as const;
@ -43,11 +40,10 @@ const tableHeaders = [
{ key: 'name', label: __('Name', 'mailpoet') }, { key: 'name', label: __('Name', 'mailpoet') },
{ key: 'subscribers', label: __('Subscribers', 'mailpoet') }, { key: 'subscribers', label: __('Subscribers', 'mailpoet') },
{ key: 'status', label: __('Status', 'mailpoet') }, { key: 'status', label: __('Status', 'mailpoet') },
{ key: 'edit' }, { key: 'actions' },
{ key: 'more' },
] as const; ] as const;
export function AutomationListing({ workflows, loading }: Props): JSX.Element { export function AutomationListing(): JSX.Element {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const pageSearch = useMemo( const pageSearch = useMemo(
@ -55,6 +51,22 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
[location], [location],
); );
const workflows = useSelect((select) => select(storeName).getWorkflows());
const { loadWorkflows } = useDispatch(storeName);
const status = pageSearch.get('status');
useEffect(() => {
loadWorkflows();
}, [loadWorkflows]);
// focus tab button on status change (needed due to the force re-mount below)
useLayoutEffect(() => {
if (status) {
document.querySelector<HTMLElement>(`.mailpoet-tab-${status}`)?.focus();
}
}, [status]);
const updateUrlSearchString = useCallback( const updateUrlSearchString = useCallback(
(search: Record<string, string>) => { (search: Record<string, string>) => {
const newSearch = new URLSearchParams({ const newSearch = new URLSearchParams({
@ -75,10 +87,8 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
); );
const groupedWorkflows = useMemo<Record<string, Workflow[]>>(() => { const groupedWorkflows = useMemo<Record<string, Workflow[]>>(() => {
const grouped = { const grouped = { all: [] };
all: [], (workflows ?? []).forEach((workflow) => {
};
workflows.forEach((workflow) => {
if (!grouped[workflow.status]) { if (!grouped[workflow.status]) {
grouped[workflow.status] = []; grouped[workflow.status] = [];
} }
@ -92,30 +102,27 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
const tabs = useMemo( const tabs = useMemo(
() => () =>
filterTabs.map((filterTab) => { tabConfig.map((tab) => {
const count = (groupedWorkflows[filterTab.name] || []).length; const count = (groupedWorkflows[tab.name] ?? []).length;
return { return {
name: filterTab.name, name: tab.name,
title: title: (
count > 0 ? ( <>
<> <span>{tab.title}</span>
<span>{filterTab.title}</span> {count > 0 && <span className="count">{count}</span>}
<span className="count">{count}</span> </>
</> ) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- typed as string but supports JSX
) : ( className: tab.className,
<span>{filterTab.title}</span>
),
className: filterTab.className,
}; };
}), }),
[groupedWorkflows], [groupedWorkflows],
); );
const tabRenderer = useCallback( const renderTabs = useCallback(
(tab) => { (tab) => {
const filteredWorkflows: Workflow[] = groupedWorkflows[tab.name] ?? []; const filteredWorkflows: Workflow[] = groupedWorkflows[tab.name] ?? [];
const rowsPerPage = parseInt(pageSearch.get('per_page') || '25', 10); const rowsPerPage = parseInt(pageSearch.get('per_page') ?? '25', 10);
const currentPage = parseInt(pageSearch.get('paged') || '1', 10); const currentPage = parseInt(pageSearch.get('paged') ?? '1', 10);
const start = (currentPage - 1) * rowsPerPage; const start = (currentPage - 1) * rowsPerPage;
const rows = filteredWorkflows const rows = filteredWorkflows
.map((workflow) => getRow(workflow)) .map((workflow) => getRow(workflow))
@ -125,7 +132,7 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
<TableCard <TableCard
className="mailpoet-automation-listing" className="mailpoet-automation-listing"
title="" title=""
isLoading={loading} isLoading={!workflows}
headers={tableHeaders} headers={tableHeaders}
rows={rows} rows={rows}
rowKey={(_, i) => filteredWorkflows[i].id} rowKey={(_, i) => filteredWorkflows[i].id}
@ -143,40 +150,30 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
allowFreeTextSearch allowFreeTextSearch
inlineTags inlineTags
key="search" key="search"
// onChange={ onSearchChange }
// placeholder={
// labels.placeholder ||
// __( 'Search by item name', 'woocommerce' )
// }
// selected={ searchedLabels }
type="custom" type="custom"
disabled={loading || workflows.length === 0} disabled={!workflows}
autocompleter={{}} autocompleter={{}}
/>, />,
]} ]}
/> />
); );
}, },
[workflows, groupedWorkflows, pageSearch, loading, updateUrlSearchString], [workflows, groupedWorkflows, pageSearch, updateUrlSearchString],
); );
return ( return (
<TabPanel <TabPanel
className="mailpoet-filter-tab-panel" className="mailpoet-filter-tab-panel"
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the Tab type actually expects a string for titles but won't render HTML,
// making it very difficult to style the count badges. It seems to be compatible with JSX
// elements, however.
tabs={tabs} tabs={tabs}
onSelect={(tabName) => { onSelect={(tabName) => {
if (pageSearch.get('status') !== tabName) { if (status !== tabName) {
updateUrlSearchString({ status: tabName }); updateUrlSearchString({ status: tabName });
} }
}} }}
initialTabName={pageSearch.get('status') || 'all'} initialTabName={status ?? 'all'}
key={pageSearch.get('status')} // Force re-render on browser forward/back key={status} // force re-mount on history change to switch tab (via "initialTabName")
> >
{tabRenderer} {renderTabs}
</TabPanel> </TabPanel>
); );
} }

View File

@ -0,0 +1,115 @@
import { dispatch, StoreDescriptor } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { Workflow, WorkflowStatus } from '../workflow';
import { EditWorkflow, UndoTrashButton } from '../components/actions';
const createSuccessNotice = (content: string, options?: unknown) =>
dispatch(noticesStore as StoreDescriptor).createSuccessNotice(
content,
options,
);
const removeNotice = (id: string) =>
dispatch(noticesStore as StoreDescriptor).removeNotice(id);
export function* loadWorkflows() {
const data = yield apiFetch({
path: `/workflows`,
});
return {
type: 'SET_WORKFLOWS',
workflows: data.data,
} as const;
}
export function* duplicateWorkflow(workflow: Workflow) {
const data = yield apiFetch({
path: `/workflows/${workflow.id}/duplicate`,
method: 'POST',
});
void createSuccessNotice(
// translators: %s is the automation workflow name
sprintf(__('Automation "%s" was duplicated.', 'mailpoet'), workflow.name),
);
return {
type: 'ADD_WORKFLOW',
workflow: data.data,
} as const;
}
export function* trashWorkflow(workflow: Workflow) {
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
status: WorkflowStatus.TRASH,
},
});
const message = __('1 automation moved to the Trash.', 'mailpoet');
void createSuccessNotice(message, {
id: `workflow-trashed-${workflow.id}`,
__unstableHTML: (
<p>
{message}{' '}
<UndoTrashButton workflow={workflow} previousStatus={workflow.status} />
</p>
),
});
return {
type: 'UPDATE_WORKFLOW',
workflow: data.data,
} as const;
}
export function* restoreWorkflow(workflow: Workflow, status: WorkflowStatus) {
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
status,
},
});
void removeNotice(`workflow-trashed-${workflow.id}`);
const message = __('1 automation restored from the Trash.', 'mailpoet');
void createSuccessNotice(message, {
__unstableHTML: (
<p>
{message}{' '}
<EditWorkflow
workflow={workflow}
label={__('Edit workflow', 'mailpoet')}
/>
</p>
),
});
return {
type: 'UPDATE_WORKFLOW',
workflow: data.data,
} as const;
}
export function* deleteWorkflow(workflow: Workflow) {
yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'DELETE',
});
void createSuccessNotice(
__('1 automation and all associated data permanently deleted.', 'mailpoet'),
);
return {
type: 'DELETE_WORKFLOW',
workflow,
} as const;
}

View File

@ -0,0 +1 @@
export const storeName = 'mailpoet/automation-listing';

View File

@ -0,0 +1,3 @@
export * from './constants';
export * from './store';
export * from './types';

View File

@ -0,0 +1,5 @@
import { State } from './types';
export const getInitialState = (): State => ({
workflows: undefined,
});

View File

@ -0,0 +1,34 @@
import { Action } from '@wordpress/data';
import { State } from './types';
import { Workflow } from '../workflow';
export function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_WORKFLOWS':
return {
...state,
workflows: action.workflows,
};
case 'ADD_WORKFLOW':
return {
...state,
workflows: [action.workflow, ...state.workflows],
};
case 'UPDATE_WORKFLOW':
return {
...state,
workflows: state.workflows.map((workflow: Workflow) =>
workflow.id === action.workflow.id ? action.workflow : workflow,
),
};
case 'DELETE_WORKFLOW':
return {
...state,
workflows: state.workflows.filter(
(workflow: Workflow) => workflow.id !== action.workflow.id,
),
};
default:
return state;
}
}

View File

@ -0,0 +1,11 @@
import { State } from './types';
import { Workflow } from '../workflow';
import { workflowCount } from '../../config';
export function getWorkflows(state: State): Workflow[] {
return state.workflows;
}
export function getWorkflowCount(state: State): number {
return state.workflows ? state.workflows.length : workflowCount;
}

View File

@ -0,0 +1,41 @@
import {
createReduxStore,
register,
StoreConfig,
StoreDescriptor,
} from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
import { storeName } from './constants';
import { getInitialState } from './initial_state';
import { reducer } from './reducer';
import * as actions from './actions';
import * as selectors from './selectors';
import { State } from './types';
import { OmitFirstArgs } from '../../../types';
type StoreType = Omit<StoreDescriptor, 'name'> & {
name: typeof storeName;
};
export const createStore = (): StoreType => {
const storeConfig = {
actions,
controls,
selectors,
reducer,
initialState: getInitialState(),
} as StoreConfig<State>;
const store = createReduxStore<State>(storeName, storeConfig) as StoreType;
register(store);
return store;
};
export type StoreKey = typeof storeName | StoreType;
declare module '@wordpress/data' {
function select(key: StoreKey): OmitFirstArgs<typeof selectors>;
function dispatch(key: StoreKey): typeof actions;
}
export { actions, selectors };

View File

@ -0,0 +1,5 @@
import { Workflow } from '../workflow';
export type State = {
workflows?: Workflow[];
};

View File

@ -3,6 +3,7 @@ export enum WorkflowStatus {
INACTIVE = 'inactive', INACTIVE = 'inactive',
DRAFT = 'draft', DRAFT = 'draft',
TRASH = 'trash', TRASH = 'trash',
DEACTIVATING = 'deactivating',
} }
export type Workflow = { export type Workflow = {

View File

@ -4,6 +4,7 @@ import { Flex } from '@wordpress/components';
import { workflowTemplates } from './config'; import { workflowTemplates } from './config';
import { TemplateListItem } from './components/template-list-item'; import { TemplateListItem } from './components/template-list-item';
import { initializeApi } from '../api'; import { initializeApi } from '../api';
import { registerTranslations } from '../i18n';
import { TopBarWithBeamer } from '../../common/top_bar/top_bar'; import { TopBarWithBeamer } from '../../common/top_bar/top_bar';
import { import {
FromScratchButton, FromScratchButton,
@ -37,6 +38,7 @@ window.addEventListener('DOMContentLoaded', () => {
return; return;
} }
registerTranslations();
initializeApi(); initializeApi();
ReactDOM.render(<Templates />, root); ReactDOM.render(<Templates />, root);
}); });

View File

@ -1,6 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { noop } from 'lodash';
import { MailPoet } from 'mailpoet'; import { MailPoet } from 'mailpoet';
import { Modal } from 'common/modal/modal'; import { Modal } from 'common/modal/modal';
import { import {
@ -99,7 +97,7 @@ function AuthorizeSenderDomainModal({
if (res.data.ok) { if (res.data.ok) {
// record verified, close the modal // record verified, close the modal
setErrorMessage(''); setErrorMessage('');
setVerifiedSenderDomain(senderDomain); setVerifiedSenderDomain?.(senderDomain);
onRequestClose(); onRequestClose();
} }
} catch (e) { } catch (e) {
@ -185,14 +183,4 @@ function AuthorizeSenderDomainModal({
); );
} }
AuthorizeSenderDomainModal.propTypes = {
senderDomain: PropTypes.string.isRequired,
useModal: PropTypes.bool,
};
AuthorizeSenderDomainModal.defaultProps = {
setVerifiedSenderDomain: noop,
useModal: true,
};
export { AuthorizeSenderDomainModal }; export { AuthorizeSenderDomainModal };

View File

@ -1,7 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import ReactStringReplace from 'react-string-replace'; import ReactStringReplace from 'react-string-replace';
import PropTypes from 'prop-types';
import { noop } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { MailPoet } from 'mailpoet'; import { MailPoet } from 'mailpoet';
import { Modal } from 'common/modal/modal'; import { Modal } from 'common/modal/modal';
@ -117,7 +115,7 @@ function AuthorizeSenderEmailModal({
setCreateEmailApiResponse(null); setCreateEmailApiResponse(null);
setShowLoader(false); setShowLoader(false);
setConfirmEmailApiResponse(true); setConfirmEmailApiResponse(true);
setAuthorizedAddress(senderEmailAddress); setAuthorizedAddress?.(senderEmailAddress);
removeUnauthorizedEmailNotices(); removeUnauthorizedEmailNotices();
}) })
.catch(() => { .catch(() => {
@ -238,14 +236,4 @@ function AuthorizeSenderEmailModal({
); );
} }
AuthorizeSenderEmailModal.propTypes = {
senderEmail: PropTypes.string.isRequired,
useModal: PropTypes.bool,
};
AuthorizeSenderEmailModal.defaultProps = {
setAuthorizedAddress: noop,
useModal: true,
};
export { AuthorizeSenderEmailModal }; export { AuthorizeSenderEmailModal };

View File

@ -6,13 +6,13 @@ type Event = {
name: string; name: string;
}; };
type Props = { export type TokenFieldProps = {
id?: string; id?: string;
label?: string; label?: string;
name?: string; name?: string;
placeholder?: string; placeholder?: string;
onChange: (event: Event) => void; onChange: (event: Event) => void;
selectedValues?: FormTokenField.Value[]; selectedValues?: FormTokenField.Value[] | [];
suggestedValues?: readonly string[]; suggestedValues?: readonly string[];
}; };
@ -24,7 +24,7 @@ export function TokenField({
selectedValues, selectedValues,
suggestedValues, suggestedValues,
onChange, onChange,
}: Props) { }: TokenFieldProps) {
const args = { const args = {
id, id,
label, label,

View File

@ -99,6 +99,7 @@ function SetFromAddressModal({ onRequestClose, setAuthorizedAddress }: Props) {
> >
{showAuthorizedEmailModal && ( {showAuthorizedEmailModal && (
<AuthorizeSenderEmailModal <AuthorizeSenderEmailModal
useModal
senderEmail={address} senderEmail={address}
onRequestClose={() => { onRequestClose={() => {
setShowAuthorizedEmailModal(false); setShowAuthorizedEmailModal(false);

View File

@ -2,6 +2,7 @@ import { Component } from 'react';
import { FormFieldText } from 'form/fields/text.jsx'; import { FormFieldText } from 'form/fields/text.jsx';
import jQuery from 'jquery'; import jQuery from 'jquery';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormFieldTextarea } from 'form/fields/textarea.jsx'; import { FormFieldTextarea } from 'form/fields/textarea.jsx';
import { FormFieldSelect } from 'form/fields/select.jsx'; import { FormFieldSelect } from 'form/fields/select.jsx';
@ -151,8 +152,26 @@ class FormField extends Component {
field = 'invalid'; field = 'invalid';
break; break;
} }
const isDisabled =
typeof this.props.field.disabled === 'function'
? this.props.field.disabled(this.props.field)
: this.props.field.disabled;
const eventListeners = {
...(this.props.field.onWrapperClick
? { onClick: this.props.field.onWrapperClick }
: {}),
};
return ( return (
<div className="mailpoet-form-field" key={`field-${data.index || 0}`}> <div
className={classNames('mailpoet-form-field', {
'mailpoet-form-field-disabled': isDisabled,
})}
key={`field-${data.index || 0}`}
{...eventListeners}
>
{field} {field}
{description} {description}
</div> </div>
@ -212,6 +231,8 @@ FormField.propTypes = {
label: PropTypes.string, label: PropTypes.string,
fields: PropTypes.arrayOf(PropTypes.object), fields: PropTypes.arrayOf(PropTypes.object),
description: PropTypes.string, description: PropTypes.string,
onWrapperClick: PropTypes.func,
disabled: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
}).isRequired, }).isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
}; };

View File

@ -1,7 +1,16 @@
import PropTypes from 'prop-types'; import { TokenFieldProps, TokenField } from 'common/form/tokenField/tokenField';
import { TokenField } from 'common/form/tokenField/tokenField'; import { FormTokenItem } from '../../automation/integrations/mailpoet/components/form-token-field';
function getItems(endpoint: string) { interface TokenFormFieldProps {
onValueChange: TokenFieldProps['onChange'];
item?: Record<string, FormTokenItem[]>;
field: Omit<TokenFieldProps, 'id' | 'onChange' | 'selectedValues'> & {
endpoint: string;
getName: (item: FormTokenItem) => string;
};
}
function getItems(endpoint: string): FormTokenItem[] {
let items = []; let items = [];
if (typeof window[`mailpoet_${endpoint}`] !== 'undefined') { if (typeof window[`mailpoet_${endpoint}`] !== 'undefined') {
items = window[`mailpoet_${endpoint}`]; items = window[`mailpoet_${endpoint}`];
@ -10,12 +19,15 @@ function getItems(endpoint: string) {
return items; return items;
} }
function FormFieldTokenField(props) { function FormFieldTokenField(props: TokenFormFieldProps) {
const selectedValues = Array.isArray(props.item[props.field.name]) const selectedValues: TokenFieldProps['selectedValues'] = Array.isArray(
? props.item[props.field.name].map((item) => props.field.getName(item)) props.item[props.field.name],
)
? props.field.name &&
props.item[props.field.name].map((item) => props.field.getName(item))
: []; : [];
let suggestedValues = []; let suggestedValues: readonly string[] = [];
if (props.field.endpoint) { if (props.field.endpoint) {
const items = getItems(String(props.field.endpoint)); const items = getItems(String(props.field.endpoint));
suggestedValues = items.map((item) => props.field.getName(item)); suggestedValues = items.map((item) => props.field.getName(item));
@ -35,16 +47,4 @@ function FormFieldTokenField(props) {
); );
} }
FormFieldTokenField.propTypes = {
onValueChange: PropTypes.func,
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
field: PropTypes.shape({
name: PropTypes.string,
label: PropTypes.string,
suggestedValues: PropTypes.arrayOf(PropTypes.string),
placeholder: PropTypes.string,
getName: PropTypes.func,
}).isRequired,
};
export { FormFieldTokenField }; export { FormFieldTokenField };

View File

@ -26,4 +26,5 @@ export type Field = {
getLabel?: (segment: Segment) => string; getLabel?: (segment: Segment) => string;
getCount?: (segment: Segment) => string; getCount?: (segment: Segment) => string;
transformChangedValue?: (arg: unknown) => Segment; transformChangedValue?: (arg: unknown) => Segment;
onWrapperClick?: () => void;
}; };

View File

@ -12,7 +12,23 @@ import { partial } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ColorGradientSettings } from '../components/color_gradient_settings'; import { ColorGradientSettings } from '../components/color_gradient_settings';
function InputStylesSettings({ styles, onChange }) { type InputStyles = {
fullWidth: boolean;
inheritFromTheme: boolean;
bold: boolean;
backgroundColor: string;
borderSize: number;
borderRadius: number;
borderColor: string;
fontColor: string;
};
type InputStylesSettingsProps = {
styles: InputStyles;
onChange: (styles: InputStyles) => void;
};
function InputStylesSettings({ styles, onChange }: InputStylesSettingsProps) {
const localStylesRef = useRef(styles); const localStylesRef = useRef(styles);
const localStyles = localStylesRef.current; const localStyles = localStylesRef.current;
@ -132,6 +148,10 @@ function InputStylesSettings({ styles, onChange }) {
); );
} }
/**
* @deprecated since removal of propTypes for InputStylesSettings
* Remove when TextInputEdit is converted to tsx
*/
export const inputStylesPropTypes = PropTypes.shape({ export const inputStylesPropTypes = PropTypes.shape({
fullWidth: PropTypes.bool.isRequired, fullWidth: PropTypes.bool.isRequired,
inheritFromTheme: PropTypes.bool.isRequired, inheritFromTheme: PropTypes.bool.isRequired,
@ -142,9 +162,4 @@ export const inputStylesPropTypes = PropTypes.shape({
borderColor: PropTypes.string, borderColor: PropTypes.string,
}); });
InputStylesSettings.propTypes = {
styles: inputStylesPropTypes.isRequired,
onChange: PropTypes.func.isRequired,
};
export { InputStylesSettings }; export { InputStylesSettings };

View File

@ -6,7 +6,6 @@ import {
SelectControl, SelectControl,
} from '@wordpress/components'; } from '@wordpress/components';
import { MailPoet } from 'mailpoet'; import { MailPoet } from 'mailpoet';
import PropTypes from 'prop-types';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { partial } from 'lodash'; import { partial } from 'lodash';
import { HorizontalAlignment } from 'common/styles'; import { HorizontalAlignment } from 'common/styles';
@ -18,7 +17,12 @@ import { CloseButtonsSettings } from 'form_editor/components/close_button_settin
import { formStyles as defaultFormStyles } from 'form_editor/store/defaults'; import { formStyles as defaultFormStyles } from 'form_editor/store/defaults';
import { FontFamilySettings } from '../font_family_settings'; import { FontFamilySettings } from '../font_family_settings';
function StylesSettingsPanel({ onToggle, isOpened }) { type StylesSettingsPanelProps = {
onToggle: PanelBody.Props['onToggle'];
isOpened: boolean;
};
function StylesSettingsPanel({ onToggle, isOpened }: StylesSettingsPanelProps) {
const { changeFormSettings } = useDispatch('mailpoet-form-editor'); const { changeFormSettings } = useDispatch('mailpoet-form-editor');
const settings = useSelect( const settings = useSelect(
(select) => select('mailpoet-form-editor').getFormSettings(), (select) => select('mailpoet-form-editor').getFormSettings(),
@ -165,9 +169,4 @@ function StylesSettingsPanel({ onToggle, isOpened }) {
); );
} }
StylesSettingsPanel.propTypes = {
onToggle: PropTypes.func.isRequired,
isOpened: PropTypes.bool.isRequired,
};
export { StylesSettingsPanel }; export { StylesSettingsPanel };

View File

@ -22,6 +22,6 @@
} }
}, },
"parent": ["woocommerce/checkout-contact-information-block"], "parent": ["woocommerce/checkout-contact-information-block"],
"editorScript": "file:../../../dist/js/marketing_optin_block/marketing-optin-block.js", "editorScript": "file:./marketing-optin-block.js",
"editorStyle": "file:../../../dist/js/marketing_optin_block/marketing-optin-block.css" "editorStyle": "file:./marketing-optin-block.css"
} }

View File

@ -21,11 +21,14 @@ function EmptyState(): JSX.Element {
return ( return (
<Placeholder <Placeholder
icon={<Icon icon={megaphone} />} icon={<Icon icon={megaphone} />}
label={__('marketing-opt-in-label', 'mailpoet')} label={__('Marketing opt-in', 'mailpoet')}
className="wp-block-mailpoet-newsletter-block-placeholder" className="wp-block-mailpoet-newsletter-block-placeholder"
> >
<span className="wp-block-mailpoet-newsletter-block-placeholder__description"> <span className="wp-block-mailpoet-newsletter-block-placeholder__description">
{__('marketing-opt-in-not-shown', 'mailpoet')} {__(
'MailPoet marketing opt-in would be shown here if enabled. You can enable from the settings page.',
'mailpoet',
)}
</span> </span>
<Button <Button
isPrimary isPrimary
@ -34,7 +37,7 @@ function EmptyState(): JSX.Element {
rel="noopener noreferrer" rel="noopener noreferrer"
className="wp-block-mailpoet-newsletter-block-placeholder__button" className="wp-block-mailpoet-newsletter-block-placeholder__button"
> >
{__('marketing-opt-in-enable', 'mailpoet')} {__('Enable opt-in for Checkout', 'mailpoet')}
</Button> </Button>
</Placeholder> </Placeholder>
); );

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