Move dynamic segments JS and CSS from Premium plugin
[MAILPOET-2382]
This commit is contained in:
committed by
Jack Kitterhing
parent
80f335fee3
commit
2f7ad3b35d
@ -2,6 +2,7 @@
|
|||||||
@import '../../../node_modules/select2/dist/css/select2';
|
@import '../../../node_modules/select2/dist/css/select2';
|
||||||
@import 'components/automaticEmails';
|
@import 'components/automaticEmails';
|
||||||
@import 'components/datepicker/datepicker';
|
@import 'components/datepicker/datepicker';
|
||||||
|
@import 'components/dynamicSegments';
|
||||||
@import 'components/common';
|
@import 'components/common';
|
||||||
@import 'components/modal';
|
@import 'components/modal';
|
||||||
@import 'components/notice';
|
@import 'components/notice';
|
||||||
|
7
assets/css/src/components/_dynamicSegments.scss
Normal file
7
assets/css/src/components/_dynamicSegments.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.form-field-row-filters div {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.stats-create-segment {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
22
assets/js/src/dynamic_segments/dynamic_segments.jsx
Normal file
22
assets/js/src/dynamic_segments/dynamic_segments.jsx
Normal file
@ -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(
|
||||||
|
(
|
||||||
|
<HashRouter>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/new" component={DynamicSegmentForm} />
|
||||||
|
<Route path="/edit/:id" component={DynamicSegmentForm} />
|
||||||
|
<Route path="*" component={DynamicSegmentList} />
|
||||||
|
</Switch>
|
||||||
|
</HashRouter>
|
||||||
|
), container
|
||||||
|
);
|
||||||
|
}
|
70
assets/js/src/dynamic_segments/filters/email.jsx
Normal file
70
assets/js/src/dynamic_segments/filters/email.jsx
Normal file
@ -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;
|
||||||
|
});
|
45
assets/js/src/dynamic_segments/filters/woocommerce.jsx
Normal file
45
assets/js/src/dynamic_segments/filters/woocommerce.jsx
Normal file
@ -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);
|
||||||
|
};
|
13
assets/js/src/dynamic_segments/filters/wordpress_role.jsx
Normal file
13
assets/js/src/dynamic_segments/filters/wordpress_role.jsx
Normal file
@ -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 })
|
||||||
|
), {}),
|
||||||
|
},
|
||||||
|
]);
|
186
assets/js/src/dynamic_segments/form.jsx
Normal file
186
assets/js/src/dynamic_segments/form.jsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<h1 className="title">
|
||||||
|
{MailPoet.I18n.t('formPageTitle')}
|
||||||
|
{' '}
|
||||||
|
<Link className="page-title-action" to="/">{MailPoet.I18n.t('backToList')}</Link>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
endpoint="dynamic_segments"
|
||||||
|
fields={fields}
|
||||||
|
params={match.params}
|
||||||
|
messages={messages}
|
||||||
|
onChange={this.handleValueChange}
|
||||||
|
onSubmit={this.handleSave}
|
||||||
|
onItemLoad={this.onItemLoad}
|
||||||
|
item={item}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
172
assets/js/src/dynamic_segments/list.jsx
Normal file
172
assets/js/src/dynamic_segments/list.jsx
Normal file
@ -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) => (
|
||||||
|
<Link to={`/edit/${item.id}`}>
|
||||||
|
{MailPoet.I18n.t('edit')}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'view_subscribers',
|
||||||
|
link: (item) => (
|
||||||
|
<a href={item.subscribers_url}>
|
||||||
|
{MailPoet.I18n.t('viewSubscribers')}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'trash',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderItem(item, actions) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<td data-colname={MailPoet.I18n.t('nameColumn')}>
|
||||||
|
<strong>
|
||||||
|
{ item.name }
|
||||||
|
</strong>
|
||||||
|
{ actions }
|
||||||
|
</td>
|
||||||
|
<td className="column" data-colname={MailPoet.I18n.t('subscribersCountColumn')}>
|
||||||
|
{ parseInt(item.count, 10).toLocaleString() }
|
||||||
|
</td>
|
||||||
|
<td className="column" data-colname={MailPoet.I18n.t('updatedAtColumn')}>
|
||||||
|
{ MailPoet.Date.format(item.updated_at) }
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DynamicSegmentList(props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="title">
|
||||||
|
{MailPoet.I18n.t('pageTitle')}
|
||||||
|
{' '}
|
||||||
|
<Link
|
||||||
|
className="page-title-action"
|
||||||
|
to="/new"
|
||||||
|
data-automation-id="new-segment"
|
||||||
|
>
|
||||||
|
{MailPoet.I18n.t('new')}
|
||||||
|
</Link>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<Listing
|
||||||
|
limit={window.mailpoet_listing_per_page}
|
||||||
|
location={props.location}
|
||||||
|
params={props.match.params}
|
||||||
|
search
|
||||||
|
onRenderItem={renderItem}
|
||||||
|
endpoint="dynamic_segments"
|
||||||
|
columns={columns}
|
||||||
|
messages={messages}
|
||||||
|
sort_by="created_at"
|
||||||
|
sort_order="desc"
|
||||||
|
item_actions={itemActions}
|
||||||
|
/>
|
||||||
|
<p className="mailpoet_sending_methods_help help">
|
||||||
|
<b>
|
||||||
|
{MailPoet.I18n.t('segmentsTip')}
|
||||||
|
:
|
||||||
|
</b>
|
||||||
|
{' '}
|
||||||
|
{MailPoet.I18n.t('segmentsTipText')}
|
||||||
|
{' '}
|
||||||
|
<a
|
||||||
|
href="https://kb.mailpoet.com/article/237-guide-to-subscriber-segmentation?utm_source=plugin&utm_medium=segments&utm_campaign=helpdocs"
|
||||||
|
data-beacon-article="5a574bd92c7d3a194368233e"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{MailPoet.I18n.t('segmentsTipLink')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
Reference in New Issue
Block a user