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 'components/automaticEmails';
|
||||
@import 'components/datepicker/datepicker';
|
||||
@import 'components/dynamicSegments';
|
||||
@import 'components/common';
|
||||
@import 'components/modal';
|
||||
@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