From 2f7ad3b35d44f0835194ef029fdb5e5cbfc032c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Thu, 3 Oct 2019 10:50:04 +0200 Subject: [PATCH] Move dynamic segments JS and CSS from Premium plugin [MAILPOET-2382] --- assets/css/src/admin.scss | 1 + .../css/src/components/_dynamicSegments.scss | 7 + .../src/dynamic_segments/dynamic_segments.jsx | 22 +++ .../js/src/dynamic_segments/filters/email.jsx | 70 +++++++ .../dynamic_segments/filters/woocommerce.jsx | 45 +++++ .../filters/wordpress_role.jsx | 13 ++ assets/js/src/dynamic_segments/form.jsx | 186 ++++++++++++++++++ assets/js/src/dynamic_segments/list.jsx | 172 ++++++++++++++++ 8 files changed, 516 insertions(+) create mode 100644 assets/css/src/components/_dynamicSegments.scss create mode 100644 assets/js/src/dynamic_segments/dynamic_segments.jsx create mode 100644 assets/js/src/dynamic_segments/filters/email.jsx create mode 100644 assets/js/src/dynamic_segments/filters/woocommerce.jsx create mode 100644 assets/js/src/dynamic_segments/filters/wordpress_role.jsx create mode 100644 assets/js/src/dynamic_segments/form.jsx create mode 100644 assets/js/src/dynamic_segments/list.jsx diff --git a/assets/css/src/admin.scss b/assets/css/src/admin.scss index fa0195e592..0c8b8676f6 100644 --- a/assets/css/src/admin.scss +++ b/assets/css/src/admin.scss @@ -2,6 +2,7 @@ @import '../../../node_modules/select2/dist/css/select2'; @import 'components/automaticEmails'; @import 'components/datepicker/datepicker'; +@import 'components/dynamicSegments'; @import 'components/common'; @import 'components/modal'; @import 'components/notice'; diff --git a/assets/css/src/components/_dynamicSegments.scss b/assets/css/src/components/_dynamicSegments.scss new file mode 100644 index 0000000000..3aab7a13df --- /dev/null +++ b/assets/css/src/components/_dynamicSegments.scss @@ -0,0 +1,7 @@ +.form-field-row-filters div { + margin-bottom: 10px; +} + +.button.stats-create-segment { + margin-top: 3px; +} diff --git a/assets/js/src/dynamic_segments/dynamic_segments.jsx b/assets/js/src/dynamic_segments/dynamic_segments.jsx new file mode 100644 index 0000000000..3d1f058cb7 --- /dev/null +++ b/assets/js/src/dynamic_segments/dynamic_segments.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter, Switch, Route } from 'react-router-dom'; + +import DynamicSegmentList from './list.jsx'; +import DynamicSegmentForm from './form.jsx'; + +const container = document.getElementById('dynamic_segments_container'); + +if (container) { + ReactDOM.render( + ( + + + + + + + + ), container + ); +} diff --git a/assets/js/src/dynamic_segments/filters/email.jsx b/assets/js/src/dynamic_segments/filters/email.jsx new file mode 100644 index 0000000000..482fdafa5c --- /dev/null +++ b/assets/js/src/dynamic_segments/filters/email.jsx @@ -0,0 +1,70 @@ +import MailPoet from 'mailpoet'; + +const loadedLinks = {}; + +function loadLinks(formItems) { + if (formItems.action !== 'clicked' && formItems.action !== 'notClicked') return Promise.resolve(); + if (!formItems.newsletter_id) return Promise.resolve(); + if (loadedLinks[formItems.newsletter_id] !== undefined) { + return Promise.resolve(loadedLinks[formItems.newsletter_id]); + } + + return MailPoet.Ajax.post({ + api_version: window.mailpoet_api_version, + endpoint: 'newsletter_links', + action: 'get', + data: { + newsletterId: formItems.newsletter_id, + }, + }) + .then((response) => { + const { data } = response; + loadedLinks[formItems.newsletter_id] = data; + return data; + }) + .fail((response) => { + MailPoet.Notice.error( + response.errors.map((error) => error.message), + { scroll: true } + ); + }); +} + +export default (formItems) => loadLinks(formItems).then((links) => { + const basicFields = [ + { + name: 'action', + type: 'select', + values: { + '': MailPoet.I18n.t('selectActionPlaceholder'), + opened: MailPoet.I18n.t('emailActionOpened'), + notOpened: MailPoet.I18n.t('emailActionNotOpened'), + clicked: MailPoet.I18n.t('emailActionClicked'), + notClicked: MailPoet.I18n.t('emailActionNotClicked'), + }, + }, + { + name: 'newsletter_id', + type: 'selection', + resetSelect2OnUpdate: true, + endpoint: 'newsletters_list', + placeholder: MailPoet.I18n.t('selectNewsletterPlaceholder'), + forceSelect2: true, + getLabel: (newsletter) => { + const sentAt = (newsletter.sent_at) ? MailPoet.Date.format(newsletter.sent_at) : MailPoet.I18n.t('notSentYet'); + return `${newsletter.subject} (${sentAt})`; + }, + }, + ]; + if (links) { + return [...basicFields, { + name: 'link_id', + type: 'selection', + placeholder: MailPoet.I18n.t('selectLinkPlaceholder'), + forceSelect2: true, + getLabel: (link) => link.url, + values: links, + }]; + } + return basicFields; +}); diff --git a/assets/js/src/dynamic_segments/filters/woocommerce.jsx b/assets/js/src/dynamic_segments/filters/woocommerce.jsx new file mode 100644 index 0000000000..b2060360b2 --- /dev/null +++ b/assets/js/src/dynamic_segments/filters/woocommerce.jsx @@ -0,0 +1,45 @@ +import MailPoet from 'mailpoet'; +import _ from 'underscore'; + +const actionsField = { + name: 'action', + type: 'select', + values: { + '': MailPoet.I18n.t('selectActionPlaceholder'), + purchasedCategory: MailPoet.I18n.t('wooPurchasedCategory'), + purchasedProduct: MailPoet.I18n.t('wooPurchasedProduct'), + }, +}; + +const categoriesField = { + name: 'category_id', + type: 'selection', + endpoint: 'product_categories', + resetSelect2OnUpdate: true, + placeholder: MailPoet.I18n.t('selectWooPurchasedCategory'), + forceSelect2: true, + getLabel: _.property('cat_name'), + getValue: _.property('term_id'), +}; + +const productsField = { + name: 'product_id', + type: 'selection', + endpoint: 'products', + resetSelect2OnUpdate: true, + placeholder: MailPoet.I18n.t('selectWooPurchasedProduct'), + forceSelect2: true, + getLabel: _.property('title'), + getValue: _.property('ID'), +}; + +export default (formItems) => { + const formFields = [actionsField]; + if (formItems.action === 'purchasedCategory') { + formFields.push(categoriesField); + } + if (formItems.action === 'purchasedProduct') { + formFields.push(productsField); + } + return Promise.resolve(formFields); +}; diff --git a/assets/js/src/dynamic_segments/filters/wordpress_role.jsx b/assets/js/src/dynamic_segments/filters/wordpress_role.jsx new file mode 100644 index 0000000000..1ac04e15df --- /dev/null +++ b/assets/js/src/dynamic_segments/filters/wordpress_role.jsx @@ -0,0 +1,13 @@ +import _ from 'underscore'; +import MailPoet from 'mailpoet'; + +export default () => Promise.resolve([ + { + name: 'wordpressRole', + type: 'select', + placeholder: MailPoet.I18n.t('selectUserRolePlaceholder'), + values: window.wordpress_editable_roles_list.reduce((currentValue, accumulator) => ( + _.extend({}, currentValue, { [accumulator.role_id]: accumulator.role_name }) + ), {}), + }, +]); diff --git a/assets/js/src/dynamic_segments/form.jsx b/assets/js/src/dynamic_segments/form.jsx new file mode 100644 index 0000000000..29b99429e4 --- /dev/null +++ b/assets/js/src/dynamic_segments/form.jsx @@ -0,0 +1,186 @@ +import React from 'react'; +import _ from 'underscore'; +import { Link, withRouter } from 'react-router-dom'; +import MailPoet from 'mailpoet'; +import Form from 'form'; +import PropTypes from 'prop-types'; + +import wordpressRoleFields from './filters/wordpress_role.jsx'; +import emailFields from './filters/email.jsx'; +import woocommerceFields from './filters/woocommerce.jsx'; + +const messages = { + onUpdate: () => MailPoet.Notice.success(MailPoet.I18n.t('segmentUpdated')), + onCreate: (data) => { + MailPoet.Notice.success(MailPoet.I18n.t('segmentAdded')); + MailPoet.trackEvent('Segments > Add new', { + 'MailPoet Free version': window.mailpoet_version, + type: data.segmentType || 'unknown type', + subtype: data.action || data.wordpressRole || 'unknown subtype', + }); + }, +}; + +function getAvailableFilters() { + const filters = { + email: MailPoet.I18n.t('email'), + userRole: MailPoet.I18n.t('wpUserRole'), + }; + if (window.is_woocommerce_active) { + filters.woocommerce = MailPoet.I18n.t('woocommerce'); + } + return filters; +} + +class DynamicSegmentForm extends React.Component { + constructor(props) { + super(props); + this.state = { + item: { + segmentType: 'email', + }, + childFields: [], + errors: undefined, + }; + this.loadFields(); + this.handleValueChange = this.handleValueChange.bind(this); + this.handleSave = this.handleSave.bind(this); + this.onItemLoad = this.onItemLoad.bind(this); + } + + onItemLoad(loadedData) { + const item = _.mapObject(loadedData, (val) => (_.isNull(val) ? '' : val)); + this.setState({ item }, this.loadFields); + } + + getFields() { + const { childFields } = this.state; + return [ + { + name: 'name', + label: MailPoet.I18n.t('name'), + type: 'text', + }, + { + name: 'description', + label: MailPoet.I18n.t('description'), + type: 'textarea', + tip: MailPoet.I18n.t('descriptionTip'), + }, + { + name: 'filters', + description: 'main', + label: MailPoet.I18n.t('formSegmentTitle'), + fields: [ + { + name: 'segmentType', + type: 'select', + values: getAvailableFilters(), + }, + ...childFields, + ], + }, + ]; + } + + getChildFields() { + const { item } = this.state; + switch (item.segmentType) { + case 'userRole': + return wordpressRoleFields(); + + case 'email': + return emailFields(item); + + case 'woocommerce': + return woocommerceFields(item); + + default: return []; + } + } + + loadFields() { + this.getChildFields().then((fields) => this.setState({ + childFields: fields, + })); + } + + handleValueChange(e) { + const { item } = this.state; + const field = e.target.name; + + item[field] = e.target.value; + + this.setState({ + item, + }); + this.loadFields(); + return true; + } + + handleSave(e) { + const { item } = this.state; + const { history, match } = this.props; + + e.preventDefault(); + this.setState({ errors: undefined }); + MailPoet.Ajax.post({ + api_version: window.mailpoet_api_version, + endpoint: 'dynamic_segments', + action: 'save', + data: item, + }).done(() => { + history.push('/'); + + if (match.params.id !== undefined) { + messages.onUpdate(); + } else { + messages.onCreate(item); + } + }).fail((response) => { + if (response.errors.length > 0) { + this.setState({ errors: response.errors }); + } + }); + } + + render() { + const fields = this.getFields(); + const { match } = this.props; + const { item, errors } = this.state; + return ( + <> +

+ {MailPoet.I18n.t('formPageTitle')} + {' '} + {MailPoet.I18n.t('backToList')} +

+ +
+ + ); + } +} + +DynamicSegmentForm.propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.string, + }).isRequired, + }).isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, +}; + +export default withRouter(DynamicSegmentForm); diff --git a/assets/js/src/dynamic_segments/list.jsx b/assets/js/src/dynamic_segments/list.jsx new file mode 100644 index 0000000000..5bf1dc1525 --- /dev/null +++ b/assets/js/src/dynamic_segments/list.jsx @@ -0,0 +1,172 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import MailPoet from 'mailpoet'; +import Listing from 'listing'; +import PropTypes from 'prop-types'; + +const columns = [ + { + name: 'name', + label: MailPoet.I18n.t('nameColumn'), + sortable: true, + }, + { + name: 'count', + label: MailPoet.I18n.t('subscribersCountColumn'), + sortable: false, + }, + { + name: 'updated_at', + label: MailPoet.I18n.t('updatedAtColumn'), + sortable: true, + }, +]; + +const messages = { + onLoadingItems: () => MailPoet.I18n.t('loadingDynamicSegmentItems'), + onNoItemsFound: () => MailPoet.I18n.t('noDynamicSegmentItemsFound'), + onTrash: (response) => { + const count = Number(response.meta.count); + let message = null; + + if (count === 1) { + message = ( + MailPoet.I18n.t('oneSegmentTrashed') + ); + } else { + message = ( + MailPoet.I18n.t('multipleSegmentsTrashed') + ).replace('%$1d', count.toLocaleString()); + } + MailPoet.Notice.success(message); + }, + onDelete: (response) => { + const count = Number(response.meta.count); + let message = null; + + if (count === 1) { + message = ( + MailPoet.I18n.t('oneSegmentDeleted') + ); + } else { + message = ( + MailPoet.I18n.t('multipleSegmentsDeleted') + ).replace('%$1d', count.toLocaleString()); + } + MailPoet.Notice.success(message); + }, + onRestore: (response) => { + const count = Number(response.meta.count); + let message = null; + + if (count === 1) { + message = ( + MailPoet.I18n.t('oneSegmentRestored') + ); + } else { + message = ( + MailPoet.I18n.t('multipleSegmentsRestored') + ).replace('%$1d', count.toLocaleString()); + } + MailPoet.Notice.success(message); + }, +}; + +const itemActions = [ + { + name: 'edit', + link: (item) => ( + + {MailPoet.I18n.t('edit')} + + ), + }, + { + name: 'view_subscribers', + link: (item) => ( + + {MailPoet.I18n.t('viewSubscribers')} + + ), + }, + { + name: 'trash', + }, +]; + +function renderItem(item, actions) { + return ( + <> + + + { item.name } + + { actions } + + + { parseInt(item.count, 10).toLocaleString() } + + + { MailPoet.Date.format(item.updated_at) } + + + ); +} + +function DynamicSegmentList(props) { + return ( + <> +

+ {MailPoet.I18n.t('pageTitle')} + {' '} + + {MailPoet.I18n.t('new')} + +

+ + +

+ + {MailPoet.I18n.t('segmentsTip')} +: + + {' '} + {MailPoet.I18n.t('segmentsTipText')} + {' '} + + {MailPoet.I18n.t('segmentsTipLink')} + +

+ + ); +} + +DynamicSegmentList.propTypes = { + location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + match: PropTypes.shape({ + params: PropTypes.object, // eslint-disable-line react/forbid-prop-types + }).isRequired, +}; + +export default DynamicSegmentList;