WiP on admin interface for subscribers.
TODO: - format data based on field info in listDTAjax - integrate with the whole subscription machinery
This commit is contained in:
parent
e6bd9cd943
commit
6f5b50e932
38 changed files with 1233 additions and 181 deletions
|
@ -9,7 +9,7 @@ import {
|
|||
Dropdown, StaticField, CheckBox
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling } from '../lib/error-handling';
|
||||
import { DeleteModalDialog } from '../lib/delete';
|
||||
import { DeleteModalDialog } from '../lib/modals';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import { UnsubscriptionMode } from '../../../shared/lists';
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'
|
|||
import { Table } from '../lib/table';
|
||||
import axios from '../lib/axios';
|
||||
import {Link} from "react-router-dom";
|
||||
import {Icon} from "../lib/bootstrap-components";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -66,28 +67,28 @@ export default class List extends Component {
|
|||
|
||||
if (perms.includes('viewSubscriptions')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-user" aria-hidden="true" title="Subscribers"></span>,
|
||||
label: <Icon icon="user" title="Subscribers"/>,
|
||||
link: `/lists/${data[0]}/subscriptions`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/lists/${data[0]}/edit`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('manageFields')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-th-list" aria-hidden="true" title="Manage Fields"></span>,
|
||||
label: <Icon icon="th-list" title={t('Manage Fields')}/>,
|
||||
link: `/lists/${data[0]}/fields`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||
link: `/lists/${data[0]}/share`
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField
|
||||
} from '../../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import {DeleteModalDialog} from "../../lib/delete";
|
||||
import { getFieldTypes } from './field-types';
|
||||
import {DeleteModalDialog} from "../../lib/modals";
|
||||
import { getFieldTypes } from './helpers';
|
||||
import interoperableErrors from '../../../../shared/interoperable-errors';
|
||||
import validators from '../../../../shared/validators';
|
||||
import slugify from 'slugify';
|
||||
|
@ -86,7 +86,7 @@ export default class CUD extends Component {
|
|||
|
||||
case 'radio-enum':
|
||||
case 'dropdown-enum':
|
||||
data.enumOptions = this.renderEnumOptions(data.settings.enumOptions);
|
||||
data.enumOptions = this.renderEnumOptions(data.settings.options);
|
||||
data.renderTemplate = data.settings.renderTemplate;
|
||||
break;
|
||||
|
||||
|
@ -151,7 +151,9 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
const defaultValue = state.getIn(['default_value', 'value']);
|
||||
if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
|
||||
if (defaultValue === '') {
|
||||
state.setIn(['default_value', 'error'], null);
|
||||
} else if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
|
||||
state.setIn(['default_value', 'error'], t('Default value is not integer number'));
|
||||
} else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) {
|
||||
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted date'));
|
||||
|
@ -168,7 +170,7 @@ export default class CUD extends Component {
|
|||
} else {
|
||||
state.setIn(['enumOptions', 'error'], null);
|
||||
|
||||
if (defaultValue !== '' && !(defaultValue in enumOptions.options)) {
|
||||
if (defaultValue !== '' && !(enumOptions.options.find(x => x.key === defaultValue))) {
|
||||
state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +182,7 @@ export default class CUD extends Component {
|
|||
parseEnumOptions(text) {
|
||||
const t = this.props.t;
|
||||
const errors = [];
|
||||
const options = {};
|
||||
const options = [];
|
||||
|
||||
const lines = text.split('\n');
|
||||
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
||||
|
@ -191,7 +193,7 @@ export default class CUD extends Component {
|
|||
if (matches) {
|
||||
const key = matches[1].trim();
|
||||
const label = matches[2].trim();
|
||||
options[key] = label;
|
||||
options.push({ key, label });
|
||||
} else {
|
||||
errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1}));
|
||||
}
|
||||
|
@ -210,7 +212,7 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
renderEnumOptions(options) {
|
||||
return Object.keys(options).map(key => `${key}|${options[key]}`).join('\n');
|
||||
return options.map(opt => `${opt.key}|${opt.label}`).join('\n');
|
||||
}
|
||||
|
||||
|
||||
|
@ -250,7 +252,7 @@ export default class CUD extends Component {
|
|||
|
||||
case 'radio-enum':
|
||||
case 'dropdown-enum':
|
||||
data.settings.enumOptions = this.parseEnumOptions(data.enumOptions).options;
|
||||
data.settings.options = this.parseEnumOptions(data.enumOptions).options;
|
||||
data.settings.renderTemplate = data.renderTemplate;
|
||||
break;
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ import { translate } from 'react-i18next';
|
|||
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
|
||||
import { withErrorHandling } from '../../lib/error-handling';
|
||||
import { Table } from '../../lib/table';
|
||||
import { getFieldTypes } from './field-types';
|
||||
import { getFieldTypes } from './helpers';
|
||||
import {Icon} from "../../lib/bootstrap-components";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -40,7 +41,7 @@ export default class List extends Component {
|
|||
{ data: 3, title: t('Merge Tag') },
|
||||
{
|
||||
actions: data => [{
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '../../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import { validateNamespace, NamespaceSelect } from '../../lib/namespace';
|
||||
import {DeleteModalDialog} from "../../lib/delete";
|
||||
import {DeleteModalDialog} from "../../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
|
|
|
@ -6,6 +6,7 @@ import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} f
|
|||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import { Table } from '../../lib/table';
|
||||
import axios from '../../lib/axios';
|
||||
import {Icon} from "../../lib/bootstrap-components";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -52,13 +53,13 @@ export default class List extends Component {
|
|||
|
||||
if (perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/lists/forms/${data[0]}/edit`
|
||||
});
|
||||
}
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||
link: `/lists/forms/${data[0]}/share`
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
import qs from 'querystringify';
|
||||
|
||||
import { Section } from '../lib/page';
|
||||
import ListsList from './List';
|
||||
|
@ -13,6 +14,7 @@ import FormsCUD from './forms/CUD';
|
|||
import FieldsList from './fields/List';
|
||||
import FieldsCUD from './fields/CUD';
|
||||
import SubscriptionsList from './subscriptions/List';
|
||||
import SubscriptionsCUD from './subscriptions/CUD';
|
||||
import SegmentsList from './segments/List';
|
||||
import SegmentsCUD from './segments/CUD';
|
||||
import Share from '../shares/Share';
|
||||
|
@ -41,13 +43,35 @@ const getStructure = t => {
|
|||
subscriptions: {
|
||||
title: t('Subscribers'),
|
||||
resolve: {
|
||||
segments: params => `/rest/segments/${params.listId}`
|
||||
segments: params => `/rest/segments/${params.listId}`,
|
||||
},
|
||||
extraParams: [':segmentId?'],
|
||||
link: params => `/lists/${params.listId}/subscriptions`,
|
||||
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
|
||||
render: props => <SubscriptionsList list={props.resolved.list} segments={props.resolved.segments} segmentId={props.match.params.segmentId} />
|
||||
},
|
||||
render: props => <SubscriptionsList list={props.resolved.list} segments={props.resolved.segments} segmentId={qs.parse(props.location.search).segment} />,
|
||||
children: {
|
||||
':subscriptionId([0-9]+)': {
|
||||
title: resolved => resolved.subscription.email,
|
||||
resolve: {
|
||||
subscription: params => `/rest/subscriptions/${params.listId}/${params.subscriptionId}`,
|
||||
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
|
||||
},
|
||||
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
title: t('Edit'),
|
||||
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
|
||||
render: props => <SubscriptionsCUD action={props.match.params.action} entity={props.resolved.subscription} list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
title: t('Create'),
|
||||
resolve: {
|
||||
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
|
||||
},
|
||||
render: props => <SubscriptionsCUD action="create" list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
|
||||
}
|
||||
} },
|
||||
':action(edit|delete)': {
|
||||
title: t('Edit'),
|
||||
link: params => `/lists/${params.listId}/edit`,
|
||||
|
|
|
@ -6,7 +6,7 @@ import {translate} from "react-i18next";
|
|||
import {NavButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from "../../lib/page";
|
||||
import {Button as FormButton, ButtonRow, Dropdown, Form, FormSendMethod, InputField, withForm} from "../../lib/form";
|
||||
import {withAsyncErrorHandler, withErrorHandling} from "../../lib/error-handling";
|
||||
import {DeleteModalDialog} from "../../lib/delete";
|
||||
import {DeleteModalDialog} from "../../lib/modals";
|
||||
import interoperableErrors from "../../../../shared/interoperable-errors";
|
||||
|
||||
import styles from "./CUD.scss";
|
||||
|
@ -15,7 +15,7 @@ import HTML5Backend from "react-dnd-html5-backend";
|
|||
import TouchBackend from "react-dnd-touch-backend";
|
||||
import SortableTree from "react-sortable-tree";
|
||||
import {ActionLink, Button, Icon} from "../../lib/bootstrap-components";
|
||||
import {getRuleHelpers} from "./rule-helpers";
|
||||
import {getRuleHelpers} from "./helpers";
|
||||
import RuleSettingsPane from "./RuleSettingsPane";
|
||||
|
||||
// https://stackoverflow.com/a/4819886/1601953
|
||||
|
@ -381,8 +381,8 @@ export default class CUD extends Component {
|
|||
canDrop={ data => !data.nextParent || (ruleHelpers.isCompositeRuleType(data.nextParent.rule.type)) }
|
||||
generateNodeProps={data => ({
|
||||
buttons: [
|
||||
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.showRuleOptions(data.node.rule)} className={styles.ruleActionLink}><Icon name="edit"/></ActionLink>,
|
||||
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.deleteRule(data.node.rule)} className={styles.ruleActionLink}><Icon name="remove"/></ActionLink>
|
||||
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.showRuleOptions(data.node.rule)} className={styles.ruleActionLink}><Icon icon="edit" title={t('Edit')}/></ActionLink>,
|
||||
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.deleteRule(data.node.rule)} className={styles.ruleActionLink}><Icon icon="remove" title={t('Delete')}/></ActionLink>
|
||||
]
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { translate } from 'react-i18next';
|
|||
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
|
||||
import { withErrorHandling } from '../../lib/error-handling';
|
||||
import { Table } from '../../lib/table';
|
||||
import {Icon} from "../../lib/bootstrap-components";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -32,7 +33,7 @@ export default class List extends Component {
|
|||
{ data: 1, title: t('Name') },
|
||||
{
|
||||
actions: data => [{
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/lists/${this.props.list.id}/segments/${data[0]}/edit`
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import {translate} from "react-i18next";
|
|||
import {requiresAuthenticatedUser, withPageHelpers} from "../../lib/page";
|
||||
import {Button, ButtonRow, Dropdown, Form, TableSelect, withForm} from "../../lib/form";
|
||||
import {withErrorHandling} from "../../lib/error-handling";
|
||||
import {getRuleHelpers} from "./rule-helpers";
|
||||
import {getFieldTypes} from "../fields/field-types";
|
||||
import {getRuleHelpers} from "./helpers";
|
||||
import {getFieldTypes} from "../fields/helpers";
|
||||
|
||||
import styles from "./CUD.scss";
|
||||
|
||||
|
|
|
@ -241,8 +241,8 @@ export function getRuleHelpers(t, fields) {
|
|||
rule.value = parseInt(getter('value'));
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['value', 'value']);
|
||||
if (!value) {
|
||||
const value = state.getIn(['value', 'value']).trim();
|
||||
if (value === '') {
|
||||
state.setIn(['value', 'error'], t('Value must not be empty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['value', 'error'], t('Value must be a number'));
|
213
client/src/lists/subscriptions/CUD.js
Normal file
213
client/src/lists/subscriptions/CUD.js
Normal file
|
@ -0,0 +1,213 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {HTTPMethod} from '../../lib/axios';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
|
||||
import {
|
||||
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
|
||||
Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField, CheckBox
|
||||
} from '../../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import {DeleteModalDialog, RestActionModalDialog} from "../../lib/modals";
|
||||
import interoperableErrors from '../../../../shared/interoperable-errors';
|
||||
import validators from '../../../../shared/validators';
|
||||
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
|
||||
import { SubscriptionStatus } from '../../../../shared/lists';
|
||||
import {getFieldTypes, getSubscriptionStatusLabels} from './helpers';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class CUD extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.subscriptionStatusLabels = getSubscriptionStatusLabels(t);
|
||||
this.fieldTypes = getFieldTypes(t);
|
||||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: `/rest/subscriptions-validate/${this.props.list.id}`,
|
||||
changed: ['email'],
|
||||
extra: ['id']
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
action: PropTypes.string.isRequired,
|
||||
list: PropTypes.object,
|
||||
fieldsGrouped: PropTypes.array,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
data.status = data.status.toString();
|
||||
data.tz = data.tz || '';
|
||||
|
||||
for (const fld of this.props.fieldsGrouped) {
|
||||
this.fieldTypes[fld.type].assignFormData(fld, data);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
const data = {
|
||||
email: '',
|
||||
tz: '',
|
||||
is_test: false,
|
||||
status: SubscriptionStatus.SUBSCRIBED
|
||||
};
|
||||
|
||||
for (const fld of this.props.fieldsGrouped) {
|
||||
this.fieldTypes[fld.type].initFormData(fld, data);
|
||||
}
|
||||
|
||||
this.populateFormValues(data);
|
||||
}
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
const emailServerValidation = state.getIn(['email', 'serverValidation']);
|
||||
if (!state.getIn(['email', 'value'])) {
|
||||
state.setIn(['email', 'error'], t('Email must not be empty'));
|
||||
} else if (!emailServerValidation) {
|
||||
state.setIn(['email', 'error'], t('Validation is in progress...'));
|
||||
} else if (emailServerValidation.exists) {
|
||||
state.setIn(['email', 'error'], t('Another subscription with the same email already exists.'));
|
||||
} else {
|
||||
state.setIn(['email', 'error'], null);
|
||||
}
|
||||
|
||||
for (const fld of this.props.fieldsGrouped) {
|
||||
this.fieldTypes[fld.type].validate(fld, state);
|
||||
}
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = `/rest/subscriptions/${this.props.list.id}`
|
||||
}
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
data.status = parseInt(data.status);
|
||||
data.tz = data.tz || null;
|
||||
|
||||
for (const fld of this.props.fieldsGrouped) {
|
||||
this.fieldTypes[fld.type].assignEntity(fld, data);
|
||||
}
|
||||
});
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/subscriptions`, 'success', t('Susbscription saved'));
|
||||
} else {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.DuplicitEmailError) {
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('It seems that another subscription with the same email has been created in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
||||
const fieldsGrouped = this.props.fieldsGrouped;
|
||||
|
||||
const statusOptions = Object.keys(this.subscriptionStatusLabels)
|
||||
.map(key => ({key, label: this.subscriptionStatusLabels[key]}));
|
||||
|
||||
const tzOptions = [
|
||||
{ key: '', label: t('Not selected') },
|
||||
...moment.tz.names().map(tz => ({ key: tz.toLowerCase(), label: tz }))
|
||||
];
|
||||
|
||||
const customFields = [];
|
||||
for (const fld of this.props.fieldsGrouped) {
|
||||
customFields.push(this.fieldTypes[fld.type].form(fld));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isEdit &&
|
||||
<div>
|
||||
<RestActionModalDialog
|
||||
title={t('Confirm deletion')}
|
||||
message={t('Are you sure you want to delete subscription for "{{email}}"?', {name: this.getFormValue('email')})}
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
actionMethod={HTTPMethod.DELETE}
|
||||
actionUrl={`/rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`}
|
||||
backUrl={`/lists/${this.props.list.id}/subscriptions/${this.props.entity.id}/edit`}
|
||||
successUrl={`/lists/${this.props.list.id}/subscriptions`}
|
||||
actionInProgressMsg={t('Deleting subscription ...')}
|
||||
actionDoneMsg={t('Subscription deleted')}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Title>{isEdit ? t('Edit Subscription') : t('Create Subscription')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="email" label={t('Email')}/>
|
||||
|
||||
{customFields}
|
||||
|
||||
<hr />
|
||||
|
||||
<Dropdown id="tz" label={t('Timezone')} options={tzOptions}/>
|
||||
|
||||
<Dropdown id="status" label={t('Subscription status')} options={statusOptions}/>
|
||||
|
||||
<CheckBox id="is_test" text={t('Test user?')} help={t('If checked then this subscription can be used for previewing campaign messages')}/>
|
||||
|
||||
{!isEdit &&
|
||||
<AlignedRow>
|
||||
<p className="text-warning">
|
||||
This person will not receive a confirmation email so make sure that you have permission to
|
||||
email them.
|
||||
</p>
|
||||
</AlignedRow>
|
||||
}
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/subscriptions/${this.props.entity.id}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,9 @@ import {
|
|||
Dropdown, Form,
|
||||
withForm
|
||||
} from '../../lib/form';
|
||||
import {Icon} from "../../lib/bootstrap-components";
|
||||
import axios from '../../lib/axios';
|
||||
import {getSubscriptionStatusLabels} from './helpers';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -23,19 +26,15 @@ export default class List extends Component {
|
|||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.subscriptionStatusLabels = {
|
||||
[SubscriptionStatus.SUBSCRIBED]: t('Subscribed'),
|
||||
[SubscriptionStatus.UNSUBSCRIBED]: t('Unubscribed'),
|
||||
[SubscriptionStatus.BOUNCED]: t('Bounced'),
|
||||
[SubscriptionStatus.COMPLAINED]: t('Complained'),
|
||||
};
|
||||
this.subscriptionStatusLabels = getSubscriptionStatusLabels(t);
|
||||
|
||||
this.initForm({
|
||||
onChange: {
|
||||
segment: (newState, key, oldValue, value) => {
|
||||
this.navigateTo(`/lists/${this.props.list.id}/subscriptions` + (value ? '/' + value : ''));
|
||||
this.navigateTo(`/lists/${this.props.list.id}/subscriptions` + (value ? '?segment=' + value : ''));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -61,6 +60,24 @@ export default class List extends Component {
|
|||
this.updateSegmentSelection(nextProps);
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async deleteSubscription(id) {
|
||||
await axios.delete(`/rest/subscriptions/${this.props.list.id}/${id}`);
|
||||
this.subscriptionsTable.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async unsubscribeSubscription(id) {
|
||||
await axios.post(`/rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`);
|
||||
this.subscriptionsTable.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async blacklistSubscription(id) {
|
||||
await axios.post(`/rest/XXX/${this.props.list.id}/${id}`); // FIXME - add url one the blacklist functionality is in
|
||||
this.subscriptionsTable.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const list = this.props.list;
|
||||
|
@ -72,19 +89,53 @@ export default class List extends Component {
|
|||
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
|
||||
];
|
||||
|
||||
let colIdx = 5;
|
||||
for (const fld of list.listFields) {
|
||||
columns.push({
|
||||
data: colIdx,
|
||||
title: fld.name
|
||||
});
|
||||
|
||||
colIdx += 1;
|
||||
}
|
||||
|
||||
if (list.permissions.includes('manageSubscriptions')) {
|
||||
columns.push({
|
||||
actions: data => [{
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: `/lists/${this.props.list.id}/subscriptions/${data[0]}/edit`
|
||||
}]
|
||||
actions: data => {
|
||||
const actions = [];
|
||||
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/lists/${this.props.list.id}/subscriptions/${data[0]}/edit`
|
||||
});
|
||||
|
||||
if (data[3] === SubscriptionStatus.SUBSCRIBED) {
|
||||
actions.push({
|
||||
label: <Icon icon="off" title={t('Unsubscribe')}/>,
|
||||
action: () => this.unsubscribeSubscription(data[0])
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME - add condition here to show it only if not blacklisted already
|
||||
actions.push({
|
||||
label: <Icon icon="ban-circle" title={t('Blacklist')}/>,
|
||||
action: () => this.blacklistSubscription(data[0])
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: <Icon icon="remove" title={t('Remove')}/>,
|
||||
action: () => this.deleteSubscription(data[0])
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const segmentOptions = [
|
||||
{key: '', label: t('All subscriptions')},
|
||||
...segments.map(x => ({ key: x.id.toString(), label: x.name}))
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
let dataUrl = '/rest/subscriptions-table/' + list.id;
|
||||
|
|
175
client/src/lists/subscriptions/helpers.js
Normal file
175
client/src/lists/subscriptions/helpers.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {SubscriptionStatus} from "../../../../shared/lists";
|
||||
import {ACEEditor, CheckBoxGroup, DatePicker, Dropdown, InputField, RadioGroup, TextArea} from "../../lib/form";
|
||||
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
||||
import {getFieldKey} from '../../../../shared/lists';
|
||||
|
||||
export function getSubscriptionStatusLabels(t) {
|
||||
|
||||
const subscriptionStatusLabels = {
|
||||
[SubscriptionStatus.SUBSCRIBED]: t('Subscribed'),
|
||||
[SubscriptionStatus.UNSUBSCRIBED]: t('Unubscribed'),
|
||||
[SubscriptionStatus.BOUNCED]: t('Bounced'),
|
||||
[SubscriptionStatus.COMPLAINED]: t('Complained'),
|
||||
};
|
||||
|
||||
return subscriptionStatusLabels;
|
||||
}
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const fieldTypes = {};
|
||||
|
||||
const stringFieldType = long => ({
|
||||
form: field => long ? <TextArea key={getFieldKey(field)} id={getFieldKey(field)} label={field.name}/> : <InputField key={getFieldKey(field)} id={getFieldKey(field)} label={field.name}/>,
|
||||
assignFormData: (field, data) => {},
|
||||
initFormData: (field, data) => {
|
||||
data[getFieldKey(field)] = '';
|
||||
},
|
||||
assignEntity: (field, data) => {},
|
||||
validate: (field, state) => {}
|
||||
});
|
||||
|
||||
const numberFieldType = {
|
||||
form: field => <InputField key={getFieldKey(field)} id={getFieldKey(field)} label={field.name}/>,
|
||||
assignFormData: (field, data) => {
|
||||
const value = data[getFieldKey(field)];
|
||||
data[getFieldKey(field)] = value ? value.toString() : '';
|
||||
},
|
||||
initFormData: (field, data) => {
|
||||
data[getFieldKey(field)] = '';
|
||||
},
|
||||
assignEntity: (field, data) => {
|
||||
data[getFieldKey(field)] = parseInt(data[getFieldKey(field)]);
|
||||
},
|
||||
validate: (field, state) => {
|
||||
const value = state.getIn([getFieldKey(field), 'value']).trim();
|
||||
if (value !== '' && isNaN(value)) {
|
||||
state.setIn([getFieldKey(field), 'error'], t('Value must be a number'));
|
||||
} else {
|
||||
state.setIn([getFieldKey(field), 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateFieldType = {
|
||||
form: field => <DatePicker key={getFieldKey(field)} id={getFieldKey(field)} label={field.name} dateFormat={field.settings.dateFormat} />,
|
||||
assignFormData: (field, data) => {
|
||||
const value = data[getFieldKey(field)];
|
||||
data[getFieldKey(field)] = value ? formatDate(field.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (field, data) => {
|
||||
data[getFieldKey(field)] = '';
|
||||
},
|
||||
assignEntity: (field, data) => {
|
||||
const date = parseDate(field.settings.dateFormat, data[getFieldKey(field)]);
|
||||
data[getFieldKey(field)] = date;
|
||||
},
|
||||
validate: (field, state) => {
|
||||
const value = state.getIn([getFieldKey(field), 'value']);
|
||||
const date = parseDate(field.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldKey(field), 'error'], t('Date is invalid'));
|
||||
} else {
|
||||
state.setIn([getFieldKey(field), 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const birthdayFieldType = {
|
||||
form: field => <DatePicker key={getFieldKey(field)} id={getFieldKey(field)} label={field.name} dateFormat={field.settings.dateFormat} birthday />,
|
||||
assignFormData: (field, data) => {
|
||||
const value = data[getFieldKey(field)];
|
||||
data[getFieldKey(field)] = value ? formatBirthday(field.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (field, data) => {
|
||||
data[getFieldKey(field)] = '';
|
||||
},
|
||||
assignEntity: (field, data) => {
|
||||
const date = parseBirthday(field.settings.dateFormat, data[getFieldKey(field)]);
|
||||
data[getFieldKey(field)] = date;
|
||||
},
|
||||
validate: (field, state) => {
|
||||
const value = state.getIn([getFieldKey(field), 'value']);
|
||||
const date = parseBirthday(field.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldKey(field), 'error'], t('Date is invalid'));
|
||||
} else {
|
||||
state.setIn([getFieldKey(field), 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const jsonFieldType = {
|
||||
form: field => <ACEEditor key={getFieldKey(field)} id={getFieldKey(field)} label={field.name} mode="json" height="300px"/>,
|
||||
assignFormData: (field, data) => {},
|
||||
initFormData: (field, data) => {
|
||||
data[getFieldKey(field)] = '';
|
||||
},
|
||||
assignEntity: (field, data) => {},
|
||||
validate: (field, state) => {}
|
||||
};
|
||||
|
||||
const enumSingleFieldType = componentType => ({
|
||||
form: field => React.createElement(componentType, { key: getFieldKey(field), id: getFieldKey(field), label: field.name, options: field.settings.options }, null),
|
||||
assignFormData: (field, data) => {
|
||||
if (data[getFieldKey(field)] === null) {
|
||||
if (field.default_value) {
|
||||
data[getFieldKey(field)] = field.default_value;
|
||||
} else if (field.settings.options.length > 0) {
|
||||
data[getFieldKey(field)] = field.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldKey(field)] = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
initFormData: (field, data) => {
|
||||
if (field.default_value) {
|
||||
data[getFieldKey(field)] = field.default_value;
|
||||
} else if (field.settings.options.length > 0) {
|
||||
data[getFieldKey(field)] = field.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldKey(field)] = '';
|
||||
}
|
||||
},
|
||||
assignEntity: (field, data) => {
|
||||
},
|
||||
validate: (field, state) => {}
|
||||
});
|
||||
|
||||
const enumMultipleFieldType = componentType => ({
|
||||
form: field => React.createElement(componentType, { key: getFieldKey(field), id: getFieldKey(field), label: field.name, options: field.settings.options }, null),
|
||||
assignFormData: (field, data) => {
|
||||
if (data[getFieldKey(field)] === null) {
|
||||
data[getFieldKey(field)] = [];
|
||||
}
|
||||
},
|
||||
initFormData: (field, data) => {
|
||||
data[getFieldKey(field)] = [];
|
||||
},
|
||||
assignEntity: (field, data) => {},
|
||||
validate: (field, state) => {}
|
||||
});
|
||||
|
||||
|
||||
fieldTypes.text = stringFieldType(false);
|
||||
fieldTypes.website = stringFieldType(false);
|
||||
fieldTypes.longtext = stringFieldType(true);
|
||||
fieldTypes.gpg = stringFieldType(true);
|
||||
fieldTypes.number = numberFieldType;
|
||||
fieldTypes.date = dateFieldType;
|
||||
fieldTypes.birthday = birthdayFieldType;
|
||||
fieldTypes.json = jsonFieldType;
|
||||
fieldTypes['dropdown-enum'] = enumSingleFieldType(Dropdown);
|
||||
fieldTypes['radio-enum'] = enumSingleFieldType(RadioGroup);
|
||||
|
||||
// Here we rely on the fact the model/fields and model/subscriptions preprocess the field info and subscription
|
||||
// such that the grouped entries behave the same as the enum entries
|
||||
fieldTypes['checkbox-grouped'] = enumMultipleFieldType(CheckBoxGroup);
|
||||
fieldTypes['radio-grouped'] = enumSingleFieldType(RadioGroup);
|
||||
fieldTypes['dropdown-grouped'] = enumSingleFieldType(Dropdown);
|
||||
|
||||
return fieldTypes;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue