Reports ported to ReactJS and Knex
Note that the interface for the custom JS code inside a report template has changed. It now offers promise-based interface and exposes knex.
This commit is contained in:
parent
6d95fa515e
commit
d63eed9ca9
27 changed files with 649 additions and 953 deletions
|
@ -49,36 +49,6 @@ class Form extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
static async handleChangedError(owner, fn) {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.ChangedError) {
|
||||
owner.disableForm();
|
||||
owner.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof interoperableErrors.NotFoundError) {
|
||||
owner.disableForm();
|
||||
owner.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('It seems that someone else has deleted the entity in the meantime.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onSubmit(evt) {
|
||||
const t = this.props.t;
|
||||
|
@ -88,7 +58,7 @@ class Form extends Component {
|
|||
evt.preventDefault();
|
||||
|
||||
if (this.props.onSubmitAsync) {
|
||||
await Form.handleChangedError(owner, async () => await this.props.onSubmitAsync(evt));
|
||||
await this.formHandleChangedError(async () => await this.props.onSubmitAsync(evt));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -520,6 +490,7 @@ class ACEEditor extends Component {
|
|||
showPrintMargin={false}
|
||||
value={owner.getFormValue(id)}
|
||||
tabSize={2}
|
||||
setOptions={{useWorker: false}} // This disables syntax check because it does not always work well (e.g. in case of JS code in report templates)
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -839,6 +810,37 @@ function withForm(target) {
|
|||
return this.state.formState.get('isDisabled');
|
||||
};
|
||||
|
||||
inst.formHandleChangedError = async function(fn) {
|
||||
const t = this.props.t;
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.ChangedError) {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof interoperableErrors.NotFoundError) {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('It seems that someone else has deleted the entity in the meantime.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.mt-action-links > a {
|
||||
.mt-action-links > * {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mt-action-links > a:last-child {
|
||||
.mt-action-links > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ const TableSelectMode = {
|
|||
};
|
||||
|
||||
|
||||
@translate()
|
||||
@translate(null, { withRef: true })
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
class Table extends Component {
|
||||
|
@ -48,8 +48,9 @@ class Table extends Component {
|
|||
selectionAsArray: PropTypes.bool,
|
||||
onSelectionChangedAsync: PropTypes.func,
|
||||
onSelectionDataAsync: PropTypes.func,
|
||||
actionLinks: PropTypes.array,
|
||||
withHeader: PropTypes.bool
|
||||
actions: PropTypes.func,
|
||||
withHeader: PropTypes.bool,
|
||||
refreshInterval: PropTypes.number
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -57,6 +58,12 @@ class Table extends Component {
|
|||
selectionKeyIndex: 0
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this.table) {
|
||||
this.table.rows().draw('page');
|
||||
}
|
||||
}
|
||||
|
||||
getSelectionMap(props) {
|
||||
let selArray = [];
|
||||
if (props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
|
||||
|
@ -162,17 +169,56 @@ class Table extends Component {
|
|||
componentDidMount() {
|
||||
const columns = this.props.columns.slice();
|
||||
|
||||
if (this.props.actionLinks) {
|
||||
const actionLinks = this.props.actionLinks;
|
||||
|
||||
if (this.props.actions) {
|
||||
const createdCellFn = (td, data) => {
|
||||
const linksContainer = jQuery('<span class="mt-action-links"/>');
|
||||
for (const {label, link} of actionLinks) {
|
||||
const dest = link(data);
|
||||
const lnkHtml = ReactDOMServer.renderToStaticMarkup(<a href={dest}>{label}</a>);
|
||||
const lnk = jQuery(lnkHtml);
|
||||
lnk.click((evt) => { evt.preventDefault(); this.navigateTo(dest) });
|
||||
linksContainer.append(lnk);
|
||||
|
||||
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);
|
||||
|
@ -238,6 +284,15 @@ class Table extends Component {
|
|||
|
||||
this.table = jQuery(this.domTable).DataTable(dtOptions);
|
||||
|
||||
if (this.props.refreshInterval) {
|
||||
this.refreshIntervalId = setInterval(() => this.refresh(), this.props.refreshInterval);
|
||||
}
|
||||
|
||||
this.table.on('destroy.dt', () => {
|
||||
clearInterval(this.refreshIntervalId);
|
||||
clearTimeout(this.refreshTimeoutId);
|
||||
});
|
||||
|
||||
this.fetchSelectionData();
|
||||
}
|
||||
|
||||
|
@ -306,6 +361,14 @@ class Table extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Refreshes the table. This method is provided to allow programmatic refresh from a handler outside the table.
|
||||
The reference to the table can be obtained by ref.
|
||||
*/
|
||||
Table.prototype.refresh = function() {
|
||||
this.getWrappedInstance().refresh()
|
||||
};
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableSelectMode
|
||||
|
|
|
@ -65,7 +65,7 @@ class TreeTable extends Component {
|
|||
selectMode: PropTypes.number,
|
||||
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||
onSelectionChangedAsync: PropTypes.func,
|
||||
actionLinks: PropTypes.array,
|
||||
actions: PropTypes.func,
|
||||
withHeader: PropTypes.bool
|
||||
}
|
||||
|
||||
|
@ -100,19 +100,18 @@ class TreeTable extends Component {
|
|||
};
|
||||
|
||||
let createNodeFn;
|
||||
if (this.props.actionLinks) {
|
||||
const actionLinks = this.props.actionLinks;
|
||||
|
||||
if (this.props.actions) {
|
||||
createNodeFn = (event, data) => {
|
||||
const node = data.node;
|
||||
const tdList = jQuery(node.tr).find(">td");
|
||||
|
||||
const linksContainer = jQuery('<span class="mt-action-links"/>');
|
||||
for (const {label, link} of actionLinks) {
|
||||
const dest = link(node.key);
|
||||
const lnkHtml = ReactDOMServer.renderToStaticMarkup(<a href={dest}>{label}</a>);
|
||||
|
||||
const actions = this.props.actions(node.key);
|
||||
for (const {label, link} of actions) {
|
||||
const lnkHtml = ReactDOMServer.renderToStaticMarkup(<a href={link}>{label}</a>);
|
||||
const lnk = jQuery(lnkHtml);
|
||||
lnk.click((evt) => { evt.preventDefault(); this.navigateTo(dest) });
|
||||
lnk.click((evt) => { evt.preventDefault(); this.navigateTo(link) });
|
||||
linksContainer.append(lnk);
|
||||
}
|
||||
|
||||
|
@ -202,7 +201,7 @@ class TreeTable extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const props = this.props;
|
||||
const actionLinks = props.actionLinks;
|
||||
const actions = props.actions;
|
||||
const withHeader = props.withHeader;
|
||||
|
||||
let containerClass = 'mt-treetable-container';
|
||||
|
@ -223,14 +222,14 @@ class TreeTable extends Component {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>{t('Name')}</th>
|
||||
{actionLinks && <th></th>}
|
||||
{actions && <th></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
{actionLinks && <td></td>}
|
||||
{actions && <td></td>}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -10,10 +10,10 @@ export default class List extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actionLinks = [
|
||||
const actions = key => [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: key => '/namespaces/edit/' + key
|
||||
link: '/namespaces/edit/' + key
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -25,7 +25,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Namespaces')}</Title>
|
||||
|
||||
<TreeTable withHeader dataUrl="/rest/namespaces-tree" actionLinks={actionLinks} />
|
||||
<TreeTable withHeader dataUrl="/rest/namespaces-tree" actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,24 +4,100 @@ import React, { Component } from 'react';
|
|||
import { translate } from 'react-i18next';
|
||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
||||
import { Table } from '../lib/table';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import moment from 'moment';
|
||||
import axios from '../lib/axios';
|
||||
import { ReportState } from '../../../shared/reports';
|
||||
|
||||
@translate()
|
||||
@withErrorHandling
|
||||
export default class List extends Component {
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async stop(table, id) {
|
||||
await axios.post(`/rest/report-stop/${id}`);
|
||||
table.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async start(table, id) {
|
||||
await axios.post(`/rest/report-start/${id}`);
|
||||
table.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actionLinks = [{
|
||||
label: 'Edit',
|
||||
link: data => '/reports/edit/' + data[0]
|
||||
}];
|
||||
const actions = data => {
|
||||
let view, startStop, refreshTimeout;
|
||||
|
||||
const state = data[5];
|
||||
const id = data[0];
|
||||
const mimeType = data[6];
|
||||
|
||||
if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) {
|
||||
view = {
|
||||
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') {
|
||||
view = {
|
||||
label: <span className="glyphicon glyphicon-eye-open" aria-hidden="true" title="View"></span>,
|
||||
link: `reports/view/${id}`
|
||||
};
|
||||
} else if (mimeType === 'text/csv') {
|
||||
view = {
|
||||
label: <span className="glyphicon glyphicon-download-alt" aria-hidden="true" title="Download"></span>,
|
||||
href: `reports/download/${id}`
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
view = {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
refreshTimeout,
|
||||
actions: [
|
||||
view,
|
||||
{
|
||||
label: <span className="glyphicon glyphicon-modal-window" aria-hidden="true" title="View console output"></span>,
|
||||
link: `reports/output/${id}`
|
||||
},
|
||||
startStop,
|
||||
{
|
||||
label: <span className="glyphicon glyphicon-wrench" aria-hidden="true" title="Edit"></span>,
|
||||
link: `/reports/edit/${id}`
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Template') },
|
||||
{ data: 3, title: t('Description') },
|
||||
{ data: 4, title: t('Last Run'), render: data => data ? moment(data).fromNow() : t('Not run yet') }
|
||||
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -33,7 +109,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Reports')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/reports-table" columns={columns} actionLinks={actionLinks} />
|
||||
<Table withHeader dataUrl="/rest/reports-table" columns={columns} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
53
client/src/reports/Output.js
Normal file
53
client/src/reports/Output.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import axios from '../lib/axios';
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class Output extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
output: null
|
||||
};
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadOutput() {
|
||||
const id = parseInt(this.props.match.params.id);
|
||||
const outputResp = await axios.get(`/rest/report-output/${id}`);
|
||||
const reportResp = await axios.get(`/rest/reports/${id}`);
|
||||
|
||||
this.setState({
|
||||
output: outputResp.data,
|
||||
report: reportResp.data,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadOutput();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (this.state.report) {
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Output for report {{name}}', { name: this.state.report.name })}</Title>
|
||||
|
||||
<pre>{this.state.output}</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div>{t('Loading report output ...')}</div>;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
57
client/src/reports/View.js
Normal file
57
client/src/reports/View.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import axios from '../lib/axios';
|
||||
import { ReportState } from '../../../shared/reports';
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class View extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
content: null
|
||||
};
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadContent() {
|
||||
const id = parseInt(this.props.match.params.id);
|
||||
const contentResp = await axios.get(`/rest/report-content/${id}`);
|
||||
const reportResp = await axios.get(`/rest/reports/${id}`);
|
||||
|
||||
this.setState({
|
||||
content: contentResp.data,
|
||||
report: reportResp.data
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadContent();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (this.state.report) {
|
||||
if (this.state.report.state === ReportState.FINISHED) {
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Report {{name}}', { name: this.state.report.name })}</Title>
|
||||
|
||||
<div dangerouslySetInnerHTML={{ __html: this.state.content }}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div className="alert alert-danger" role="alert">{t('Report not generated')}</div>;
|
||||
}
|
||||
} else {
|
||||
return <div>{t('Loading report ...')}</div>;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ import i18n from '../lib/i18n';
|
|||
import { Section } from '../lib/page'
|
||||
import ReportsCUD from './CUD'
|
||||
import ReportsList from './List'
|
||||
import ReportsView from './View'
|
||||
import ReportsOutput from './Output'
|
||||
import ReportTemplatesCUD from './templates/CUD'
|
||||
import ReportTemplatesList from './templates/List'
|
||||
|
||||
|
@ -33,6 +35,16 @@ const getStructure = t => {
|
|||
title: t('Create Report'),
|
||||
render: props => (<ReportsCUD {...props} />)
|
||||
},
|
||||
view: {
|
||||
title: t('View Report'),
|
||||
params: [':id' ],
|
||||
render: props => (<ReportsView {...props} />)
|
||||
},
|
||||
output: {
|
||||
title: t('View Report Output'),
|
||||
params: [':id' ],
|
||||
render: props => (<ReportsOutput {...props} />)
|
||||
},
|
||||
'templates': {
|
||||
title: t('Templates'),
|
||||
link: '/reports/templates',
|
||||
|
|
|
@ -57,17 +57,8 @@ export default class CUD extends Component {
|
|||
' }\n' +
|
||||
']',
|
||||
js:
|
||||
'campaigns.results(inputs.campaign, ["*"], "", (err, results) => {\n' +
|
||||
' if (err) {\n' +
|
||||
' return callback(err);\n' +
|
||||
' }\n' +
|
||||
'\n' +
|
||||
' const data = {\n' +
|
||||
' results: results\n' +
|
||||
' };\n' +
|
||||
'\n' +
|
||||
' return callback(null, data);\n' +
|
||||
'});',
|
||||
'const results = await campaigns.getResults(inputs.campaign, ["*"]);\n' +
|
||||
'render({ results });',
|
||||
hbs:
|
||||
'<h2>{{title}}</h2>\n' +
|
||||
'\n' +
|
||||
|
@ -115,21 +106,17 @@ export default class CUD extends Component {
|
|||
' }\n' +
|
||||
']',
|
||||
js:
|
||||
'campaigns.results(inputs.campaign, ["custom_country", "count(*) AS count_all", "SUM(IF(tracker.count IS NULL, 0, 1)) AS count_opened"], "GROUP BY custom_country", (err, results) => {\n' +
|
||||
' if (err) {\n' +
|
||||
' return callback(err);\n' +
|
||||
' }\n' +
|
||||
'const results = await campaigns.getResults(inputs.campaign, ["custom_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' +
|
||||
');\n' +
|
||||
'\n' +
|
||||
' for (let row of results) {\n' +
|
||||
' row["percentage"] = Math.round((row.count_opened / row.count_all) * 100);\n' +
|
||||
' }\n' +
|
||||
'for (const row of results) {\n' +
|
||||
' row.percentage = Math.round((row.count_opened / row.count_all) * 100);\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
' let data = {\n' +
|
||||
' results: results\n' +
|
||||
' };\n' +
|
||||
'\n' +
|
||||
' return callback(null, data);\n' +
|
||||
'});',
|
||||
'render({ results });',
|
||||
hbs:
|
||||
'<h2>{{title}}</h2>\n' +
|
||||
'\n' +
|
||||
|
@ -189,17 +176,8 @@ export default class CUD extends Component {
|
|||
' }\n' +
|
||||
']',
|
||||
js:
|
||||
'subscriptions.list(inputs.list.id,0,0, (err, results) => {\n' +
|
||||
' if (err) {\n' +
|
||||
' return callback(err);\n' +
|
||||
' }\n' +
|
||||
'\n' +
|
||||
' let data = {\n' +
|
||||
' results: results\n' +
|
||||
' };\n' +
|
||||
'\n' +
|
||||
' return callback(null, data);\n' +
|
||||
'});',
|
||||
'const results = await subscriptions.list(inputs.list.id);\n' +
|
||||
'render({ results });',
|
||||
hbs:
|
||||
'{{#each results}}\n' +
|
||||
'{{firstName}},{{lastName}},{{email}}\n' +
|
||||
|
@ -246,11 +224,11 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
async submitAndStay() {
|
||||
await Form.handleChangedError(this, async () => await this.doSubmit(true));
|
||||
await this.formHandleChangedError(async () => await this.doSubmit(true));
|
||||
}
|
||||
|
||||
async submitAndLeave() {
|
||||
await Form.handleChangedError(this, async () => await this.doSubmit(false));
|
||||
await this.formHandleChangedError(async () => await this.doSubmit(false));
|
||||
}
|
||||
|
||||
async doSubmit(stay) {
|
||||
|
@ -273,6 +251,7 @@ export default class CUD extends Component {
|
|||
|
||||
if (submitSuccessful) {
|
||||
if (stay) {
|
||||
await this.loadFormValues();
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('success', t('Report template saved'));
|
||||
} else {
|
||||
|
|
|
@ -12,10 +12,12 @@ export default class List extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actionLinks = [{
|
||||
label: 'Edit',
|
||||
link: data => '/reports/templates/edit/' + data[0]
|
||||
}];
|
||||
const actions = data => [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: '/reports/templates/edit/' + data[0]
|
||||
}
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
|
@ -37,7 +39,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Report Templates')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/report-templates-table" columns={columns} actionLinks={actionLinks} />
|
||||
<Table withHeader dataUrl="/rest/report-templates-table" columns={columns} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export default class List extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
let actionLinks;
|
||||
let actions;
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
|
@ -21,10 +21,12 @@ export default class List extends Component {
|
|||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
columns.push({ data: 2, title: "Full Name" });
|
||||
|
||||
actionLinks = [{
|
||||
label: 'Edit',
|
||||
link: data => '/users/edit/' + data[0]
|
||||
}];
|
||||
actions = data => [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: '/users/edit/' + data[0]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -35,7 +37,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Users')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/users-table" columns={columns} actionLinks={actionLinks} />
|
||||
<Table withHeader dataUrl="/rest/users-table" columns={columns} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue