Share report template functionality
This commit is contained in:
parent
e6ad0e239e
commit
4822a50d0b
35 changed files with 614 additions and 128 deletions
2
app.js
2
app.js
|
@ -50,6 +50,7 @@ const reportTemplatesRest = require('./routes/rest/report-templates');
|
||||||
const reportsRest = require('./routes/rest/reports');
|
const reportsRest = require('./routes/rest/reports');
|
||||||
const campaignsRest = require('./routes/rest/campaigns');
|
const campaignsRest = require('./routes/rest/campaigns');
|
||||||
const listsRest = require('./routes/rest/lists');
|
const listsRest = require('./routes/rest/lists');
|
||||||
|
const sharesRest = require('./routes/rest/shares');
|
||||||
|
|
||||||
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
||||||
const usersLegacyIntegration = require('./routes/users-legacy-integration');
|
const usersLegacyIntegration = require('./routes/users-legacy-integration');
|
||||||
|
@ -271,6 +272,7 @@ app.use('/rest', usersRest);
|
||||||
app.use('/rest', accountRest);
|
app.use('/rest', accountRest);
|
||||||
app.use('/rest', campaignsRest);
|
app.use('/rest', campaignsRest);
|
||||||
app.use('/rest', listsRest);
|
app.use('/rest', listsRest);
|
||||||
|
app.use('/rest', sharesRest);
|
||||||
|
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
app.use('/rest', reportTemplatesRest);
|
app.use('/rest', reportTemplatesRest);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate, Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
import URL from 'url-parse';
|
import URL from 'url-parse';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
|
@ -11,6 +11,7 @@ import { Button } from '../lib/bootstrap-components';
|
||||||
@translate()
|
@translate()
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class API extends Component {
|
export default class API extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate, Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
|
||||||
import {
|
import {
|
||||||
withForm, Form, Fieldset, FormSendMethod, InputField, ButtonRow, Button
|
withForm, Form, Fieldset, FormSendMethod, InputField, ButtonRow, Button
|
||||||
} from '../lib/form';
|
} from '../lib/form';
|
||||||
|
@ -15,6 +15,7 @@ import mailtrainConfig from 'mailtrainConfig';
|
||||||
@withForm
|
@withForm
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class Account extends Component {
|
export default class Account extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -120,6 +121,8 @@ export default class Account extends Component {
|
||||||
this.updateFormValue('currentPassword', '');
|
this.updateFormValue('currentPassword', '');
|
||||||
|
|
||||||
this.clearFormStatusMessage();
|
this.clearFormStatusMessage();
|
||||||
|
this.enableForm();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.enableForm();
|
this.enableForm();
|
||||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { withPageHelpers, Title } from '../lib/page'
|
||||||
import {
|
import {
|
||||||
withForm, Form, FormSendMethod, InputField, CheckBox, ButtonRow, Button, AlignedRow
|
withForm, Form, FormSendMethod, InputField, ButtonRow, Button
|
||||||
} from '../lib/form';
|
} from '../lib/form';
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,8 @@ export default class Login extends Component {
|
||||||
this.setFormStatusMessage('info', t('Verifying credentials ...'));
|
this.setFormStatusMessage('info', t('Verifying credentials ...'));
|
||||||
|
|
||||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/login');
|
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/login');
|
||||||
|
/* FIXME, once we turn Mailtrain to single-page application, we should receive authenticated config (from client-helpers.js:getAuthenticatedConfig)
|
||||||
|
as part of login response. Then we should integrate it in the mailtrainConfig global variable. */
|
||||||
|
|
||||||
if (submitSuccessful) {
|
if (submitSuccessful) {
|
||||||
const query = new URL(this.props.location.search, true).query;
|
const query = new URL(this.props.location.search, true).query;
|
||||||
|
|
|
@ -5,12 +5,12 @@ import ReactDOM from 'react-dom';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../lib/i18n';
|
import i18n from '../lib/i18n';
|
||||||
|
|
||||||
import { Section } from '../lib/page'
|
import { Section } from '../lib/page';
|
||||||
import Account from './Account'
|
import Account from './Account';
|
||||||
import Login from './Login'
|
import Login from './Login';
|
||||||
import Reset from './Forgot'
|
import Reset from './Forgot';
|
||||||
import ResetLink from './Reset'
|
import ResetLink from './Reset';
|
||||||
import API from './API'
|
import API from './API';
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ function handleError(that, error) {
|
||||||
function withErrorHandling(target) {
|
function withErrorHandling(target) {
|
||||||
const inst = target.prototype;
|
const inst = target.prototype;
|
||||||
|
|
||||||
if (inst._withErrorHandlingApplied) return;
|
if (inst._withErrorHandlingApplied) return target;
|
||||||
inst._withErrorHandlingApplied = true;
|
inst._withErrorHandlingApplied = true;
|
||||||
|
|
||||||
const contextTypes = target.contextTypes || {};
|
const contextTypes = target.contextTypes || {};
|
||||||
|
|
|
@ -356,7 +356,7 @@ class TreeTableSelect extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@translate()
|
@translate(null, { withRef: true })
|
||||||
class TableSelect extends Component {
|
class TableSelect extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -369,6 +369,7 @@ class TableSelect extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dataUrl: PropTypes.string,
|
dataUrl: PropTypes.string,
|
||||||
|
data: PropTypes.array,
|
||||||
columns: PropTypes.array,
|
columns: PropTypes.array,
|
||||||
selectionKeyIndex: PropTypes.number,
|
selectionKeyIndex: PropTypes.number,
|
||||||
selectionLabelIndex: PropTypes.number,
|
selectionLabelIndex: PropTypes.number,
|
||||||
|
@ -426,6 +427,10 @@ class TableSelect extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.table.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const props = this.props;
|
const props = this.props;
|
||||||
const owner = this.context.formStateOwner;
|
const owner = this.context.formStateOwner;
|
||||||
|
@ -443,7 +448,7 @@ class TableSelect extends Component {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-tableselect-table' + (this.state.open ? '' : ' mt-tableselect-table-hidden')}>
|
<div className={'mt-tableselect-table' + (this.state.open ? '' : ' mt-tableselect-table-hidden')}>
|
||||||
<Table dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -451,7 +456,7 @@ class TableSelect extends Component {
|
||||||
return wrapInput(id, htmlId, owner, props.label, props.help,
|
return wrapInput(id, htmlId, owner, props.label, props.help,
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<Table dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -459,6 +464,15 @@ class TableSelect 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.
|
||||||
|
*/
|
||||||
|
TableSelect.prototype.refresh = function() {
|
||||||
|
this.getWrappedInstance().refresh()
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
class ACEEditor extends Component {
|
class ACEEditor extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -39,3 +39,8 @@
|
||||||
.mt-tableselect-dropdown input[readonly] {
|
.mt-tableselect-dropdown input[readonly] {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3.legend {
|
||||||
|
font-size: 21px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import './page.css';
|
||||||
import { withErrorHandling } from './error-handling';
|
import { withErrorHandling } from './error-handling';
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||||
import { DismissibleAlert, Button } from './bootstrap-components';
|
import { DismissibleAlert, Button } from './bootstrap-components';
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
|
|
||||||
class PageContent extends Component {
|
class PageContent extends Component {
|
||||||
|
@ -200,10 +201,16 @@ class SectionContent extends Component {
|
||||||
this.setFlashMessage(severity, text);
|
this.setFlashMessage(severity, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorHandler(error) {
|
ensureAuthenticated() {
|
||||||
if (error instanceof interoperableErrors.NotLoggedInError) {
|
if (!mailtrainConfig.isAuthenticated) {
|
||||||
/* FIXME, once we turn Mailtrain to single-page application, this should become navigateTo */
|
/* FIXME, once we turn Mailtrain to single-page application, this should become navigateTo */
|
||||||
window.location = '/account/login?next=' + encodeURIComponent(this.props.root);
|
window.location = '/account/login?next=' + encodeURIComponent(this.props.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorHandler(error) {
|
||||||
|
if (error instanceof interoperableErrors.NotLoggedInError) {
|
||||||
|
this.ensureAuthenticated();
|
||||||
} else if (error.response && error.response.data && error.response.data.message) {
|
} else if (error.response && error.response.data && error.response.data.message) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
|
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
|
||||||
|
@ -312,7 +319,7 @@ class DropdownLink extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
function withPageHelpers(target) {
|
function withPageHelpers(target) {
|
||||||
withErrorHandling(target);
|
target = withErrorHandling(target);
|
||||||
|
|
||||||
const inst = target.prototype;
|
const inst = target.prototype;
|
||||||
|
|
||||||
|
@ -346,16 +353,32 @@ function withPageHelpers(target) {
|
||||||
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
inst.axios
|
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requiresAuthenticatedUser(target) {
|
||||||
|
const comp1 = withPageHelpers(target);
|
||||||
|
|
||||||
|
function comp2(props, context) {
|
||||||
|
comp1.call(this, props, context);
|
||||||
|
context.sectionContent.ensureAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
comp2.prototype = comp1.prototype;
|
||||||
|
|
||||||
|
for (const attr in comp1) {
|
||||||
|
comp2[attr] = comp1[attr];
|
||||||
|
}
|
||||||
|
|
||||||
|
return comp2;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Section,
|
Section,
|
||||||
Title,
|
Title,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
NavButton,
|
NavButton,
|
||||||
DropdownLink,
|
DropdownLink,
|
||||||
withPageHelpers
|
withPageHelpers,
|
||||||
|
requiresAuthenticatedUser
|
||||||
};
|
};
|
|
@ -6,7 +6,6 @@ import { translate } from 'react-i18next';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import jQuery from 'jquery';
|
import jQuery from 'jquery';
|
||||||
import '../../public/jquery/jquery-ui-1.12.1.min.js';
|
|
||||||
|
|
||||||
import 'datatables.net';
|
import 'datatables.net';
|
||||||
import 'datatables.net-bs';
|
import 'datatables.net-bs';
|
||||||
|
@ -82,13 +81,20 @@ class Table extends Component {
|
||||||
selMap.set(elem, undefined);
|
selMap.set(elem, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.table) {
|
if (props.data) {
|
||||||
const self = this;
|
for (const rowData of props.data) {
|
||||||
this.table.rows().every(function() {
|
const key = rowData[props.selectionKeyIndex];
|
||||||
const data = this.data();
|
|
||||||
const key = data[self.props.selectionKeyIndex];
|
|
||||||
if (selMap.has(key)) {
|
if (selMap.has(key)) {
|
||||||
selMap.set(key, data);
|
selMap.set(key, rowData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (this.table) {
|
||||||
|
this.table.rows().every(function() {
|
||||||
|
const rowData = this.data();
|
||||||
|
const key = rowData[props.selectionKeyIndex];
|
||||||
|
if (selMap.has(key)) {
|
||||||
|
selMap.set(key, rowData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -118,8 +124,9 @@ class Table extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async fetchSelectionData() {
|
async fetchAndNotifySelectionData() {
|
||||||
if (this.props.onSelectionDataAsync) {
|
if (this.props.onSelectionDataAsync) {
|
||||||
|
if (!this.props.data) {
|
||||||
const keysToFetch = [];
|
const keysToFetch = [];
|
||||||
for (const pair of this.selectionMap.entries()) {
|
for (const pair of this.selectionMap.entries()) {
|
||||||
if (!pair[1]) {
|
if (!pair[1]) {
|
||||||
|
@ -141,6 +148,7 @@ class Table extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.notifySelection(this.props.onSelectionDataAsync, this.selectionMap);
|
this.notifySelection(this.props.onSelectionDataAsync, this.selectionMap);
|
||||||
}
|
}
|
||||||
|
@ -293,7 +301,7 @@ class Table extends Component {
|
||||||
clearTimeout(this.refreshTimeoutId);
|
clearTimeout(this.refreshTimeoutId);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fetchSelectionData();
|
this.fetchAndNotifySelectionData();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -314,7 +322,7 @@ class Table extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSelectInfo();
|
this.updateSelectInfo();
|
||||||
this.fetchSelectionData();
|
this.fetchAndNotifySelectionData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async notifySelection(eventCallback, newSelectionMap) {
|
async notifySelection(eventCallback, newSelectionMap) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
|
||||||
import { withForm, Form, FormSendMethod, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
import { withForm, Form, FormSendMethod, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
|
@ -13,6 +14,7 @@ import { ModalDialog } from '../lib/bootstrap-components';
|
||||||
@withForm
|
@withForm
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class CUD extends Component {
|
export default class CUD extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -28,6 +30,10 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
edit: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
isEditGlobal() {
|
isEditGlobal() {
|
||||||
return this.state.entityId === 1;
|
return this.state.entityId === 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,17 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
|
||||||
import { TreeTable } from '../lib/tree';
|
import { TreeTable } from '../lib/tree';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
@withPageHelpers
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class List extends Component {
|
export default class List extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ import ReactDOM from 'react-dom';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../lib/i18n';
|
import i18n from '../lib/i18n';
|
||||||
|
|
||||||
import { Section } from '../lib/page'
|
import { Section } from '../lib/page';
|
||||||
import CUD from './CUD'
|
import CUD from './CUD';
|
||||||
import List from './List'
|
import List from './List';
|
||||||
|
|
||||||
const getStructure = t => ({
|
const getStructure = t => ({
|
||||||
'': {
|
'': {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { translate, Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
|
||||||
import {
|
import {
|
||||||
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, TableSelectMode, ButtonRow, Button,
|
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, TableSelectMode, ButtonRow, Button,
|
||||||
Fieldset
|
Fieldset
|
||||||
|
@ -16,6 +17,7 @@ import moment from 'moment';
|
||||||
@withForm
|
@withForm
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class CUD extends Component {
|
export default class CUD extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -33,6 +35,10 @@ export default class CUD extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
edit: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
isDelete() {
|
isDelete() {
|
||||||
return this.props.match.params.action === 'delete';
|
return this.props.match.params.action === 'delete';
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
import { withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
|
||||||
import { Table } from '../lib/table';
|
import { Table } from '../lib/table';
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
@ -11,6 +11,7 @@ import { ReportState } from '../../../shared/reports';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@withPageHelpers
|
||||||
export default class List extends Component {
|
export default class List extends Component {
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class Output extends Component {
|
export default class Output extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import { ReportState } from '../../../shared/reports';
|
import { ReportState } from '../../../shared/reports';
|
||||||
|
@ -10,6 +10,7 @@ import { ReportState } from '../../../shared/reports';
|
||||||
@translate()
|
@translate()
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class View extends Component {
|
export default class View extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -5,13 +5,15 @@ import ReactDOM from 'react-dom';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../lib/i18n';
|
import i18n from '../lib/i18n';
|
||||||
|
|
||||||
import { Section } from '../lib/page'
|
import { Section } from '../lib/page';
|
||||||
import ReportsCUD from './CUD'
|
import ReportsCUD from './CUD';
|
||||||
import ReportsList from './List'
|
import ReportsList from './List';
|
||||||
import ReportsView from './View'
|
import ReportsView from './View';
|
||||||
import ReportsOutput from './Output'
|
import ReportsOutput from './Output';
|
||||||
import ReportTemplatesCUD from './templates/CUD'
|
import ReportTemplatesCUD from './templates/CUD';
|
||||||
import ReportTemplatesList from './templates/List'
|
import ReportTemplatesList from './templates/List';
|
||||||
|
import Share from '../shares/Share';
|
||||||
|
|
||||||
|
|
||||||
const getStructure = t => {
|
const getStructure = t => {
|
||||||
const subPaths = {};
|
const subPaths = {};
|
||||||
|
@ -59,11 +61,16 @@ const getStructure = t => {
|
||||||
title: t('Create Report Template'),
|
title: t('Create Report Template'),
|
||||||
params: [':wizard?'],
|
params: [':wizard?'],
|
||||||
render: props => (<ReportTemplatesCUD {...props} />)
|
render: props => (<ReportTemplatesCUD {...props} />)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
share: {
|
||||||
|
title: t('Share Report Template'),
|
||||||
|
params: [':id'],
|
||||||
|
render: props => (<Share title={entity => t('Share Report Template "{{name}}"', {name: entity.name})} getUrl={id => `/rest/report-templates/${id}`} entityTypeId="reportTemplate" {...props} />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { translate, Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../../lib/page'
|
import { withPageHelpers, Title } from '../../lib/page'
|
||||||
import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../../lib/form';
|
import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../../lib/form';
|
||||||
|
@ -25,6 +26,10 @@ export default class CUD extends Component {
|
||||||
this.initForm();
|
this.initForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
edit: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
isDelete() {
|
isDelete() {
|
||||||
return this.props.match.params.action === 'delete';
|
return this.props.match.params.action === 'delete';
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,18 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { DropdownMenu } from '../../lib/bootstrap-components';
|
import { DropdownMenu } from '../../lib/bootstrap-components';
|
||||||
import { Title, Toolbar, DropdownLink } from '../../lib/page';
|
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, DropdownLink } from '../../lib/page';
|
||||||
import { Table } from '../../lib/table';
|
import { Table } from '../../lib/table';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
@withPageHelpers
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class List extends Component {
|
export default class List extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
|
@ -16,6 +22,10 @@ export default class List extends Component {
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
link: '/reports/templates/edit/' + data[0]
|
link: '/reports/templates/edit/' + data[0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Share',
|
||||||
|
link: '/reports/templates/share/' + data[0]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
179
client/src/shares/Share.js
Normal file
179
client/src/shares/Share.js
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { translate } from 'react-i18next';
|
||||||
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
|
||||||
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
|
import {
|
||||||
|
withForm, Form, FormSendMethod, TableSelect, ButtonRow, Button
|
||||||
|
} from '../lib/form';
|
||||||
|
import { Table } from '../lib/table';
|
||||||
|
import axios from '../lib/axios';
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withForm
|
||||||
|
@withPageHelpers
|
||||||
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
|
export default class Share extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
entityId: parseInt(props.match.params.id)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.initForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
title: PropTypes.func,
|
||||||
|
getUrl: PropTypes.func,
|
||||||
|
entityTypeId: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
@withAsyncErrorHandler
|
||||||
|
async loadEntity() {
|
||||||
|
const response = await axios.get(this.props.getUrl(this.state.entityId));
|
||||||
|
this.setState({
|
||||||
|
entity: response.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@withAsyncErrorHandler
|
||||||
|
async deleteShare(userId) {
|
||||||
|
const data = {
|
||||||
|
entityTypeId: this.props.entityTypeId,
|
||||||
|
entityId: this.state.entityId,
|
||||||
|
userId
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.put('/rest/shares', data);
|
||||||
|
this.sharesTable.refresh();
|
||||||
|
this.usersTableSelect.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearShareFields() {
|
||||||
|
this.populateFormValues({
|
||||||
|
entityTypeId: this.props.entityTypeId,
|
||||||
|
entityId: this.state.entityId,
|
||||||
|
userId: null,
|
||||||
|
role: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadEntity();
|
||||||
|
this.clearShareFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
localValidateFormValues(state) {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
if (!state.getIn(['userId', 'value'])) {
|
||||||
|
state.setIn(['userId', 'error'], t('User must not be empty'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['userId', 'error'], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.getIn(['role', 'value'])) {
|
||||||
|
state.setIn(['role', 'error'], t('Role must be selected'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['role', 'error'], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitHandler() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
this.disableForm();
|
||||||
|
this.setFormStatusMessage('info', t('Saving ...'));
|
||||||
|
|
||||||
|
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, '/rest/shares');
|
||||||
|
|
||||||
|
if (submitSuccessful) {
|
||||||
|
this.hideFormValidation();
|
||||||
|
this.clearShareFields();
|
||||||
|
this.enableForm();
|
||||||
|
|
||||||
|
this.clearFormStatusMessage();
|
||||||
|
this.sharesTable.refresh();
|
||||||
|
this.usersTableSelect.refresh();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.enableForm();
|
||||||
|
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and try again.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
const roles = mailtrainConfig.roles[this.props.entityTypeId];
|
||||||
|
|
||||||
|
const actions = data => [
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
action: () => this.deleteShare(data[4])
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const sharesColumns = [
|
||||||
|
{ data: 1, title: t('Username') },
|
||||||
|
{ data: 2, title: t('Name') },
|
||||||
|
{ data: 3, title: t('Role'), render: data => roles[data] ? roles[data].name : data }
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
let usersLabelIndex = 1;
|
||||||
|
const usersColumns = [
|
||||||
|
{ data: 0, title: "#" },
|
||||||
|
{ data: 1, title: "Username" },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (mailtrainConfig.isAuthMethodLocal) {
|
||||||
|
usersColumns.push({ data: 2, title: "Full Name" });
|
||||||
|
usersLabelIndex = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const rolesColumns = [
|
||||||
|
{ data: 1, title: "Name" },
|
||||||
|
{ data: 2, title: "Description" },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const rolesData = [];
|
||||||
|
for (const key in roles) {
|
||||||
|
const role = roles[key];
|
||||||
|
rolesData.push([ key, role.name, role.description ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (this.state.entity) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title>{this.props.title(this.state.entity)}</Title>
|
||||||
|
|
||||||
|
<h3 className="legend">{t('Add User')}</h3>
|
||||||
|
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||||
|
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`/rest/shares-users-table/${this.props.entityTypeId}/${this.state.entityId}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
|
||||||
|
<TableSelect id="role" label={t('Role')} withHeader dropdown data={rolesData} columns={rolesColumns} selectionLabelIndex={1}/>
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Share')}/>
|
||||||
|
</ButtonRow>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<h3 className="legend">{t('Existing Users')}</h3>
|
||||||
|
|
||||||
|
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`/rest/shares-table/${this.props.entityTypeId}/${this.state.entityId}`} columns={sharesColumns} actions={actions}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (<p>{t('Loading ...')}</p>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
|
||||||
import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button } from '../lib/form';
|
import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button } from '../lib/form';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
|
@ -16,6 +17,7 @@ import mailtrainConfig from 'mailtrainConfig';
|
||||||
@withForm
|
@withForm
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class CUD extends Component {
|
export default class CUD extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -37,6 +39,10 @@ export default class CUD extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
edit: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
isDelete() {
|
isDelete() {
|
||||||
return this.props.match.params.action === 'delete';
|
return this.props.match.params.action === 'delete';
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,18 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
|
||||||
import { Table } from '../lib/table';
|
import { Table } from '../lib/table';
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
@withPageHelpers
|
||||||
|
@requiresAuthenticatedUser
|
||||||
export default class List extends Component {
|
export default class List extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ import ReactDOM from 'react-dom';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../lib/i18n';
|
import i18n from '../lib/i18n';
|
||||||
|
|
||||||
import { Section } from '../lib/page'
|
import { Section } from '../lib/page';
|
||||||
import CUD from './CUD'
|
import CUD from './CUD';
|
||||||
import List from './List'
|
import List from './List';
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
const getStructure = t => {
|
const getStructure = t => {
|
||||||
|
|
|
@ -188,16 +188,21 @@ logger=false
|
||||||
browser="phantomjs"
|
browser="phantomjs"
|
||||||
|
|
||||||
|
|
||||||
[roles.list.master]
|
[roles.reportTemplate.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view"]
|
permissions=["view", "edit", "delete"]
|
||||||
|
|
||||||
[roles.namespace.master]
|
#[roles.list.master]
|
||||||
name="Master"
|
#name="Master"
|
||||||
description="All permissions"
|
#description="All permissions"
|
||||||
permissions=["view", "edit", "create", "delete", "create list"]
|
#permissions=["view"]
|
||||||
|
#
|
||||||
[roles.namespace.master.childperms]
|
#[roles.namespace.master]
|
||||||
list=["view"]
|
#name="Master"
|
||||||
namespace=["view", "edit", "create", "delete", "create list"]
|
#description="All permissions"
|
||||||
|
#permissions=["view", "edit", "create", "delete", "create list"]
|
||||||
|
#
|
||||||
|
#[roles.namespace.master.childperms]
|
||||||
|
#list=["view"]
|
||||||
|
#namespace=["view", "edit", "create", "delete", "create list"]
|
||||||
|
|
|
@ -2,29 +2,58 @@
|
||||||
|
|
||||||
const passport = require('./passport');
|
const passport = require('./passport');
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
|
const permissions = require('./permissions');
|
||||||
|
|
||||||
function _getConfig(context) {
|
function getAnonymousConfig(context) {
|
||||||
return {
|
return {
|
||||||
authMethod: passport.authMethod,
|
authMethod: passport.authMethod,
|
||||||
isAuthMethodLocal: passport.isAuthMethodLocal,
|
isAuthMethodLocal: passport.isAuthMethodLocal,
|
||||||
externalPasswordResetLink: config.ldap.passwordresetlink,
|
externalPasswordResetLink: config.ldap.passwordresetlink,
|
||||||
language: config.language || 'en',
|
language: config.language || 'en',
|
||||||
userId: context.user ? context.user.id : undefined
|
isAuthenticated: !!context.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthenticatedConfig(context) {
|
||||||
|
const roles = {};
|
||||||
|
for (const entityTypeId in config.roles) {
|
||||||
|
const rolesPerEntityType = {};
|
||||||
|
for (const roleId in config.roles[entityTypeId]) {
|
||||||
|
const roleSpec = config.roles[entityTypeId][roleId];
|
||||||
|
|
||||||
|
rolesPerEntityType[roleId] = {
|
||||||
|
name: roleSpec.name,
|
||||||
|
description: roleSpec.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roles[entityTypeId] = rolesPerEntityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: context.user.id,
|
||||||
|
roles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerRootRoute(router, entryPoint, title) {
|
function registerRootRoute(router, entryPoint, title) {
|
||||||
router.get('/*', passport.csrfProtection, (req, res) => {
|
router.get('/*', passport.csrfProtection, (req, res) => {
|
||||||
|
const mailtrainConfig = getAnonymousConfig(req.context);
|
||||||
|
if (req.user) {
|
||||||
|
Object.assign(mailtrainConfig, getAuthenticatedConfig(req.context));
|
||||||
|
}
|
||||||
|
|
||||||
res.render('react-root', {
|
res.render('react-root', {
|
||||||
title,
|
title,
|
||||||
reactEntryPoint: entryPoint,
|
reactEntryPoint: entryPoint,
|
||||||
reactCsrfToken: req.csrfToken(),
|
reactCsrfToken: req.csrfToken(),
|
||||||
mailtrainConfig: JSON.stringify(_getConfig(req.context))
|
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
registerRootRoute
|
registerRootRoute,
|
||||||
|
getAuthenticatedConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ const knex = require('knex')({
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: __dirname + '/../setup/knex/migrations'
|
directory: __dirname + '/../setup/knex/migrations'
|
||||||
}
|
}
|
||||||
|
// , debug: true
|
||||||
});
|
});
|
||||||
|
|
||||||
knex.migrate.latest();
|
knex.migrate.latest();
|
||||||
|
|
|
@ -2,6 +2,24 @@
|
||||||
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
|
|
||||||
|
|
||||||
|
// FIXME - redo or delete
|
||||||
|
|
||||||
|
/*
|
||||||
|
class ReportTemplatePermission {
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
this.entityType = 'report-template';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReportTemplatePermissions = {
|
||||||
|
View: new ReportTemplatePermission('view'),
|
||||||
|
Edit: new ReportTemplatePermission('edit'),
|
||||||
|
Delete: new ReportTemplatePermission('delete')
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
class ListPermission {
|
class ListPermission {
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -9,6 +27,10 @@ class ListPermission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ListPermissions = {
|
||||||
|
View: new ListPermissions('view')
|
||||||
|
};
|
||||||
|
|
||||||
class NamespacePermission {
|
class NamespacePermission {
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -16,10 +38,6 @@ class NamespacePermission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListPermissions = {
|
|
||||||
View: new ListPermissions('view')
|
|
||||||
};
|
|
||||||
|
|
||||||
const NamespacePermissions = {
|
const NamespacePermissions = {
|
||||||
View: new NamespacePermission('view'),
|
View: new NamespacePermission('view'),
|
||||||
Edit: new NamespacePermission('edit'),
|
Edit: new NamespacePermission('edit'),
|
||||||
|
@ -27,7 +45,9 @@ const NamespacePermissions = {
|
||||||
Delete: new NamespacePermission('delete'),
|
Delete: new NamespacePermission('delete'),
|
||||||
CreateList: new NamespacePermission('create list')
|
CreateList: new NamespacePermission('create list')
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
async function can(context, operation, entityId) {
|
async function can(context, operation, entityId) {
|
||||||
if (!context.user) {
|
if (!context.user) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -48,3 +68,8 @@ async function buildPermissions() {
|
||||||
|
|
||||||
can(ctx, ListPermissions.View, 3)
|
can(ctx, ListPermissions.View, 3)
|
||||||
can(ctx, NamespacePermissions.CreateList, 2)
|
can(ctx, NamespacePermissions.CreateList, 2)
|
||||||
|
can(ctx, ReportTemplatePermissions.ViewReport, 5)
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const _ = require('./translate')._;
|
const _ = require('./translate')._;
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const isemail = require('isemail')
|
const isemail = require('isemail');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validateEmail
|
validateEmail
|
||||||
|
|
94
models/shares.js
Normal file
94
models/shares.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const knex = require('../lib/knex');
|
||||||
|
const config = require('config');
|
||||||
|
const { enforce } = require('../lib/helpers');
|
||||||
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
|
||||||
|
const entityTypes = {
|
||||||
|
reportTemplate: {
|
||||||
|
entitiesTable: 'report_templates',
|
||||||
|
sharesTable: 'shares_report_template',
|
||||||
|
permissionsTable: 'permissions_report_template'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEntityType(entityTypeId) {
|
||||||
|
const entityType = entityTypes[entityTypeId];
|
||||||
|
|
||||||
|
if (!entityType) {
|
||||||
|
throw new Error(`Unknown entity type ${entityTypeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityType
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listDTAjax(entityTypeId, entityId, params) {
|
||||||
|
const entityType = getEntityType(entityTypeId);
|
||||||
|
return await dtHelpers.ajaxList(params, tx => tx(entityType.sharesTable).innerJoin('users', entityType.sharesTable + '.user', 'users.id'), [entityType.sharesTable + '.id', 'users.username', 'users.name', entityType.sharesTable + '.role', 'users.id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listUnassignedUsersDTAjax(entityTypeId, entityId, params) {
|
||||||
|
const entityType = getEntityType(entityTypeId);
|
||||||
|
return await dtHelpers.ajaxList(
|
||||||
|
params,
|
||||||
|
tx => tx('users').whereNotExists(function() { return this.select('*').from(entityType.sharesTable).whereRaw(`users.id = ${entityType.sharesTable}.user`); }),
|
||||||
|
['users.id', 'users.username', 'users.name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function assign(entityTypeId, entityId, userId, role) {
|
||||||
|
const entityType = getEntityType(entityTypeId);
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
|
||||||
|
enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id');
|
||||||
|
|
||||||
|
const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('id', 'role').first();
|
||||||
|
|
||||||
|
if (entry) {
|
||||||
|
if (!role) {
|
||||||
|
await tx(entityType.sharesTable).where('id', entry.id).del();
|
||||||
|
} else if (entry.role !== role) {
|
||||||
|
await tx(entityType.sharesTable).where('id', entry.id).update('role', role);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await tx(entityType.sharesTable).insert({
|
||||||
|
user: userId,
|
||||||
|
entity: entityId,
|
||||||
|
role
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx(entityType.permissionsTable).where({user: userId, entity: entityId}).del();
|
||||||
|
if (role) {
|
||||||
|
const permissions = config.roles[entityTypeId][role].permissions;
|
||||||
|
const data = permissions.map(operation => ({user: userId, entity: entityId, operation}));
|
||||||
|
await tx(entityType.permissionsTable).insert(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rebuildPermissions() {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
for (const entityTypeId in entityTypes) {
|
||||||
|
const entityType = entityTypes[entityTypeId];
|
||||||
|
|
||||||
|
await tx(entityType.permissionsTable).del();
|
||||||
|
|
||||||
|
const shares = await tx(entityType.sharesTable).select(['entity', 'user', 'role']);
|
||||||
|
for (const share in shares) {
|
||||||
|
const permissions = config.roles[entityTypeId][share.role].permissions;
|
||||||
|
const data = permissions.map(operation => ({user: share.user, entity: share.entity, operation}));
|
||||||
|
await tx(entityType.permissionsTable).insert(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listDTAjax,
|
||||||
|
listUnassignedUsersDTAjax,
|
||||||
|
assign,
|
||||||
|
rebuildPermissions
|
||||||
|
};
|
27
routes/rest/shares.js
Normal file
27
routes/rest/shares.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const passport = require('../../lib/passport');
|
||||||
|
const _ = require('../../lib/translate')._;
|
||||||
|
const shares = require('../../models/shares');
|
||||||
|
const permissions = require('../../lib/permissions')
|
||||||
|
|
||||||
|
const router = require('../../lib/router-async').create();
|
||||||
|
|
||||||
|
router.postAsync('/shares-table/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await shares.listDTAjax(req.params.entityTypeId, req.params.entityId, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/shares-users-table/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await shares.listUnassignedUsersDTAjax(req.params.entityTypeId, req.params.entityId, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.putAsync('/shares', passport.loggedIn, async (req, res) => {
|
||||||
|
// FIXME: Check that the user has the right to assign the role
|
||||||
|
|
||||||
|
const body = req.body;
|
||||||
|
await shares.assign(body.entityTypeId, body.entityId, body.userId, body.role);
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -39,5 +39,4 @@ router.postAsync('/users-table', passport.loggedIn, async (req, res) => {
|
||||||
return res.json(await users.listDTAjax(req.body));
|
return res.json(await users.listDTAjax(req.body));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
|
@ -5,14 +5,6 @@ exports.up = function(knex, Promise) {
|
||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
table.integer('entity').unsigned().notNullable().references('lists.id').onDelete('CASCADE');
|
table.integer('entity').unsigned().notNullable().references('lists.id').onDelete('CASCADE');
|
||||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||||
table.integer('role', 64).notNullable();
|
|
||||||
table.unique(['entity', 'user']);
|
|
||||||
})
|
|
||||||
|
|
||||||
.createTable('shares_namespace', table => {
|
|
||||||
table.increments('id').primary();
|
|
||||||
table.integer('entity').unsigned().notNullable().references('namespaces.id').onDelete('CASCADE');
|
|
||||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
|
||||||
table.string('role', 64).notNullable();
|
table.string('role', 64).notNullable();
|
||||||
table.unique(['entity', 'user']);
|
table.unique(['entity', 'user']);
|
||||||
})
|
})
|
||||||
|
@ -25,6 +17,30 @@ exports.up = function(knex, Promise) {
|
||||||
table.unique(['entity', 'user', 'operation']);
|
table.unique(['entity', 'user', 'operation']);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.createTable('shares_report_template', table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('entity').unsigned().notNullable().references('report_templates.id').onDelete('CASCADE');
|
||||||
|
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||||
|
table.string('role', 64).notNullable();
|
||||||
|
table.unique(['entity', 'user']);
|
||||||
|
})
|
||||||
|
|
||||||
|
.createTable('permissions_report_template', table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('entity').unsigned().notNullable().references('report_templates.id').onDelete('CASCADE');
|
||||||
|
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||||
|
table.string('operation', 64).notNullable();
|
||||||
|
table.unique(['entity', 'user', 'operation']);
|
||||||
|
})
|
||||||
|
|
||||||
|
.createTable('shares_namespace', table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('entity').unsigned().notNullable().references('namespaces.id').onDelete('CASCADE');
|
||||||
|
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||||
|
table.string('role', 64).notNullable();
|
||||||
|
table.unique(['entity', 'user']);
|
||||||
|
})
|
||||||
|
|
||||||
.createTable('permissions_namespace', table => {
|
.createTable('permissions_namespace', table => {
|
||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
table.integer('entity').unsigned().notNullable().references('namespaces.id').onDelete('CASCADE');
|
table.integer('entity').unsigned().notNullable().references('namespaces.id').onDelete('CASCADE');
|
||||||
|
|
|
@ -77,7 +77,6 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
{{#if admin }}
|
{{#if admin }}
|
||||||
<ul class="nav navbar-nav">
|
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Administration<span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Administration<span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
@ -108,7 +107,6 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<li><a href="https://github.com/Mailtrain-org/mailtrain/wiki"><span class="glyphicon glyphicon-share-alt" aria-hidden="true"></span> {{#translate}}Wiki{{/translate}}</a></li>
|
<li><a href="https://github.com/Mailtrain-org/mailtrain/wiki"><span class="glyphicon glyphicon-share-alt" aria-hidden="true"></span> {{#translate}}Wiki{{/translate}}</a></li>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue