Work in progress on subscriptions

This commit is contained in:
Tomas Bures 2017-08-13 20:11:58 +02:00
parent d9211377dd
commit e73c0a8b28
42 changed files with 1558 additions and 678 deletions

View file

@ -36,7 +36,8 @@ const FormSendMethod = {
class Form extends Component {
static propTypes = {
stateOwner: PropTypes.object.isRequired,
onSubmitAsync: PropTypes.func
onSubmitAsync: PropTypes.func,
inline: PropTypes.bool
}
static childContextTypes = {
@ -77,7 +78,7 @@ class Form extends Component {
}
} else {
return (
<form className="form-horizontal" onSubmit={::this.onSubmit}>
<form className={props.inline ? 'form-inline' : 'form-horizontal'} onSubmit={::this.onSubmit}>
<fieldset disabled={owner.isFormDisabled()}>
{props.children}
</fieldset>
@ -105,25 +106,53 @@ class Fieldset extends Component {
}
}
function wrapInput(id, htmlId, owner, label, help, input) {
const helpBlock = help ? <div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help'}>{help}</div> : '';
function wrapInput(id, htmlId, owner, label, help, input, inline) {
const className = id ? owner.addFormValidationClass('form-group', id) : 'form-group';
return (
<div className={id ? owner.addFormValidationClass('form-group', id) : 'form-group'} >
<div className="col-sm-2">
<label htmlFor={htmlId} className="control-label">{label}</label>
let helpBlock = null;
if (help) {
helpBlock = <div className={'help-block' + (!inline ? ' col-sm-offset-2 col-sm-10' : '')}
id={htmlId + '_help'}>{help}</div>;
}
let validationBlock = null;
if (id) {
const validationMsg = id && owner.getFormValidationMessage(id);
if (validationMsg) {
validationBlock = <div className={'help-block' + (!inline ? ' col-sm-offset-2 col-sm-10' : '')}
id={htmlId + '_help_validation'}>{validationMsg}</div>;
}
}
const labelBlock = <label htmlFor={htmlId} className="control-label">{label}</label>;
if (inline) {
return (
<div className={className} >
{labelBlock} &nbsp; {input}
{helpBlock}
{validationBlock}
</div>
<div className="col-sm-10">
{input}
);
} else {
return (
<div className={className} >
<div className="col-sm-2">
{labelBlock}
</div>
<div className="col-sm-10">
{input}
</div>
{helpBlock}
{validationBlock}
</div>
{helpBlock}
{id && <div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help_validation'}>{owner.getFormValidationMessage(id)}</div>}
</div>
);
);
}
}
function wrapInputInline(id, htmlId, owner, containerClass, label, text, help, input) {
function wrapInputWithText(id, htmlId, owner, containerClass, label, text, help, input) {
const helpBlock = help ? <div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help'}>{help}</div> : '';
const validationMsg = id && owner.getFormValidationMessage(id);
return (
<div className={id ? owner.addFormValidationClass('form-group', id) : 'form-group'} >
@ -134,7 +163,7 @@ function wrapInputInline(id, htmlId, owner, containerClass, label, text, help, i
<label>{input} {text}</label>
</div>
{helpBlock}
{id && <div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help_validation'}>{owner.getFormValidationMessage(id)}</div>}
{id && validationMsg && <div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help_validation'}>{validationMsg}</div>}
</div>
);
}
@ -216,7 +245,7 @@ class CheckBox extends Component {
const id = this.props.id;
const htmlId = 'form_' + id;
return wrapInputInline(id, htmlId, owner, 'checkbox', props.label, props.text, props.help,
return wrapInputWithText(id, htmlId, owner, 'checkbox', props.label, props.text, props.help,
<input type="checkbox" checked={owner.getFormValue(id)} id={htmlId} aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, !owner.getFormValue(id))}/>
);
}
@ -251,7 +280,9 @@ class Dropdown extends Component {
label: PropTypes.string.isRequired,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
options: PropTypes.array,
optGroups: PropTypes.array
optGroups: PropTypes.array,
className: PropTypes.string,
inline: PropTypes.bool
}
static contextTypes = {
@ -276,11 +307,16 @@ class Dropdown extends Component {
);
}
let className = 'form-control';
if (props.className) {
className += ' ' + props.className;
}
return wrapInput(id, htmlId, owner, props.label, props.help,
<select id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} value={owner.getFormValue(id)} onChange={evt => owner.updateFormValue(id, evt.target.value)}>
<select id={htmlId} className={className} aria-describedby={htmlId + '_help'} value={owner.getFormValue(id)} onChange={evt => owner.updateFormValue(id, evt.target.value)}>
{options}
</select>
</select>,
props.inline
);
}
}

View file

@ -60,7 +60,7 @@ h3.legend {
.mt-secondary-nav {
margin: 0px;
background-color: #f5f5f5;
padding: 8px 5px;
padding: 5px 5px;
border-radius: 4px;
}
}

View file

@ -47,7 +47,6 @@ class Table extends Component {
selectionAsArray: PropTypes.bool,
onSelectionChangedAsync: PropTypes.func,
onSelectionDataAsync: PropTypes.func,
actions: PropTypes.func,
withHeader: PropTypes.bool,
refreshInterval: PropTypes.number
}
@ -177,81 +176,82 @@ class Table extends Component {
componentDidMount() {
const columns = this.props.columns.slice();
if (this.props.actions) {
const createdCellFn = (td, data) => {
const linksContainer = jQuery('<span class="mt-action-links"/>');
let actions = this.props.actions(data);
let options = {};
if (!Array.isArray(actions)) {
options = actions;
actions = actions.actions;
}
for (const action of actions) {
if (action.action) {
const html = ReactDOMServer.renderToStaticMarkup(<a href="">{action.label}</a>);
const elem = jQuery(html);
elem.click((evt) => { evt.preventDefault(); action.action(this) });
linksContainer.append(elem);
} else if (action.link) {
const html = ReactDOMServer.renderToStaticMarkup(<a href={action.link}>{action.label}</a>);
const elem = jQuery(html);
elem.click((evt) => { evt.preventDefault(); this.navigateTo(action.link) });
linksContainer.append(elem);
} else if (action.href) {
const html = ReactDOMServer.renderToStaticMarkup(<a href={action.href}>{action.label}</a>);
const elem = jQuery(html);
linksContainer.append(elem);
} else {
const html = ReactDOMServer.renderToStaticMarkup(action.label);
const elem = jQuery(html);
linksContainer.append(elem);
}
}
if (options.refreshTimeout) {
const currentMS = Date.now();
if (!this.refreshTimeoutAt || this.refreshTimeoutAt > currentMS + options.refreshTimeout) {
clearTimeout(this.refreshTimeoutId);
this.refreshTimeoutAt = currentMS + options.refreshTimeout;
this.refreshTimeoutId = setTimeout(() => {
this.refreshTimeoutAt = 0;
this.refresh();
}, options.refreshTimeout);
}
}
jQuery(td).html(linksContainer);
};
columns.push({
data: null,
orderable: false,
searchable: false,
type: 'html',
createdCell: createdCellFn
});
}
// XSS protection
// XSS protection and actions rendering
for (const column of columns) {
const originalRender = column.render;
column.render = (data, ...rest) => {
if (originalRender) {
const markup = originalRender(data, ...rest);
return ReactDOMServer.renderToStaticMarkup(<div>{markup}</div>);
} else {
return ReactDOMServer.renderToStaticMarkup(<div>{data}</div>)
if (column.actions) {
const createdCellFn = (td, data, rowData) => {
const linksContainer = jQuery('<span class="mt-action-links"/>');
let actions = column.actions(rowData);
let options = {};
if (!Array.isArray(actions)) {
options = actions;
actions = actions.actions;
}
for (const action of actions) {
if (action.action) {
const html = ReactDOMServer.renderToStaticMarkup(<a href="">{action.label}</a>);
const elem = jQuery(html);
elem.click((evt) => { evt.preventDefault(); action.action(this) });
linksContainer.append(elem);
} else if (action.link) {
const html = ReactDOMServer.renderToStaticMarkup(<a href={action.link}>{action.label}</a>);
const elem = jQuery(html);
elem.click((evt) => { evt.preventDefault(); this.navigateTo(action.link) });
linksContainer.append(elem);
} else if (action.href) {
const html = ReactDOMServer.renderToStaticMarkup(<a href={action.href}>{action.label}</a>);
const elem = jQuery(html);
linksContainer.append(elem);
} else {
const html = ReactDOMServer.renderToStaticMarkup(<span>{action.label}</span>);
const elem = jQuery(html);
linksContainer.append(elem);
}
}
if (options.refreshTimeout) {
const currentMS = Date.now();
if (!this.refreshTimeoutAt || this.refreshTimeoutAt > currentMS + options.refreshTimeout) {
clearTimeout(this.refreshTimeoutId);
this.refreshTimeoutAt = currentMS + options.refreshTimeout;
this.refreshTimeoutId = setTimeout(() => {
this.refreshTimeoutAt = 0;
this.refresh();
}, options.refreshTimeout);
}
}
jQuery(td).html(linksContainer);
};
column.type = 'html';
column.createdCell = createdCellFn;
if (!('data' in column)) {
column.data = null;
column.orderable = false;
column.searchable = false;
}
};
} else {
const originalRender = column.render;
column.render = (data, ...rest) => {
if (originalRender) {
const markup = originalRender(data, ...rest);
return ReactDOMServer.renderToStaticMarkup(<div>{markup}</div>);
} else {
return ReactDOMServer.renderToStaticMarkup(<div>{data}</div>)
}
};
}
column.title = ReactDOMServer.renderToStaticMarkup(<div>{column.title}</div>);
}

View file

@ -88,11 +88,13 @@ class TreeTable extends Component {
// XSS protection
sanitizeTreeData(unsafeData) {
const data = unsafeData.slice();
for (const entry of data) {
const data = [];
for (const unsafeEntry of unsafeData) {
const entry = Object.assign({}, unsafeEntry);
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>)
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>)
entry.children = this.sanitizeTreeData(entry.children);
data.push(entry);
}
return data;
}

View file

@ -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 {Link} from "react-router-dom";
@translate()
@withPageHelpers
@ -41,40 +42,59 @@ export default class List extends Component {
render() {
const t = this.props.t;
const actions = data => {
const actions = [];
const perms = data[6];
if (perms.includes('edit')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
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>,
link: `/lists/${data[0]}/fields`
});
}
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: `/lists/${data[0]}/share`
});
}
return actions;
};
const columns = [
{ data: 1, title: t('Name') },
{
data: 1,
title: t('Name'),
actions: data => {
const perms = data[6];
if (perms.includes('viewSubscriptions')) {
return [{label: data[1], link: `/lists/${data[0]}/subscriptions`}];
} else {
return [{label: data[1]}];
}
}
},
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Subscribers') },
{ data: 4, title: t('Description') },
{ data: 5, title: t('Namespace') }
{ data: 5, title: t('Namespace') },
{
actions: data => {
const actions = [];
const perms = data[6];
if (perms.includes('viewSubscriptions')) {
actions.push({
label: <span className="glyphicon glyphicon-user" aria-hidden="true" title="Subscribers"></span>,
link: `/lists/${data[0]}/subscriptions`
});
}
if (perms.includes('edit')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
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>,
link: `/lists/${data[0]}/fields`
});
}
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: `/lists/${data[0]}/share`
});
}
return actions;
}
}
];
return (
@ -88,7 +108,7 @@ export default class List extends Component {
<Title>{t('Lists')}</Title>
<Table withHeader dataUrl="/rest/lists-table" columns={columns} actions={actions} />
<Table withHeader dataUrl="/rest/lists-table" columns={columns} />
</div>
);
}

View file

@ -1,6 +1,7 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
import { withErrorHandling } from '../../lib/error-handling';
@ -17,39 +18,43 @@ export default class List extends Component {
this.state = {};
this.state.listId = parseInt(props.match.params.listId);
this.fieldTypes = getFieldTypes(props.t);
}
static propTypes = {
list: PropTypes.object
}
componentDidMount() {
}
render() {
const t = this.props.t;
const actions = data => [{
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/lists/${this.state.listId}/fields/${data[0]}/edit`
}];
const columns = [
{ data: 4, title: "#" },
{ data: 1, title: t('Name'),
render: (data, cmd, rowData) => rowData[2] === 'option' ? <span><span className="glyphicon glyphicon-record" aria-hidden="true"></span> {data}</span> : data
},
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') }
{ data: 3, title: t('Merge Tag') },
{
actions: data => [{
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
}]
}
];
return (
<div>
<Toolbar>
<NavButton linkTo={`/lists/${this.state.listId}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
<NavButton linkTo={`/lists/${this.props.list.id}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
</Toolbar>
<Title>{t('Fields')}</Title>
<Table withHeader dataUrl={`/rest/fields-table/${this.state.listId}`} columns={columns} actions={actions} />
<Table withHeader dataUrl={`/rest/fields-table/${this.props.list.id}`} columns={columns} />
</div>
);
}

View file

@ -41,30 +41,31 @@ export default class List extends Component {
render() {
const t = this.props.t;
const actions = data => {
const actions = [];
const perms = data[4];
if (perms.includes('edit')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
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>,
link: `/lists/forms/${data[0]}/share`
});
}
return actions;
};
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Namespace') }
{ data: 3, title: t('Namespace') },
{
actions: data => {
const actions = [];
const perms = data[4];
if (perms.includes('edit')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
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>,
link: `/lists/forms/${data[0]}/share`
});
}
return actions;
}
}
];
return (
@ -77,7 +78,7 @@ export default class List extends Component {
<Title>{t('Forms')}</Title>
<Table withHeader dataUrl="/rest/forms-table" columns={columns} actions={actions} />
<Table withHeader dataUrl="/rest/forms-table" columns={columns} />
</div>
);
}

View file

@ -12,6 +12,7 @@ import FormsList from './forms/List';
import FormsCUD from './forms/CUD';
import FieldsList from './fields/List';
import FieldsCUD from './fields/CUD';
import SubscriptionsList from './subscriptions/List';
import Share from '../shares/Share';
@ -33,8 +34,14 @@ const getStructure = t => {
resolve: {
list: params => `/rest/lists/${params.listId}`
},
link: params => `/lists/${params.listId}/edit`,
link: params => `/lists/${params.listId}/subscriptions`,
navs: {
subscriptions: {
title: t('Subscribers'),
link: params => `/lists/${params.listId}/subscriptions`,
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
render: props => <SubscriptionsList list={props.resolved.list} />
},
':action(edit|delete)': {
title: t('Edit'),
link: params => `/lists/${params.listId}/edit`,
@ -45,7 +52,7 @@ const getStructure = t => {
title: t('Fields'),
link: params => `/lists/${params.listId}/fields/`,
visible: resolved => resolved.list.permissions.includes('manageFields'),
component: FieldsList,
render: props => <FieldsList list={props.resolved.list} />,
children: {
':fieldId([0-9]+)': {
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
@ -67,6 +74,11 @@ const getStructure = t => {
}
}
},
segments: {
title: t('Segments'),
link: params => `/lists/${params.listId}/segments`,
visible: resolved => resolved.list.permissions.includes('manageSegments')
},
share: {
title: t('Share'),
link: params => `/lists/${params.listId}/share`,

View file

@ -0,0 +1,486 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
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
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete";
import { getFieldTypes } from './field-types';
import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import slugify from 'slugify';
import { parseDate, parseBirthday } from '../../../../shared/fields';
@translate()
@withForm
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component {
constructor(props) {
super(props);
this.state = {};
this.fieldTypes = getFieldTypes(props.t);
this.initForm({
serverValidation: {
url: `/rest/fields-validate/${this.props.list.id}`,
changed: ['key'],
extra: ['id']
},
onChange: {
name: ::this.onChangeName
}
});
}
static propTypes = {
action: PropTypes.string.isRequired,
list: PropTypes.object,
entity: PropTypes.object
}
onChangeName(state, attr, oldValue, newValue) {
const oldComputedKey = ('MERGE_' + slugify(oldValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
const oldKey = state.formState.getIn(['data', 'key', 'value']);
if (oldKey === '' || oldKey === oldComputedKey) {
const newKey = ('MERGE_' + slugify(newValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
state.formState = state.formState.setIn(['data', 'key', 'value'], newKey);
}
}
@withAsyncErrorHandler
async loadOrderOptions() {
const t = this.props.t;
const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
];
};
this.setState({
orderListOptions: getOrderOptions('order_list'),
orderSubscribeOptions: getOrderOptions('order_subscribe'),
orderManageOptions: getOrderOptions('order_manage')
});
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
data.settings = data.settings || {};
if (data.default_value === null) {
data.default_value = '';
}
if (data.type !== 'option') {
data.group = null;
}
data.enumOptions = '';
data.dateFormat = 'eur';
data.renderTemplate = '';
switch (data.type) {
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
case 'json':
data.renderTemplate = data.settings.renderTemplate;
break;
case 'radio-enum':
case 'dropdown-enum':
data.enumOptions = this.renderEnumOptions(data.settings.enumOptions);
data.renderTemplate = data.settings.renderTemplate;
break;
case 'date':
case 'birthday':
data.dateFormat = data.settings.dateFormat;
break;
}
data.orderListBefore = data.orderListBefore.toString();
data.orderSubscribeBefore = data.orderSubscribeBefore.toString();
data.orderManageBefore = data.orderManageBefore.toString();
});
} else {
this.populateFormValues({
name: '',
type: 'text',
key: '',
default_value: '',
group: null,
renderTemplate: '',
enumOptions: '',
dateFormat: 'eur',
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
orderSubscribeBefore: 'end',
orderManageBefore: 'end',
orderListOptions: [],
orderSubscribeOptions: [],
orderManageOptions: []
});
}
this.loadOrderOptions();
}
localValidateFormValues(state) {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
const keyServerValidation = state.getIn(['key', 'serverValidation']);
if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.'));
} else if (!keyServerValidation) {
state.setIn(['key', 'error'], t('Validation is in progress...'));
} else if (keyServerValidation.exists) {
state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.'));
} else {
state.setIn(['key', 'error'], null);
}
const type = state.getIn(['type', 'value']);
const group = state.getIn(['group', 'value']);
if (type === 'option' && !group) {
state.setIn(['group', 'error'], t('Group has to be selected'));
} else {
state.setIn(['group', 'error'], null);
}
const defaultValue = state.getIn(['default_value', 'value']);
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'));
} else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) {
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted birthday date'));
} else {
state.setIn(['default_value', 'error'], null);
}
if (type === 'radio-enum' || type === 'dropdown-enum') {
const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value']));
if (enumOptions.errors) {
state.setIn(['enumOptions', 'error'], <div>{enumOptions.errors.map((err, idx) => <div key={idx}>{err}</div>)}</div>);
} else {
state.setIn(['enumOptions', 'error'], null);
if (defaultValue !== '' && !(defaultValue in enumOptions.options)) {
state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
}
}
} else {
state.setIn(['enumOptions', 'error'], null);
}
}
parseEnumOptions(text) {
const t = this.props.t;
const errors = [];
const options = {};
const lines = text.split('\n');
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const line = lines[lineIdx].trim();
if (line != '') {
const matches = line.match(/^([^|]*)[|](.*)$/);
if (matches) {
const key = matches[1].trim();
const label = matches[2].trim();
options[key] = label;
} else {
errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1}));
}
}
}
if (errors.length) {
return {
errors
};
} else {
return {
options
};
}
}
renderEnumOptions(options) {
return Object.keys(options).map(key => `${key}|${options[key]}`).join('\n');
}
async submitHandler() {
const t = this.props.t;
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `/rest/fields/${this.props.list.id}`
}
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving field ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') {
data.default_value = null;
}
if (data.type !== 'option') {
data.group = null;
}
data.settings = {};
switch (data.type) {
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
case 'json':
data.settings.renderTemplate = data.renderTemplate;
break;
case 'radio-enum':
case 'dropdown-enum':
data.settings.enumOptions = this.parseEnumOptions(data.enumOptions).options;
data.settings.renderTemplate = data.renderTemplate;
break;
case 'date':
case 'birthday':
data.settings.dateFormat = data.dateFormat;
break;
}
delete data.renderTemplate;
delete data.enumOptions;
delete data.dateFormat;
if (data.type === 'option') {
data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none';
} else {
data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore;
data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore;
data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore;
}
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field 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.DependencyNotFoundError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('It seems that another field upon which sort field order was established has been deleted 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 typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
const type = this.getFormValue('type');
let fieldSettings = null;
switch (type) {
case 'text':
case 'website':
case 'longtext':
case 'gpg':
case 'number':
fieldSettings =
<Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array, for
example <code>{'{{#each values}} {{this}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'radio-enum':
case 'dropdown-enum':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="enumOptions"
label={t('Options')}
height="250px"
mode="text"
help={<Trans><div>Specify the options to select from in the following format:<code>key|label</code>. For example:</div>
<div><code>au|Australia</code></div><div><code>at|Austria</code></div></Trans>}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array.
Each entry in the array is an object with attributes <code>key</code> and <code>label</code>.
For example <code>{'{{#each values}} {{this.value}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'date':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD/YYYY')},
{key: 'eur', label: t('DD/MM/YYYY')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'birthday':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD')},
{key: 'eur', label: t('DD/MM')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'json':
fieldSettings = <Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="json"
help={<Trans>You can use this template to render JSON values (if the JSON is an array then the array is
exposed as <code>values</code>, otherwise you can access the JSON keys directly).</Trans>}
/>
</Fieldset>;
break;
case 'option':
const fieldsGroupedColumns = [
{ data: 4, title: "#" },
{ data: 1, title: t('Name') },
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') }
];
fieldSettings =
<Fieldset label={t('Field settings')}>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`/rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
}
return (
<div>
{isEdit &&
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/edit`}
listUrl={`/lists/fields/${this.props.list.id}`}
deletingMsg={t('Deleting field ...')}
deletedMsg={t('Field deleted')}/>
}
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
{isEdit ?
<StaticField id="type" className="mt-form-disabled" label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
:
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
}
<InputField id="key" label={t('Merge tag')}/>
{fieldSettings}
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -0,0 +1,122 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling';
import { Table } from '../../lib/table';
import { SubscriptionStatus } from '../../../../shared/lists';
import moment from 'moment';
import {
Dropdown, Form,
withForm
} from '../../lib/form';
import axios from '../../lib/axios';
@translate()
@withForm
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
super(props);
const t = props.t;
this.state = {
segmentOptions: [
{key: 'none', label: t('All subscriptions')}
]
};
this.subscriptionStatusLabels = {
[SubscriptionStatus.SUBSCRIBED]: t('Subscribed'),
[SubscriptionStatus.UNSUBSCRIBED]: t('Unubscribed'),
[SubscriptionStatus.BOUNCED]: t('Bounced'),
[SubscriptionStatus.COMPLAINED]: t('Complained'),
}
this.initForm({
onChange: {
segment: ::this.onSegmentChange
}
});
}
static propTypes = {
list: PropTypes.object
}
onSegmentChange(state, attr, oldValue, newValue) {
// TODO
this.subscriptionsTable.refresh();
}
@withAsyncErrorHandler
async loadSegmentOptions() {
const t = this.props.t;
const result = await axios.get(`/rest/segments/${this.props.list.id}`);
this.setState({
segmentOptions: [
{key: 'none', label: t('All subscriptions')},
...result.data.map(x => ({ key: x.id.toString(), label: x.name})),
]
});
}
componentDidMount() {
this.populateFormValues({
segment: 'none'
});
this.loadSegmentOptions();
}
render() {
const t = this.props.t;
const list = this.props.list;
const columns = [
{ data: 2, title: t('Email') },
{ data: 3, title: t('Status'), render: data => this.subscriptionStatusLabels[data] },
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
];
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`
}]
});
}
return (
<div>
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
</Toolbar>
<Title>{t('Subscribers')}</Title>
{list.description &&
<div className="well well-sm">{list.description}</div>
}
<div className="well well-sm">
<Form inline stateOwner={this}>
<Dropdown inline className="input-sm" id="segment" label={t('Segment')} options={this.state.segmentOptions}/>
</Form>
</div>
<Table ref={node => this.subscriptionsTable = node} withHeader dataUrl={`/rest/subscriptions-table/${list.id}`} columns={columns} />
</div>
);
}
}

View file

@ -68,97 +68,98 @@ export default class List extends Component {
render() {
const t = this.props.t;
const actions = data => {
const actions = [];
const perms = data[8];
const permsReportTemplate = data[9];
let viewContent, startStop, refreshTimeout;
const state = data[6];
const id = data[0];
const mimeType = data[7];
if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) {
viewContent = {
label: <span className="glyphicon glyphicon-hourglass" aria-hidden="true" title="Processing"></span>,
};
startStop = {
label: <span className="glyphicon glyphicon-stop" aria-hidden="true" title="Stop"></span>,
action: (table) => this.stop(table, id)
};
refreshTimeout = 1000;
} else if (state === ReportState.FINISHED) {
if (mimeType === 'text/html') {
viewContent = {
label: <span className="glyphicon glyphicon-eye-open" aria-hidden="true" title="View"></span>,
link: `/reports/${id}/view`
};
} else if (mimeType === 'text/csv') {
viewContent = {
label: <span className="glyphicon glyphicon-download-alt" aria-hidden="true" title="Download"></span>,
href: `/reports/${id}/download`
};
}
startStop = {
label: <span className="glyphicon glyphicon-repeat" aria-hidden="true" title="Refresh report"></span>,
action: (table) => this.start(table, id)
};
} else if (state === ReportState.FAILED) {
viewContent = {
label: <span className="glyphicon glyphicon-thumbs-down" aria-hidden="true" title="Report generation failed"></span>,
};
startStop = {
label: <span className="glyphicon glyphicon-repeat" aria-hidden="true" title="Regenerate report"></span>,
action: (table) => this.start(table, id)
};
}
if (perms.includes('viewContent')) {
actions.push(viewContent);
}
if (perms.includes('viewOutput')) {
actions.push(
{
label: <span className="glyphicon glyphicon-modal-window" aria-hidden="true" title="View console output"></span>,
link: `/reports/${id}/output`
}
);
}
if (perms.includes('execute') && permsReportTemplate.includes('execute')) {
actions.push(startStop);
}
if (perms.includes('edit') && permsReportTemplate.includes('execute')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/reports/${id}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: `/reports/${id}/share`
});
}
return { refreshTimeout, actions };
};
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Template') },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' },
{ data: 5, title: t('Namespace') }
{ data: 5, title: t('Namespace') },
{
actions: data => {
const actions = [];
const perms = data[8];
const permsReportTemplate = data[9];
let viewContent, startStop, refreshTimeout;
const state = data[6];
const id = data[0];
const mimeType = data[7];
if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) {
viewContent = {
label: <span className="glyphicon glyphicon-hourglass" aria-hidden="true" title="Processing"></span>,
};
startStop = {
label: <span className="glyphicon glyphicon-stop" aria-hidden="true" title="Stop"></span>,
action: (table) => this.stop(table, id)
};
refreshTimeout = 1000;
} else if (state === ReportState.FINISHED) {
if (mimeType === 'text/html') {
viewContent = {
label: <span className="glyphicon glyphicon-eye-open" aria-hidden="true" title="View"></span>,
link: `/reports/${id}/view`
};
} else if (mimeType === 'text/csv') {
viewContent = {
label: <span className="glyphicon glyphicon-download-alt" aria-hidden="true" title="Download"></span>,
href: `/reports/${id}/download`
};
}
startStop = {
label: <span className="glyphicon glyphicon-repeat" aria-hidden="true" title="Refresh report"></span>,
action: (table) => this.start(table, id)
};
} else if (state === ReportState.FAILED) {
viewContent = {
label: <span className="glyphicon glyphicon-thumbs-down" aria-hidden="true" title="Report generation failed"></span>,
};
startStop = {
label: <span className="glyphicon glyphicon-repeat" aria-hidden="true" title="Regenerate report"></span>,
action: (table) => this.start(table, id)
};
}
if (perms.includes('viewContent')) {
actions.push(viewContent);
}
if (perms.includes('viewOutput')) {
actions.push(
{
label: <span className="glyphicon glyphicon-modal-window" aria-hidden="true" title="View console output"></span>,
link: `/reports/${id}/output`
}
);
}
if (perms.includes('execute') && permsReportTemplate.includes('execute')) {
actions.push(startStop);
}
if (perms.includes('edit') && permsReportTemplate.includes('execute')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/reports/${id}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: `/reports/${id}/share`
});
}
return { refreshTimeout, actions };
}
}
];
@ -175,7 +176,7 @@ export default class List extends Component {
<Title>{t('Reports')}</Title>
<Table withHeader dataUrl="/rest/reports-table" columns={columns} actions={actions} />
<Table withHeader dataUrl="/rest/reports-table" columns={columns} />
</div>
);
}

View file

@ -14,6 +14,7 @@ import ReportTemplatesCUD from './templates/CUD';
import ReportTemplatesList from './templates/List';
import Share from '../shares/Share';
import { ReportState } from '../../../shared/reports';
import mailtrainConfig from 'mailtrainConfig';
const getStructure = t => {
@ -86,7 +87,7 @@ const getStructure = t => {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/reports/templates/${params.templateId}/edit`,
visible: resolved => resolved.template.permissions.includes('edit'),
visible: resolved => mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && resolved.template.permissions.includes('edit'),
render: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
},
share: {

View file

@ -103,10 +103,10 @@ export default class CUD extends Component {
' }\n' +
']',
js:
'const results = await campaigns.getResults(inputs.campaign, ["custom_country"], query =>\n' +
'const results = await campaigns.getResults(inputs.campaign, ["merge_country"], query =>\n' +
' query.count("* AS count_all")\n' +
' .select(knex.raw("SUM(IF(tracker.count IS NULL, 0, 1)) AS count_opened"))\n' +
' .groupBy("custom_country")\n' +
' .groupBy("merge_country")\n' +
');\n' +
'\n' +
'for (const row of results) {\n' +
@ -138,7 +138,7 @@ export default class CUD extends Component {
' {{#each results}}\n' +
' <tr>\n' +
' <th scope="row">\n' +
' {{custom_country}}\n' +
' {{merge_country}}\n' +
' </th>\n' +
' <td style="width: 20%;">\n' +
' {{count_opened}}\n' +

View file

@ -8,6 +8,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handli
import { Table } from '../../lib/table';
import axios from '../../lib/axios';
import moment from 'moment';
import mailtrainConfig from 'mailtrainConfig';
@translate()
@withPageHelpers
@ -32,7 +33,7 @@ export default class List extends Component {
const result = await axios.post('/rest/permissions-check', request);
this.setState({
createPermitted: result.data.createReportTemplate
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess')
});
}
@ -43,32 +44,33 @@ export default class List extends Component {
render() {
const t = this.props.t;
const actions = data => {
const actions = [];
const perms = data[5];
if (perms.includes('view')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/reports/templates/${data[0]}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: `/reports/templates/${data[0]}/share`
});
}
return actions;
};
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 4, title: t('Namespace') }
{ data: 4, title: t('Namespace') },
{
actions: data => {
const actions = [];
const perms = data[5];
if (mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && perms.includes('edit')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/reports/templates/${data[0]}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: `/reports/templates/${data[0]}/share`
});
}
return actions;
}
}
];
return (
@ -86,7 +88,7 @@ export default class List extends Component {
<Title>{t('Report Templates')}</Title>
<Table withHeader dataUrl="/rest/report-templates-table" columns={columns} actions={actions} />
<Table withHeader dataUrl="/rest/report-templates-table" columns={columns} />
</div>
);
}

View file

@ -98,20 +98,6 @@ export default class Share extends Component {
render() {
const t = this.props.t;
const actions = data => {
const actions = [];
const autoGenerated = data[4];
if (!autoGenerated) {
actions.push({
label: 'Delete',
action: () => this.deleteShare(data[3])
});
}
return actions;
};
const sharesColumns = [];
sharesColumns.push({ data: 0, title: t('Username') });
if (mailtrainConfig.isAuthMethodLocal) {
@ -119,6 +105,21 @@ export default class Share extends Component {
}
sharesColumns.push({ data: 2, title: t('Role') });
sharesColumns.push({
actions: data => {
const actions = [];
const autoGenerated = data[4];
if (!autoGenerated) {
actions.push({
label: 'Delete',
action: () => this.deleteShare(data[3])
});
}
return actions;
}
})
let usersLabelIndex = 1;
const usersColumns = [
@ -155,7 +156,7 @@ export default class Share extends Component {
<hr/>
<h3 className="legend">{t('Existing Users')}</h3>
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`/rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} actions={actions}/>
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`/rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} />
</div>
);
}

View file

@ -43,30 +43,31 @@ export default class UserShares extends Component {
render() {
const renderSharesTable = (entityTypeId, title) => {
const actions = data => {
const actions = [];
const autoGenerated = data[3];
const perms = data[4];
if (!autoGenerated && perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-remove" aria-hidden="true" title="Remove"></span>,
action: () => this.deleteShare(entityTypeId, data[2])
});
}
return actions;
};
const columns = [
{ data: 0, title: t('Name') },
{ data: 1, title: t('Role') }
{ data: 1, title: t('Role') },
{
actions: data => {
const actions = [];
const autoGenerated = data[3];
const perms = data[4];
if (!autoGenerated && perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-remove" aria-hidden="true" title="Remove"></span>,
action: () => this.deleteShare(entityTypeId, data[2])
});
}
return actions;
}
}
];
return (
<div>
<h3>{title}</h3>
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`/rest/shares-table-by-user/${entityTypeId}/${this.props.user.id}`} columns={columns} actions={actions}/>
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`/rest/shares-table-by-user/${entityTypeId}/${this.props.user.id}`} columns={columns} />
</div>
);
};

View file

@ -20,19 +20,8 @@ export default class List extends Component {
const t = this.props.t;
const actions = data => [
{
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/users/${data[0]}/edit`
},
{
label: <span className="glyphicon glyphicon-share" aria-hidden="true" title="Share"></span>,
link: `/users/${data[0]}/shares`
}
];
const columns = [
{ data: 1, title: "Username" }
{ data: 1, title: "Username" },
];
if (mailtrainConfig.isAuthMethodLocal) {
@ -42,6 +31,19 @@ export default class List extends Component {
columns.push({ data: 3, title: "Namespace" });
columns.push({ data: 4, title: "Role" });
columns.push({
actions: data => [
{
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/users/${data[0]}/edit`
},
{
label: <span className="glyphicon glyphicon-share" aria-hidden="true" title="Share"></span>,
link: `/users/${data[0]}/shares`
}
]
});
return (
<div>
<Toolbar>
@ -50,7 +52,7 @@ export default class List extends Component {
<Title>{t('Users')}</Title>
<Table withHeader dataUrl="/rest/users-table" columns={columns} actions={actions} />
<Table withHeader dataUrl="/rest/users-table" columns={columns} />
</div>
);
}