Move dynamic segments JS and CSS from Premium plugin

[MAILPOET-2382]
This commit is contained in:
Jan Jakeš
2019-10-03 10:50:04 +02:00
committed by Jack Kitterhing
parent 80f335fee3
commit 2f7ad3b35d
8 changed files with 516 additions and 0 deletions

View File

@ -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';

View File

@ -0,0 +1,7 @@
.form-field-row-filters div {
margin-bottom: 10px;
}
.button.stats-create-segment {
margin-top: 3px;
}

View 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
);
}

View 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;
});

View 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);
};

View 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 })
), {}),
},
]);

View 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);

View 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;