Various fixes in the UI.

Check permissions mechanism reworked to allow specifying permission checks already in menu structure.
This commit is contained in:
Tomas Bures 2019-07-29 09:24:50 +02:00
parent a46c8fa9c3
commit a258479621
37 changed files with 485 additions and 399 deletions

View file

@ -22,7 +22,7 @@ import {
withFormErrorHandlers withFormErrorHandlers
} from '../lib/form'; } from '../lib/form';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling';
import {NamespaceSelect, validateNamespace} from '../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals"; import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import {getTagLanguages, getTemplateTypes, getTypeForm, ResourceType} from '../templates/helpers'; import {getTagLanguages, getTemplateTypes, getTypeForm, ResourceType} from '../templates/helpers';
@ -109,6 +109,7 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
entity: PropTypes.object, entity: PropTypes.object,
permissions: PropTypes.object,
type: PropTypes.number type: PropTypes.number
} }
@ -176,7 +177,12 @@ export default class CUD extends Component {
} }
for (const overridable of campaignOverridables) { for (const overridable of campaignOverridables) {
data[overridable + '_overriden'] = data[overridable + '_override'] !== null; if (data[overridable + '_override'] === null) {
data[overridable + '_override'] = '';
data[overridable + '_overriden'] = false;
} else {
data[overridable + '_overriden'] = true;
}
} }
const lsts = []; const lsts = [];
@ -297,7 +303,7 @@ export default class CUD extends Component {
lists: [lstUid], lists: [lstUid],
send_configuration: null, send_configuration: null,
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
subject: '', subject: '',

View file

@ -4,15 +4,15 @@ import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n'; import {withTranslation} from '../lib/i18n';
import {ButtonDropdown, Icon} from '../lib/bootstrap-components'; import {ButtonDropdown, Icon} from '../lib/bootstrap-components';
import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page'; import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table'; import {Table} from '../lib/table';
import moment from 'moment'; import moment from 'moment';
import {CampaignSource, CampaignStatus, CampaignType} from "../../../shared/campaigns"; import {CampaignSource, CampaignStatus, CampaignType} from "../../../shared/campaigns";
import {checkPermissions} from "../lib/permissions";
import {getCampaignLabels} from "./helpers"; import {getCampaignLabels} from "./helpers";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import styles from "./styles.scss"; import styles from "./styles.scss";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -34,28 +34,16 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createCampaign: {
entityTypeId: 'namespace',
requiredOperations: ['createCampaign']
}
});
this.setState({
createPermitted: result.data.createCampaign
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createCampaign;
const columns = [ const columns = [
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code>, className: styles.tblCol_id }, { data: 2, title: t('id'), render: data => <code>{data}</code>, className: styles.tblCol_id },
@ -153,7 +141,7 @@ export default class List extends Component {
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
<Toolbar> <Toolbar>
{this.state.createPermitted && {createPermitted &&
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createCampaign')}> <ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createCampaign')}>
<DropdownLink to="/campaigns/create-regular">{t('regular')}</DropdownLink> <DropdownLink to="/campaigns/create-regular">{t('regular')}</DropdownLink>
<DropdownLink to="/campaigns/create-rss">{t('rss')}</DropdownLink> <DropdownLink to="/campaigns/create-rss">{t('rss')}</DropdownLink>

View file

@ -17,6 +17,7 @@ import {SubscriptionStatus} from "../../../shared/lists";
import StatisticsOpened from "./StatisticsOpened"; import StatisticsOpened from "./StatisticsOpened";
import StatisticsLinkClicks from "./StatisticsLinkClicks"; import StatisticsLinkClicks from "./StatisticsLinkClicks";
import {ellipsizeBreadcrumbLabel} from "../lib/helpers" import {ellipsizeBreadcrumbLabel} from "../lib/helpers"
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) { function getMenus(t) {
const aggLabels = { const aggLabels = {
@ -28,7 +29,14 @@ function getMenus(t) {
'campaigns': { 'campaigns': {
title: t('campaigns'), title: t('campaigns'),
link: '/campaigns', link: '/campaigns',
panelComponent: CampaignsList, checkPermissions: {
createCampaign: {
entityTypeId: 'namespace',
requiredOperations: ['createCampaign']
},
...namespaceCheckPermissions('createCampaign')
},
panelRender: props => <CampaignsList permissions={props.permissions}/>,
children: { children: {
':campaignId([0-9]+)': { ':campaignId([0-9]+)': {
title: resolved => t('campaignName', {name: ellipsizeBreadcrumbLabel(resolved.campaign.name)}), title: resolved => t('campaignName', {name: ellipsizeBreadcrumbLabel(resolved.campaign.name)}),
@ -94,7 +102,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/campaigns/${params.campaignId}/edit`, link: params => `/campaigns/${params.campaignId}/edit`,
visible: resolved => resolved.campaign.permissions.includes('edit'), visible: resolved => resolved.campaign.permissions.includes('edit'),
panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} /> panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} permissions={props.permissions} />
}, },
content: { content: {
title: t('content'), title: t('content'),
@ -153,15 +161,15 @@ function getMenus(t) {
}, },
'create-regular': { 'create-regular': {
title: t('createRegularCampaign'), title: t('createRegularCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.REGULAR} /> panelRender: props => <CampaignsCUD action="create" type={CampaignType.REGULAR} permissions={props.permissions} />
}, },
'create-rss': { 'create-rss': {
title: t('createRssCampaign'), title: t('createRssCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.RSS} /> panelRender: props => <CampaignsCUD action="create" type={CampaignType.RSS} permissions={props.permissions} />
}, },
'create-triggered': { 'create-triggered': {
title: t('createTriggeredCampaign'), title: t('createTriggeredCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.TRIGGERED} /> panelRender: props => <CampaignsCUD action="create" type={CampaignType.TRIGGERED} permissions={props.permissions} />
} }
} }
} }

View file

@ -818,7 +818,7 @@ class TreeTableSelect extends Component {
dataUrl: PropTypes.string, dataUrl: PropTypes.string,
data: PropTypes.array, data: PropTypes.array,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
format: PropTypes.string format: PropTypes.string,
} }
async onSelectionChangedAsync(sel) { async onSelectionChangedAsync(sel) {

View file

@ -89,7 +89,7 @@ export class RestActionModalDialog extends Component {
const t = this.props.t; const t = this.props.t;
return ( return (
<ModalDialog hidden={!this.props.visible} title={this.props.title} onCloseAsync={() => this.hideModal(true)} buttons={[ <ModalDialog hidden={!this.props.visible} title={this.props.title} onCloseAsync={async () => await this.hideModal(true)} buttons={[
{ label: t('no'), className: 'btn-primary', onClickAsync: async () => await this.hideModal(true) }, { label: t('no'), className: 'btn-primary', onClickAsync: async () => await this.hideModal(true) },
{ label: t('yes'), className: 'btn-danger', onClickAsync: ::this.performAction } { label: t('yes'), className: 'btn-danger', onClickAsync: ::this.performAction }
]}> ]}>
@ -115,16 +115,23 @@ const entityTypeLabels = {
function _getDependencyErrorMessage(err, t, name) { function _getDependencyErrorMessage(err, t, name) {
return ( return (
<div> <div>
{err.data.dependencies.length > 0 ?
<>
<p>{t('cannoteDeleteNameDueToTheFollowing', {name})}</p> <p>{t('cannoteDeleteNameDueToTheFollowing', {name})}</p>
<ul className={styles.errorsList}> <ul className={styles.errorsList}>
{err.data.dependencies.map(dep => {err.data.dependencies.map(dep =>
dep.link ? dep.link ?
<li key={dep.link}><Link to={dep.link}>{entityTypeLabels[dep.entityTypeId](t)}: {dep.name}</Link></li> <li key={dep.link}><Link
to={dep.link}>{entityTypeLabels[dep.entityTypeId](t)}: {dep.name}</Link></li>
: // if no dep.link is present, it means the user has no permission to view the entity, thus only id without the link is shown : // if no dep.link is present, it means the user has no permission to view the entity, thus only id without the link is shown
<li key={dep.id}>{entityTypeLabels[dep.entityTypeId](t)}: [{dep.id}]</li> <li key={dep.id}>{entityTypeLabels[dep.entityTypeId](t)}: [{dep.id}]</li>
)} )}
{err.data.andMore && <li>{t('andMore')}</li>} {err.data.andMore && <li>{t('andMore')}</li>}
</ul> </ul>
</>
:
<p>{t('Cannot delete {{name}} due to hidden dependencies', {name})}</p>
}
</div> </div>
); );
} }
@ -144,10 +151,11 @@ export class DeleteModalDialog extends Component {
visible: PropTypes.bool.isRequired, visible: PropTypes.bool.isRequired,
stateOwner: PropTypes.object.isRequired, stateOwner: PropTypes.object.isRequired,
deleteUrl: PropTypes.string.isRequired, deleteUrl: PropTypes.string.isRequired,
backUrl: PropTypes.string, backUrl: PropTypes.string.isRequired,
successUrl: PropTypes.string, successUrl: PropTypes.string.isRequired,
deletingMsg: PropTypes.string.isRequired, deletingMsg: PropTypes.string.isRequired,
deletedMsg: PropTypes.string.isRequired deletedMsg: PropTypes.string.isRequired,
name: PropTypes.string
} }
async onErrorAsync(err) { async onErrorAsync(err) {
@ -172,7 +180,7 @@ export class DeleteModalDialog extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
const owner = this.props.stateOwner; const owner = this.props.stateOwner;
const name = owner.getFormValue('name') || ''; const name = this.props.name || owner.getFormValue('name') || '';
return <RestActionModalDialog return <RestActionModalDialog
title={t('confirmDeletion')} title={t('confirmDeletion')}

View file

@ -9,7 +9,7 @@ import {withComponentMixins} from "./decorator-helpers";
@withComponentMixins([ @withComponentMixins([
withTranslation withTranslation
]) ])
class NamespaceSelect extends Component { export class NamespaceSelect extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
@ -19,7 +19,7 @@ class NamespaceSelect extends Component {
} }
} }
function validateNamespace(t, state) { export function validateNamespace(t, state) {
if (!state.getIn(['namespace', 'value'])) { if (!state.getIn(['namespace', 'value'])) {
state.setIn(['namespace', 'error'], t('namespaceMustBeSelected')); state.setIn(['namespace', 'error'], t('namespaceMustBeSelected'));
} else { } else {
@ -27,7 +27,21 @@ function validateNamespace(t, state) {
} }
} }
export { export function getDefaultNamespace(permissions) {
NamespaceSelect, return permissions.viewUsersNamespace && permissions.createEntityInUsersNamespace ? mailtrainConfig.user.namespace : null;
validateNamespace }
export function namespaceCheckPermissions(createOperation) {
return {
createEntityInUsersNamespace: {
entityTypeId: 'namespace',
entityId: mailtrainConfig.user.namespace,
requiredOperations: [createOperation]
},
viewUsersNamespace: {
entityTypeId: 'namespace',
entityId: mailtrainConfig.user.namespace,
requiredOperations: ['view']
}
}; };
}

View file

@ -9,13 +9,23 @@ import {getUrl} from "./urls";
import {createComponentMixin, withComponentMixins} from "./decorator-helpers"; import {createComponentMixin, withComponentMixins} from "./decorator-helpers";
import {withTranslation} from "./i18n"; import {withTranslation} from "./i18n";
import shallowEqual from "shallowequal"; import shallowEqual from "shallowequal";
import {checkPermissions} from "./permissions";
async function resolve(route, match, prevResolvedByUrl) { async function resolve(route, match, prevResolverState) {
const resolved = {}; const resolved = {};
const resolvedByUrl = {}; const permissions = {};
const keysToGo = new Set(Object.keys(route.resolve)); const resolverState = {
resolvedByUrl: {},
permissionsBySig: {}
};
prevResolvedByUrl = prevResolvedByUrl || {}; prevResolverState = prevResolverState || {
resolvedByUrl: {},
permissionsBySig: {}
};
async function processResolve() {
const keysToGo = new Set(Object.keys(route.resolve));
while (keysToGo.size > 0) { while (keysToGo.size > 0) {
const urlsToResolve = []; const urlsToResolve = [];
@ -60,10 +70,10 @@ async function resolve(route, match, prevResolvedByUrl) {
const key = keysToResolve[idx]; const key = keysToResolve[idx];
const url = urlsToResolve[idx]; const url = urlsToResolve[idx];
if (url in prevResolvedByUrl) { if (url in prevResolverState.resolvedByUrl) {
const entity = prevResolvedByUrl[url]; const entity = prevResolverState.resolvedByUrl[url];
resolved[key] = entity; resolved[key] = entity;
resolvedByUrl[url] = entity; resolverState.resolvedByUrl[url] = entity;
} else { } else {
urlsToResolveByRest.push(url); urlsToResolveByRest.push(url);
@ -83,7 +93,7 @@ async function resolve(route, match, prevResolvedByUrl) {
for (let idx = 0; idx < keysToResolveByRest.length; idx++) { for (let idx = 0; idx < keysToResolveByRest.length; idx++) {
resolved[keysToResolveByRest[idx]] = resolvedArr[idx].data; resolved[keysToResolveByRest[idx]] = resolvedArr[idx].data;
resolvedByUrl[urlsToResolveByRest[idx]] = resolvedArr[idx].data; resolverState.resolvedByUrl[urlsToResolveByRest[idx]] = resolvedArr[idx].data;
} }
} }
@ -91,12 +101,50 @@ async function resolve(route, match, prevResolvedByUrl) {
keysToGo.delete(key); keysToGo.delete(key);
} }
} }
}
return { resolved, resolvedByUrl }; async function processCheckPermissions() {
const checkPermsRequest = {};
function getSig(checkPermissionsEntry) {
return `${checkPermissionsEntry.entityTypeId}-${checkPermissionsEntry.entityId || ''}-${checkPermissionsEntry.requiredOperations.join(',')}`;
}
for (const key in route.checkPermissions) {
const checkPermissionsEntry = route.checkPermissions[key];
const sig = getSig(checkPermissionsEntry);
if (sig in prevResolverState.permissionsBySig) {
const perm = prevResolverState.permissionsBySig[sig];
permissions[key] = perm;
resolverState.permissionsBySig[sig] = perm;
} else {
checkPermsRequest[key] = checkPermissionsEntry;
}
}
if (Object.keys(checkPermsRequest).length > 0) {
const result = await checkPermissions(checkPermsRequest);
for (const key in checkPermsRequest) {
const checkPermissionsEntry = checkPermsRequest[key];
const perm = result.data[key];
permissions[key] = perm;
resolverState.permissionsBySig[getSig(checkPermissionsEntry)] = perm;
}
}
}
await Promise.all([processResolve(), processCheckPermissions()]);
return { resolved, permissions, resolverState };
} }
export function getRoutes(structure, parentRoute) { export function getRoutes(structure, parentRoute) {
function _getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) { function _getRoutes(urlPrefix, resolve, checkPermissions, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
let routes = []; let routes = [];
for (let routeKey in structure) { for (let routeKey in structure) {
const entry = structure[routeKey]; const entry = structure[routeKey];
@ -115,6 +163,13 @@ export function getRoutes(structure, parentRoute) {
entryResolve = resolve; entryResolve = resolve;
} }
let entryCheckPermissions;
if (entry.checkPermissions) {
entryCheckPermissions = Object.assign({}, checkPermissions, entry.checkPermissions);
} else {
entryCheckPermissions = checkPermissions;
}
let navKeys; let navKeys;
const entryNavs = []; const entryNavs = [];
if (entry.navs) { if (entry.navs) {
@ -145,6 +200,7 @@ export function getRoutes(structure, parentRoute) {
panelInFullScreen: entry.panelInFullScreen, panelInFullScreen: entry.panelInFullScreen,
insideIframe: entry.insideIframe, insideIframe: entry.insideIframe,
resolve: entryResolve, resolve: entryResolve,
checkPermissions: entryCheckPermissions,
parents, parents,
navs: [...navs, ...entryNavs], navs: [...navs, ...entryNavs],
@ -167,12 +223,12 @@ export function getRoutes(structure, parentRoute) {
const childNavs = [...entryNavs]; const childNavs = [...entryNavs];
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true }); childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
routes = routes.concat(_getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent)); routes = routes.concat(_getRoutes(path + '/', entryResolve, entryCheckPermissions, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
} }
} }
if (entry.children) { if (entry.children) {
routes = routes.concat(_getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent)); routes = routes.concat(_getRoutes(path + '/', entryResolve, entryCheckPermissions, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
} }
} }
@ -192,10 +248,10 @@ export function getRoutes(structure, parentRoute) {
children: { ...(routeSpec.children || {}), ...(structure.children || {}) } children: { ...(routeSpec.children || {}), ...(structure.children || {}) }
}; };
return _getRoutes(parentRoute.urlPrefix, parentRoute.resolve, parentRoute.parents, { [parentRoute.routeKey]: extStructure }, parentRoute.siblingNavs, parentRoute.primaryMenuComponent, parentRoute.secondaryMenuComponent); return _getRoutes(parentRoute.urlPrefix, parentRoute.resolve, parentRoute.checkPermissions, parentRoute.parents, { [parentRoute.routeKey]: extStructure }, parentRoute.siblingNavs, parentRoute.primaryMenuComponent, parentRoute.secondaryMenuComponent);
} else { } else {
return _getRoutes('', {}, [], { "": structure }, [], null, null); return _getRoutes('', {}, {}, [], { "": structure }, [], null, null);
} }
} }
@ -209,11 +265,13 @@ export class Resolver extends Component {
this.state = { this.state = {
resolved: null, resolved: null,
resolvedByUrl: null permissions: null,
resolverState: null
}; };
if (Object.keys(props.route.resolve).length === 0) { if (Object.keys(props.route.resolve).length === 0 && Object.keys(props.route.checkPermissions).length === 0) {
this.state.resolved = {}; this.state.resolved = {};
this.state.permissions = {};
} }
} }
@ -228,28 +286,31 @@ export class Resolver extends Component {
async resolve(prevMatch) { async resolve(prevMatch) {
const props = this.props; const props = this.props;
if (Object.keys(props.route.resolve).length === 0) { if (Object.keys(props.route.resolve).length === 0 && Object.keys(props.route.checkPermissions).length === 0) {
this.setState({ this.setState({
resolved: {}, resolved: {},
resolvedByUrl: {} permissions: {},
resolverState: null
}); });
} else { } else {
const prevResolvedByUrl = this.state.resolvedByUrl; const prevResolverState = this.state.resolverState;
if (this.state.resolved) { if (this.state.resolverState) {
this.setState({ this.setState({
resolved: null, resolved: null,
resolvedByUrl: null permissions: null,
resolverState: null
}); });
} }
const {resolved, resolvedByUrl} = await resolve(props.route, props.match, prevResolvedByUrl); const {resolved, permissions, resolverState} = await resolve(props.route, props.match, prevResolverState);
if (!this.disregardResolve) { // This is to prevent the warning about setState on discarded component when we immediatelly redirect. if (!this.disregardResolve) { // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
this.setState({ this.setState({
resolved, resolved,
resolvedByUrl permissions,
resolverState
}); });
} }
} }
@ -272,7 +333,7 @@ export class Resolver extends Component {
} }
render() { render() {
return this.props.render(this.state.resolved, this.props); return this.props.render(this.state.resolved, this.state.permissions, this.props);
} }
} }
@ -316,9 +377,9 @@ class SubRoute extends Component {
const route = this.props.route; const route = this.props.route;
const params = this.props.match.params; const params = this.props.match.params;
const render = resolved => { const render = (resolved, permissions) => {
if (resolved) { if (resolved && permissions) {
const subStructure = route.structure(resolved, params); const subStructure = route.structure(resolved, permissions, params);
const routes = getRoutes(subStructure, route); const routes = getRoutes(subStructure, route);
const _renderRoute = route => { const _renderRoute = route => {

View file

@ -268,16 +268,17 @@ class PanelRoute extends Component {
const panelInFullScreen = this.state.panelInFullScreen; const panelInFullScreen = this.state.panelInFullScreen;
const render = resolved => { const render = (resolved, permissions) => {
let primaryMenu = null; let primaryMenu = null;
let secondaryMenu = null; let secondaryMenu = null;
let content = null; let content = null;
if (resolved) { if (resolved && permissions) {
const compProps = { const compProps = {
match: this.props.match, match: this.props.match,
location: this.props.location, location: this.props.location,
resolved, resolved,
permissions,
setPanelInFullScreen: this.setPanelInFullScreen, setPanelInFullScreen: this.setPanelInFullScreen,
panelInFullScreen: this.state.panelInFullScreen panelInFullScreen: this.state.panelInFullScreen
}; };

View file

@ -3,10 +3,6 @@
import {getUrl} from "./urls"; import {getUrl} from "./urls";
import axios from "./axios"; import axios from "./axios";
async function checkPermissions(request) { export async function checkPermissions(request) {
return await axios.post(getUrl('rest/permissions-check'), request); return await axios.post(getUrl('rest/permissions-check'), request);
} }
export {
checkPermissions
}

View file

@ -36,7 +36,7 @@ class TreeTable extends Component {
this.mounted = false; this.mounted = false;
this.state = { this.state = {
treeData: [] treeData: null
}; };
if (props.data) { if (props.data) {
@ -99,6 +99,7 @@ class TreeTable extends Component {
// XSS protection // XSS protection
sanitizeTreeData(unsafeData) { sanitizeTreeData(unsafeData) {
const data = []; const data = [];
if (unsafeData) {
for (const unsafeEntry of unsafeData) { for (const unsafeEntry of unsafeData) {
const entry = Object.assign({}, unsafeEntry); const entry = Object.assign({}, unsafeEntry);
entry.unsanitizedTitle = entry.title; entry.unsanitizedTitle = entry.title;
@ -109,6 +110,8 @@ class TreeTable extends Component {
} }
data.push(entry); data.push(entry);
} }
}
return data; return data;
} }
@ -193,6 +196,7 @@ class TreeTable extends Component {
createNode: createNodeFn, createNode: createNodeFn,
checkbox: this.selectMode === TreeSelectMode.MULTI, checkbox: this.selectMode === TreeSelectMode.MULTI,
activate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null), activate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null),
deactivate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null),
select: (this.selectMode === TreeSelectMode.MULTI ? ::this.onSelect : null), select: (this.selectMode === TreeSelectMode.MULTI ? ::this.onSelect : null),
}; };
@ -241,7 +245,22 @@ class TreeTable extends Component {
tree.enableUpdate(true); tree.enableUpdate(true);
} else if (this.selectMode === TreeSelectMode.SINGLE) { } else if (this.selectMode === TreeSelectMode.SINGLE) {
this.tree.activateKey(this.stringifyKey(this.props.selection)); let selection = this.stringifyKey(this.props.selection);
if (this.state.treeData) {
if (!tree.getNodeByKey(selection)) {
selection = null;
}
if (selection === null && !this.tree.getActiveNode()) {
// This covers the case when we mount the tree and selection is not present in the tree.
// At this point, nothing is selected, so the onActive event won't trigger. So we have to
// call it manually, so that the form can update and set null instead of the invalid selection.
this.onActivate();
} else {
tree.activateKey(selection);
}
}
} }
} }
@ -270,7 +289,8 @@ class TreeTable extends Component {
// Single-select // Single-select
onActivate(event, data) { onActivate(event, data) {
const selection = this.destringifyKey(this.tree.getActiveNode().key); const activeNode = this.tree.getActiveNode();
const selection = activeNode ? this.destringifyKey(activeNode.key) : null;
if (selection !== this.props.selection) { if (selection !== this.props.selection) {
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
@ -298,7 +318,7 @@ class TreeTable extends Component {
if (updated) { if (updated) {
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
this.onSelectionChanged(selection); this.onSelectionChanged(newSel);
} }
} }

View file

@ -22,10 +22,9 @@ import {
} from '../lib/form'; } from '../lib/form';
import {withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {DeleteModalDialog} from '../lib/modals'; import {DeleteModalDialog} from '../lib/modals';
import {NamespaceSelect, validateNamespace} from '../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {FieldWizard, UnsubscriptionMode} from '../../../shared/lists'; import {FieldWizard, UnsubscriptionMode} from '../../../shared/lists';
import styles from "../lib/styles.scss"; import styles from "../lib/styles.scss";
import mailtrainConfig from 'mailtrainConfig';
import {getMailerTypes} from "../send-configurations/helpers"; import {getMailerTypes} from "../send-configurations/helpers";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
@ -49,7 +48,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
getFormValuesMutator(data) { getFormValuesMutator(data) {
@ -86,7 +86,7 @@ export default class CUD extends Component {
contact_email: '', contact_email: '',
homepage: '', homepage: '',
unsubscription_mode: UnsubscriptionMode.ONE_STEP, unsubscription_mode: UnsubscriptionMode.ONE_STEP,
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
to_name: '', to_name: '',
fieldWizard: FieldWizard.FIRST_LAST_NAME, fieldWizard: FieldWizard.FIRST_LAST_NAME,
send_configuration: null, send_configuration: null,

View file

@ -3,13 +3,13 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n'; import {withTranslation} from '../lib/i18n';
import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page'; import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table'; import {Table} from '../lib/table';
import {Icon} from "../lib/bootstrap-components"; import {Icon} from "../lib/bootstrap-components";
import {checkPermissions} from "../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import {withForm} from "../lib/form"; import {withForm} from "../lib/form";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -26,28 +26,17 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createList: {
entityTypeId: 'namespace',
requiredOperations: ['createList']
}
});
this.setState({
createPermitted: result.data.createList
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createList;
const customFormsPermitted = permissions.createCustomForm || permissions.viewCustomForm;
const columns = [ const columns = [
{ {
data: 1, data: 1,
@ -130,12 +119,14 @@ export default class List extends Component {
return ( return (
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
{this.state.createPermitted &&
<Toolbar> <Toolbar>
{ createPermitted &&
<LinkButton to="/lists/create" className="btn-primary" icon="plus" label={t('createList')}/> <LinkButton to="/lists/create" className="btn-primary" icon="plus" label={t('createList')}/>
<LinkButton to="/lists/forms" className="btn-primary" label={t('customForms-1')}/>
</Toolbar>
} }
{ customFormsPermitted &&
<LinkButton to="/lists/forms" className="btn-primary" label={t('customForms-1')}/>
}
</Toolbar>
<Title>{t('lists')}</Title> <Title>{t('lists')}</Title>

View file

@ -23,7 +23,7 @@ import {
withFormErrorHandlers withFormErrorHandlers
} from '../../lib/form'; } from '../../lib/form';
import {withErrorHandling} from '../../lib/error-handling'; import {withErrorHandling} from '../../lib/error-handling';
import {NamespaceSelect, validateNamespace} from '../../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../../lib/namespace';
import {DeleteModalDialog} from "../../lib/modals"; import {DeleteModalDialog} from "../../lib/modals";
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import {getTrustedUrl, getUrl} from "../../lib/urls"; import {getTrustedUrl, getUrl} from "../../lib/urls";
@ -280,7 +280,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
@ -337,7 +338,7 @@ export default class CUD extends Component {
fromExistingEntity: false, fromExistingEntity: false,
existingEntity: null, existingEntity: null,
selectedTemplate: 'layout', selectedTemplate: 'layout',
namespace: mailtrainConfig.user.namespace namespace: getDefaultNamespace(this.props.permissions)
}; };
this.supplyDefaults(data); this.supplyDefaults(data);

View file

@ -3,12 +3,12 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {withTranslation} from '../../lib/i18n'; import {withTranslation} from '../../lib/i18n';
import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page'; import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling'; import {withErrorHandling} from '../../lib/error-handling';
import {Table} from '../../lib/table'; import {Table} from '../../lib/table';
import {Icon} from "../../lib/bootstrap-components"; import {Icon} from "../../lib/bootstrap-components";
import {checkPermissions} from "../../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
import {withComponentMixins} from "../../lib/decorator-helpers"; import {withComponentMixins} from "../../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -24,28 +24,16 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createCustomForm: {
entityTypeId: 'namespace',
requiredOperations: ['createCustomForm']
}
});
this.setState({
createPermitted: result.data.createCustomForm
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createCustomForm;
const columns = [ const columns = [
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('description') }, { data: 2, title: t('description') },
@ -78,7 +66,7 @@ export default class List extends Component {
return ( return (
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
{this.state.createPermitted && {createPermitted &&
<Toolbar> <Toolbar>
<LinkButton to="/lists/forms/create" className="btn-primary" icon="plus" label={t('createCustomForm')}/> <LinkButton to="/lists/forms/create" className="btn-primary" icon="plus" label={t('createCustomForm')}/>
</Toolbar> </Toolbar>

View file

@ -19,13 +19,29 @@ import ImportRunsStatus from './imports/RunStatus';
import Share from '../shares/Share'; import Share from '../shares/Share';
import TriggersList from './TriggersList'; import TriggersList from './TriggersList';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"; import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) { function getMenus(t) {
return { return {
'lists': { 'lists': {
title: t('lists'), title: t('lists'),
link: '/lists', link: '/lists',
panelComponent: ListsList, checkPermissions: {
createList: {
entityTypeId: 'namespace',
requiredOperations: ['createList']
},
createCustomForm: {
entityTypeId: 'namespace',
requiredOperations: ['createCustomForm']
},
viewCustomForm: {
entityTypeId: 'customForm',
requiredOperations: ['view']
},
...namespaceCheckPermissions('createList')
},
panelRender: props => <ListsList permissions={props.permissions}/>,
children: { children: {
':listId([0-9]+)': { ':listId([0-9]+)': {
title: resolved => t('listName', {name: ellipsizeBreadcrumbLabel(resolved.list.name)}), title: resolved => t('listName', {name: ellipsizeBreadcrumbLabel(resolved.list.name)}),
@ -70,7 +86,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/lists/${params.listId}/edit`, link: params => `/lists/${params.listId}/edit`,
visible: resolved => resolved.list.permissions.includes('edit'), visible: resolved => resolved.list.permissions.includes('edit'),
panelRender: props => <ListsCUD action={props.match.params.action} entity={props.resolved.list} /> panelRender: props => <ListsCUD action={props.match.params.action} entity={props.resolved.list} permissions={props.permissions} />
}, },
fields: { fields: {
title: t('fields'), title: t('fields'),
@ -191,12 +207,15 @@ function getMenus(t) {
}, },
create: { create: {
title: t('create'), title: t('create'),
panelRender: props => <ListsCUD action="create" /> panelRender: props => <ListsCUD action="create" permissions={props.permissions} />
}, },
forms: { forms: {
title: t('customForms-1'), title: t('customForms-1'),
link: '/lists/forms', link: '/lists/forms',
panelComponent: FormsList, checkPermissions: {
...namespaceCheckPermissions('createCustomForm')
},
panelRender: props => <FormsList permissions={props.permissions}/>,
children: { children: {
':formsId([0-9]+)': { ':formsId([0-9]+)': {
title: resolved => t('customFormsName', {name: ellipsizeBreadcrumbLabel(resolved.forms.name)}), title: resolved => t('customFormsName', {name: ellipsizeBreadcrumbLabel(resolved.forms.name)}),
@ -209,7 +228,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/lists/forms/${params.formsId}/edit`, link: params => `/lists/forms/${params.formsId}/edit`,
visible: resolved => resolved.forms.permissions.includes('edit'), visible: resolved => resolved.forms.permissions.includes('edit'),
panelRender: props => <FormsCUD action={props.match.params.action} entity={props.resolved.forms} /> panelRender: props => <FormsCUD action={props.match.params.action} entity={props.resolved.forms} permissions={props.permissions} />
}, },
share: { share: {
title: t('share'), title: t('share'),
@ -221,7 +240,7 @@ function getMenus(t) {
}, },
create: { create: {
title: t('create'), title: t('create'),
panelRender: props => <FormsCUD action="create" /> panelRender: props => <FormsCUD action="create" permissions={props.permissions} />
} }
} }
} }

View file

@ -24,6 +24,7 @@ import mailtrainConfig from 'mailtrainConfig';
import {getGlobalNamespaceId} from "../../../shared/namespaces"; import {getGlobalNamespaceId} from "../../../shared/namespaces";
import {getUrl} from "../lib/urls"; import {getUrl} from "../lib/urls";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import {getDefaultNamespace} from "../lib/namespace";
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -43,7 +44,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
submitFormValuesMutator(data) { submitFormValuesMutator(data) {
@ -97,7 +99,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: '', description: '',
namespace: mailtrainConfig.user.namespace namespace: getDefaultNamespace(this.props.permissions)
}); });
} }

View file

@ -4,13 +4,13 @@ import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n'; import {withTranslation} from '../lib/i18n';
import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page'; import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {TreeTable} from '../lib/tree'; import {TreeTable} from '../lib/tree';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {Icon} from "../lib/bootstrap-components"; import {Icon} from "../lib/bootstrap-components";
import {checkPermissions} from "../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {getGlobalNamespaceId} from "../../../shared/namespaces"; import {getGlobalNamespaceId} from "../../../shared/namespaces";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -26,28 +26,16 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createNamespace: {
entityTypeId: 'namespace',
requiredOperations: ['createNamespace']
}
});
this.setState({
createPermitted: result.data.createNamespace
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createNamespace;
const actions = node => { const actions = node => {
const actions = []; const actions = [];
@ -76,7 +64,7 @@ export default class List extends Component {
return ( return (
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
{this.state.createPermitted && {createPermitted &&
<Toolbar> <Toolbar>
<LinkButton to="/namespaces/create" className="btn-primary" icon="plus" label={t('createNamespace')}/> <LinkButton to="/namespaces/create" className="btn-primary" icon="plus" label={t('createNamespace')}/>
</Toolbar> </Toolbar>

View file

@ -5,13 +5,21 @@ import CUD from './CUD';
import List from './List'; import List from './List';
import Share from '../shares/Share'; import Share from '../shares/Share';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"; import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) { function getMenus(t) {
return { return {
namespaces: { namespaces: {
title: t('namespaces'), title: t('namespaces'),
link: '/namespaces', link: '/namespaces',
panelComponent: List, checkPermissions: {
createNamespace: {
entityTypeId: 'namespace',
requiredOperations: ['createNamespace']
},
...namespaceCheckPermissions('createNamespace')
},
panelRender: props => <List permissions={props.permissions}/>,
children: { children: {
':namespaceId([0-9]+)': { ':namespaceId([0-9]+)': {
title: resolved => t('namespaceName', {name: ellipsizeBreadcrumbLabel(resolved.namespace.name)}), title: resolved => t('namespaceName', {name: ellipsizeBreadcrumbLabel(resolved.namespace.name)}),
@ -24,7 +32,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/namespaces/${params.namespaceId}/edit`, link: params => `/namespaces/${params.namespaceId}/edit`,
visible: resolved => resolved.namespace.permissions.includes('edit'), visible: resolved => resolved.namespace.permissions.includes('edit'),
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.namespace} /> panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.namespace} permissions={props.permissions} />
}, },
share: { share: {
title: t('share'), title: t('share'),
@ -36,7 +44,7 @@ function getMenus(t) {
}, },
create: { create: {
title: t('create'), title: t('create'),
panelRender: props => <CUD action="create" /> panelRender: props => <CUD action="create" permissions={props.permissions} />
}, },
} }
} }

View file

@ -21,9 +21,8 @@ import {
import axios from '../lib/axios'; import axios from '../lib/axios';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling';
import moment from 'moment'; import moment from 'moment';
import {NamespaceSelect, validateNamespace} from '../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals"; import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "../lib/urls"; import {getUrl} from "../lib/urls";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
@ -49,7 +48,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
@withAsyncErrorHandler @withAsyncErrorHandler
@ -97,7 +97,7 @@ export default class CUD extends Component {
name: '', name: '',
description: '', description: '',
report_template: null, report_template: null,
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
user_fields: null user_fields: null
}); });
} }

View file

@ -9,10 +9,10 @@ import moment from 'moment';
import axios from '../lib/axios'; import axios from '../lib/axios';
import {ReportState} from '../../../shared/reports'; import {ReportState} from '../../../shared/reports';
import {Icon} from "../lib/bootstrap-components"; import {Icon} from "../lib/bootstrap-components";
import {checkPermissions} from "../lib/permissions";
import {getUrl} from "../lib/urls"; import {getUrl} from "../lib/urls";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -28,36 +28,8 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createReport: {
entityTypeId: 'namespace',
requiredOperations: ['createReport']
},
executeReportTemplate: {
entityTypeId: 'reportTemplate',
requiredOperations: ['execute']
},
createReportTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createReportTemplate']
},
viewReportTemplate: {
entityTypeId: 'reportTemplate',
requiredOperations: ['view']
}
});
this.setState({
createPermitted: result.data.createReport && result.data.executeReportTemplate,
templatesPermitted: result.data.createReportTemplate || result.data.viewReportTemplate
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
@withAsyncErrorHandler @withAsyncErrorHandler
@ -75,6 +47,10 @@ export default class List extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createReport && permissions.executeReportTemplate;
const templatesPermitted = permissions.createReportTemplate || permissions.viewReportTemplate;
const columns = [ const columns = [
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('template') }, { data: 2, title: t('template') },
@ -176,10 +152,10 @@ export default class List extends Component {
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
<Toolbar> <Toolbar>
{this.state.createPermitted && {createPermitted &&
<LinkButton to="/reports/create" className="btn-primary" icon="plus" label={t('createReport')}/> <LinkButton to="/reports/create" className="btn-primary" icon="plus" label={t('createReport')}/>
} }
{this.state.templatesPermitted && {templatesPermitted &&
<LinkButton to="/reports/templates" className="btn-primary" label={t('reportTemplates')}/> <LinkButton to="/reports/templates" className="btn-primary" label={t('reportTemplates')}/>
} }
</Toolbar> </Toolbar>

View file

@ -10,6 +10,7 @@ import Share from '../shares/Share';
import {ReportState} from '../../../shared/reports'; import {ReportState} from '../../../shared/reports';
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"; import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) { function getMenus(t) {
@ -17,7 +18,26 @@ function getMenus(t) {
'reports': { 'reports': {
title: t('reports'), title: t('reports'),
link: '/reports', link: '/reports',
panelComponent: ReportsList, checkPermissions: {
createReport: {
entityTypeId: 'namespace',
requiredOperations: ['createReport']
},
executeReportTemplate: {
entityTypeId: 'reportTemplate',
requiredOperations: ['execute']
},
createReportTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createReportTemplate']
},
viewReportTemplate: {
entityTypeId: 'reportTemplate',
requiredOperations: ['view']
},
...namespaceCheckPermissions('createReport')
},
panelRender: props => <ReportsList permissions={props.permissions}/>,
children: { children: {
':reportId([0-9]+)': { ':reportId([0-9]+)': {
title: resolved => t('reportName', {name: ellipsizeBreadcrumbLabel(resolved.report.name)}), title: resolved => t('reportName', {name: ellipsizeBreadcrumbLabel(resolved.report.name)}),
@ -30,7 +50,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/reports/${params.reportId}/edit`, link: params => `/reports/${params.reportId}/edit`,
visible: resolved => resolved.report.permissions.includes('edit'), visible: resolved => resolved.report.permissions.includes('edit'),
panelRender: props => <ReportsCUD action={props.match.params.action} entity={props.resolved.report} /> panelRender: props => <ReportsCUD action={props.match.params.action} entity={props.resolved.report} permissions={props.permissions} />
}, },
view: { view: {
title: t('view'), title: t('view'),
@ -59,12 +79,15 @@ function getMenus(t) {
}, },
create: { create: {
title: t('create'), title: t('create'),
panelRender: props => <ReportsCUD action="create" /> panelRender: props => <ReportsCUD action="create" permissions={props.permissions} />
}, },
templates: { templates: {
title: t('templates'), title: t('templates'),
link: '/reports/templates', link: '/reports/templates',
panelComponent: ReportTemplatesList, checkPermissions: {
...namespaceCheckPermissions('createReportTemplate')
},
panelRender: props => <ReportTemplatesList permissions={props.permissions}/>,
children: { children: {
':templateId([0-9]+)': { ':templateId([0-9]+)': {
title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.template.name)}), title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.template.name)}),
@ -77,7 +100,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/reports/templates/${params.templateId}/edit`, link: params => `/reports/templates/${params.templateId}/edit`,
visible: resolved => mailtrainConfig.globalPermissions.createJavascriptWithROAccess && resolved.template.permissions.includes('edit'), visible: resolved => mailtrainConfig.globalPermissions.createJavascriptWithROAccess && resolved.template.permissions.includes('edit'),
panelRender: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} /> panelRender: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} permissions={props.permissions} />
}, },
share: { share: {
title: t('share'), title: t('share'),
@ -90,7 +113,7 @@ function getMenus(t) {
create: { create: {
title: t('create'), title: t('create'),
extraParams: [':wizard?'], extraParams: [':wizard?'],
panelRender: props => <ReportTemplatesCUD action="create" wizard={props.match.params.wizard} /> panelRender: props => <ReportTemplatesCUD action="create" wizard={props.match.params.wizard} permissions={props.permissions} />
} }
} }
} }

View file

@ -19,9 +19,8 @@ import {
withFormErrorHandlers withFormErrorHandlers
} from '../../lib/form'; } from '../../lib/form';
import {withErrorHandling} from '../../lib/error-handling'; import {withErrorHandling} from '../../lib/error-handling';
import {NamespaceSelect, validateNamespace} from '../../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../../lib/namespace';
import {DeleteModalDialog} from "../../lib/modals"; import {DeleteModalDialog} from "../../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import 'brace/mode/javascript'; import 'brace/mode/javascript';
import 'brace/mode/json'; import 'brace/mode/json';
import 'brace/mode/handlebars'; import 'brace/mode/handlebars';
@ -46,7 +45,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
wizard: PropTypes.string, wizard: PropTypes.string,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
submitFormValuesMutator(data) { submitFormValuesMutator(data) {
@ -64,7 +64,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: 'Generates a campaign report listing all subscribers along with open counts.', description: 'Generates a campaign report listing all subscribers along with open counts.',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
mime_type: 'text/html', mime_type: 'text/html',
user_fields: user_fields:
'[\n' + '[\n' +
@ -114,7 +114,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: 'Generates a campaign report as CSV that lists all subscribers along with open counts.', description: 'Generates a campaign report as CSV that lists all subscribers along with open counts.',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
mime_type: 'text/csv', mime_type: 'text/csv',
user_fields: user_fields:
'[\n' + '[\n' +
@ -144,7 +144,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: 'Generates a campaign report with results are aggregated by "Country" custom field. (Note that this custom field has to be presents in the subscription custom fields.)', description: 'Generates a campaign report with results are aggregated by "Country" custom field. (Note that this custom field has to be presents in the subscription custom fields.)',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
mime_type: 'text/html', mime_type: 'text/html',
user_fields: user_fields:
'[\n' + '[\n' +
@ -215,7 +215,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: '', description: '',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
mime_type: 'text/html', mime_type: 'text/html',
user_fields: '', user_fields: '',
js: '', js: '',

View file

@ -4,13 +4,13 @@ import React, {Component} from 'react';
import {withTranslation} from '../../lib/i18n'; import {withTranslation} from '../../lib/i18n';
import {ButtonDropdown, Icon} from '../../lib/bootstrap-components'; import {ButtonDropdown, Icon} from '../../lib/bootstrap-components';
import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page'; import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling'; import {withErrorHandling} from '../../lib/error-handling';
import {Table} from '../../lib/table'; import {Table} from '../../lib/table';
import moment from 'moment'; import moment from 'moment';
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import {checkPermissions} from "../../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
import {withComponentMixins} from "../../lib/decorator-helpers"; import {withComponentMixins} from "../../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -26,28 +26,16 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createReportTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createReportTemplate']
}
});
this.setState({
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.createJavascriptWithROAccess
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createReportTemplate && mailtrainConfig.globalPermissions.createJavascriptWithROAccess;
const columns = [ const columns = [
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('description') }, { data: 2, title: t('description') },
@ -82,7 +70,7 @@ export default class List extends Component {
return ( return (
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
{this.state.createPermitted && {createPermitted &&
<Toolbar> <Toolbar>
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createReportTemplate')}> <ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createReportTemplate')}>
<DropdownLink to="/reports/templates/create">{t('blank')}</DropdownLink> <DropdownLink to="/reports/templates/create">{t('blank')}</DropdownLink>

View file

@ -20,7 +20,7 @@ import {
withFormErrorHandlers withFormErrorHandlers
} from '../lib/form'; } from '../lib/form';
import {withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {NamespaceSelect, validateNamespace} from '../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals"; import {DeleteModalDialog} from "../lib/modals";
import {getMailerTypes} from "./helpers"; import {getMailerTypes} from "./helpers";
@ -60,7 +60,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
wizard: PropTypes.string, wizard: PropTypes.string,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
onMailerTypeChanged(mutStateDate, key, oldType, type) { onMailerTypeChanged(mutStateDate, key, oldType, type) {
@ -96,7 +97,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: '', description: '',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
from_email: '', from_email: '',
from_email_overridable: false, from_email_overridable: false,
from_name: '', from_name: '',

View file

@ -4,13 +4,13 @@ import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n'; import {withTranslation} from '../lib/i18n';
import {Icon} from '../lib/bootstrap-components'; import {Icon} from '../lib/bootstrap-components';
import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page'; import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table'; import {Table} from '../lib/table';
import moment from 'moment'; import moment from 'moment';
import {getMailerTypes} from './helpers'; import {getMailerTypes} from './helpers';
import {checkPermissions} from "../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
@ -29,28 +29,16 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createSendConfiguration: {
entityTypeId: 'namespace',
requiredOperations: ['createSendConfiguration']
}
});
this.setState({
createPermitted: result.data.createSendConfiguration
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createSendConfiguration;
const columns = [ const columns = [
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> }, { data: 2, title: t('id'), render: data => <code>{data}</code> },
@ -87,7 +75,7 @@ export default class List extends Component {
return ( return (
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
{this.state.createPermitted && {createPermitted &&
<Toolbar> <Toolbar>
<LinkButton to="/send-configurations/create" className="btn-primary" icon="plus" label={t('createSendConfiguration')}/> <LinkButton to="/send-configurations/create" className="btn-primary" icon="plus" label={t('createSendConfiguration')}/>
</Toolbar> </Toolbar>

View file

@ -6,6 +6,7 @@ import CUD from './CUD';
import List from './List'; import List from './List';
import Share from '../shares/Share'; import Share from '../shares/Share';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"; import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) { function getMenus(t) {
@ -13,7 +14,14 @@ function getMenus(t) {
'send-configurations': { 'send-configurations': {
title: t('sendConfigurations-1'), title: t('sendConfigurations-1'),
link: '/send-configurations', link: '/send-configurations',
panelComponent: List, checkPermissions: {
createSendConfiguration: {
entityTypeId: 'namespace',
requiredOperations: ['createSendConfiguration']
},
...namespaceCheckPermissions('createSendConfiguration')
},
panelRender: props => <List permissions={props.permissions}/>,
children: { children: {
':sendConfigurationId([0-9]+)': { ':sendConfigurationId([0-9]+)': {
title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.sendConfiguration.name)}), title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.sendConfiguration.name)}),
@ -26,7 +34,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/send-configurations/${params.sendConfigurationId}/edit`, link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
visible: resolved => resolved.sendConfiguration.permissions.includes('edit'), visible: resolved => resolved.sendConfiguration.permissions.includes('edit'),
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.sendConfiguration} /> panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.sendConfiguration} permissions={props.permissions} />
}, },
share: { share: {
title: t('share'), title: t('share'),
@ -38,7 +46,7 @@ function getMenus(t) {
}, },
create: { create: {
title: t('create'), title: t('create'),
panelRender: props => <CUD action="create" /> panelRender: props => <CUD action="create" permissions={props.permissions} />
} }
} }
} }

View file

@ -139,7 +139,6 @@ export default class Share extends Component {
let usersLabelIndex = 1; let usersLabelIndex = 1;
const usersColumns = [ const usersColumns = [
{ data: 0, title: "#" },
{ data: 1, title: "Username" }, { data: 1, title: "Username" },
]; ];

View file

@ -20,7 +20,7 @@ import {
withFormErrorHandlers withFormErrorHandlers
} from '../lib/form'; } from '../lib/form';
import {withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {NamespaceSelect, validateNamespace} from '../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {ContentModalDialog, DeleteModalDialog} from "../lib/modals"; import {ContentModalDialog, DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import {getEditForm, getTagLanguages, getTemplateTypes, getTypeForm} from './helpers'; import {getEditForm, getTagLanguages, getTemplateTypes, getTypeForm} from './helpers';
@ -75,6 +75,7 @@ export default class CUD extends Component {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
wizard: PropTypes.string, wizard: PropTypes.string,
entity: PropTypes.object, entity: PropTypes.object,
permissions: PropTypes.object,
setPanelInFullScreen: PropTypes.func setPanelInFullScreen: PropTypes.func
} }
@ -124,7 +125,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: '', description: '',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
type: mailtrainConfig.editors[0], type: mailtrainConfig.editors[0],
tag_language: mailtrainConfig.tagLanguages[0], tag_language: mailtrainConfig.tagLanguages[0],

View file

@ -4,13 +4,13 @@ import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n'; import {withTranslation} from '../lib/i18n';
import {Icon} from '../lib/bootstrap-components'; import {Icon} from '../lib/bootstrap-components';
import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page'; import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table'; import {Table} from '../lib/table';
import moment from 'moment'; import moment from 'moment';
import {getTemplateTypes, getTagLanguages} from './helpers'; import {getTagLanguages, getTemplateTypes} from './helpers';
import {checkPermissions} from "../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
@ -30,37 +30,17 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createTemplate']
},
createMosaicoTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createMosaicoTemplate']
},
viewMosaicoTemplate: {
entityTypeId: 'mosaicoTemplate',
requiredOperations: ['view']
}
});
this.setState({
createPermitted: result.data.createTemplate,
mosaicoTemplatesPermitted: result.data.createMosaicoTemplate || result.data.viewMosaicoTemplate
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createTemplate;
const mosaicoTemplatesPermitted = permissions.createMosaicoTemplate || permissions.viewMosaicoTemplate;
const columns = [ const columns = [
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('description') }, { data: 2, title: t('description') },
@ -105,10 +85,10 @@ export default class List extends Component {
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
<Toolbar> <Toolbar>
{this.state.createPermitted && {createPermitted &&
<LinkButton to="/templates/create" className="btn-primary" icon="plus" label={t('createTemplate')}/> <LinkButton to="/templates/create" className="btn-primary" icon="plus" label={t('createTemplate')}/>
} }
{this.state.mosaicoTemplatesPermitted && {mosaicoTemplatesPermitted &&
<LinkButton to="/templates/mosaico" className="btn-primary" label={t('mosaicoTemplates')}/> <LinkButton to="/templates/mosaico" className="btn-primary" label={t('mosaicoTemplates')}/>
} }
</Toolbar> </Toolbar>

View file

@ -18,7 +18,7 @@ import {
withFormErrorHandlers withFormErrorHandlers
} from '../../lib/form'; } from '../../lib/form';
import {withErrorHandling} from '../../lib/error-handling'; import {withErrorHandling} from '../../lib/error-handling';
import {NamespaceSelect, validateNamespace} from '../../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../../lib/namespace';
import {DeleteModalDialog} from "../../lib/modals"; import {DeleteModalDialog} from "../../lib/modals";
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import {getMJMLSample, getVersafix} from "../../../../shared/mosaico-templates"; import {getMJMLSample, getVersafix} from "../../../../shared/mosaico-templates";
@ -57,7 +57,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
wizard: PropTypes.string, wizard: PropTypes.string,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
getFormValuesMutator(data) { getFormValuesMutator(data) {
@ -88,7 +89,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: '', description: '',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
type: 'html', type: 'html',
tag_language: mailtrainConfig.tagLanguages[0] tag_language: mailtrainConfig.tagLanguages[0]
}); });
@ -97,7 +98,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: '', description: '',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
type: 'mjml', type: 'mjml',
tag_language: mailtrainConfig.tagLanguages[0] tag_language: mailtrainConfig.tagLanguages[0]
}); });
@ -106,7 +107,7 @@ export default class CUD extends Component {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
description: '', description: '',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
type: 'html', type: 'html',
tag_language: mailtrainConfig.tagLanguages[0], tag_language: mailtrainConfig.tagLanguages[0],
html: '' html: ''

View file

@ -4,14 +4,14 @@ import React, {Component} from 'react';
import {withTranslation} from '../../lib/i18n'; import {withTranslation} from '../../lib/i18n';
import {ButtonDropdown, Icon} from '../../lib/bootstrap-components'; import {ButtonDropdown, Icon} from '../../lib/bootstrap-components';
import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page'; import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling'; import {withErrorHandling} from '../../lib/error-handling';
import {Table} from '../../lib/table'; import {Table} from '../../lib/table';
import moment from 'moment'; import moment from 'moment';
import {getTemplateTypes} from './helpers'; import {getTemplateTypes} from './helpers';
import {getTagLanguages} from '../helpers'; import {getTagLanguages} from '../helpers';
import {checkPermissions} from "../../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
import {withComponentMixins} from "../../lib/decorator-helpers"; import {withComponentMixins} from "../../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([ @withComponentMixins([
@ -31,28 +31,16 @@ export default class List extends Component {
tableRestActionDialogInit(this); tableRestActionDialogInit(this);
} }
@withAsyncErrorHandler static propTypes = {
async fetchPermissions() { permissions: PropTypes.object
const result = await checkPermissions({
createMosaicoTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createMosaicoTemplate']
}
});
this.setState({
createPermitted: result.data.createMosaicoTemplate
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createMosaicoTemplate;
const columns = [ const columns = [
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('description') }, { data: 2, title: t('description') },
@ -103,7 +91,7 @@ export default class List extends Component {
return ( return (
<div> <div>
{tableRestActionDialogRender(this)} {tableRestActionDialogRender(this)}
{this.state.createPermitted && {createPermitted &&
<Toolbar> <Toolbar>
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createMosaicoTemplate')}> <ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createMosaicoTemplate')}>
<DropdownLink to="/templates/mosaico/create">{t('blank')}</DropdownLink> <DropdownLink to="/templates/mosaico/create">{t('blank')}</DropdownLink>

View file

@ -9,14 +9,29 @@ import Files from "../lib/files";
import MosaicoCUD from './mosaico/CUD'; import MosaicoCUD from './mosaico/CUD';
import MosaicoList from './mosaico/List'; import MosaicoList from './mosaico/List';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"; import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) { function getMenus(t) {
return { return {
'templates': { 'templates': {
title: t('templates'), title: t('templates'),
link: '/templates', link: '/templates',
panelComponent: TemplatesList, checkPermissions: {
createTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createTemplate']
},
createMosaicoTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createMosaicoTemplate']
},
viewMosaicoTemplate: {
entityTypeId: 'mosaicoTemplate',
requiredOperations: ['view']
},
...namespaceCheckPermissions('createTemplate')
},
panelRender: props => <TemplatesList permissions={props.permissions}/>,
children: { children: {
':templateId([0-9]+)': { ':templateId([0-9]+)': {
title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.template.name)}), title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.template.name)}),
@ -29,7 +44,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/templates/${params.templateId}/edit`, link: params => `/templates/${params.templateId}/edit`,
visible: resolved => resolved.template.permissions.includes('edit'), visible: resolved => resolved.template.permissions.includes('edit'),
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} setPanelInFullScreen={props.setPanelInFullScreen} /> panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} permissions={props.permissions} setPanelInFullScreen={props.setPanelInFullScreen} />
}, },
files: { files: {
title: t('files'), title: t('files'),
@ -47,12 +62,15 @@ function getMenus(t) {
}, },
create: { create: {
title: t('create'), title: t('create'),
panelRender: props => <TemplatesCUD action="create" /> panelRender: props => <TemplatesCUD action="create" permissions={props.permissions} />
}, },
mosaico: { mosaico: {
title: t('mosaicoTemplates'), title: t('mosaicoTemplates'),
link: '/templates/mosaico', link: '/templates/mosaico',
panelComponent: MosaicoList, checkPermissions: {
...namespaceCheckPermissions('createMosaicoTemplate')
},
panelRender: props => <MosaicoList permissions={props.permissions}/>,
children: { children: {
':mosaiceTemplateId([0-9]+)': { ':mosaiceTemplateId([0-9]+)': {
title: resolved => t('mosaicoTemplateName', {name: ellipsizeBreadcrumbLabel(resolved.mosaicoTemplate.name)}), title: resolved => t('mosaicoTemplateName', {name: ellipsizeBreadcrumbLabel(resolved.mosaicoTemplate.name)}),
@ -65,7 +83,7 @@ function getMenus(t) {
title: t('edit'), title: t('edit'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`, link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'), visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'),
panelRender: props => <MosaicoCUD action={props.match.params.action} entity={props.resolved.mosaicoTemplate} /> panelRender: props => <MosaicoCUD action={props.match.params.action} entity={props.resolved.mosaicoTemplate} permissions={props.permissions}/>
}, },
files: { files: {
title: t('files'), title: t('files'),
@ -90,7 +108,7 @@ function getMenus(t) {
create: { create: {
title: t('create'), title: t('create'),
extraParams: [':wizard?'], extraParams: [':wizard?'],
panelRender: props => <MosaicoCUD action="create" wizard={props.match.params.wizard} /> panelRender: props => <MosaicoCUD action="create" wizard={props.match.params.wizard} permissions={props.permissions} />
} }
} }
} }

View file

@ -19,7 +19,7 @@ import {withErrorHandling} from '../lib/error-handling';
import interoperableErrors from '../../../shared/interoperable-errors'; import interoperableErrors from '../../../shared/interoperable-errors';
import passwordValidator from '../../../shared/password-validator'; import passwordValidator from '../../../shared/password-validator';
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import {NamespaceSelect, validateNamespace} from '../lib/namespace'; import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals"; import {DeleteModalDialog} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
@ -49,7 +49,8 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
entity: PropTypes.object entity: PropTypes.object,
permissions: PropTypes.object
} }
getFormValuesMutator(data) { getFormValuesMutator(data) {
@ -71,7 +72,7 @@ export default class CUD extends Component {
email: '', email: '',
password: '', password: '',
password2: '', password2: '',
namespace: mailtrainConfig.user.namespace, namespace: getDefaultNamespace(this.props.permissions),
role: null role: null
}); });
} }

View file

@ -5,12 +5,17 @@ import CUD from './CUD';
import List from './List'; import List from './List';
import UserShares from '../shares/UserShares'; import UserShares from '../shares/UserShares';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"; import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
import MosaicoCUD from "../templates/mosaico/CUD";
function getMenus(t) { function getMenus(t) {
return { return {
'users': { 'users': {
title: t('users'), title: t('users'),
link: '/users', link: '/users',
checkPermissions: {
...namespaceCheckPermissions('manageUsers')
},
panelComponent: List, panelComponent: List,
children: { children: {
':userId([0-9]+)': { ':userId([0-9]+)': {
@ -23,7 +28,7 @@ function getMenus(t) {
':action(edit|delete)': { ':action(edit|delete)': {
title: t('edit'), title: t('edit'),
link: params => `/users/${params.userId}/edit`, link: params => `/users/${params.userId}/edit`,
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.user} /> panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.user} permissions={props.permissions} />
}, },
shares: { shares: {
title: t('shares'), title: t('shares'),
@ -34,7 +39,7 @@ function getMenus(t) {
}, },
create: { create: {
title: t('create'), title: t('create'),
panelRender: props => <CUD action="create" /> panelRender: props => <CUD action="create" permissions={props.permissions} />
}, },
} }
} }

View file

@ -257,6 +257,10 @@ seleniumWebDriver:
browser: phantomjs browser: phantomjs
# The section below defines the definition of roles (permissions) to be used when no "roles" section is provided
# in custom config (typically production.yaml). If you want to extend rules provided below, add corresponding rules
# in "defaultRoles" section in custom config. If you want to define roles from scratch, create "roles" section in
# the custom config.
defaultRoles: defaultRoles:
global: global:
master: master:
@ -308,12 +312,14 @@ defaultRoles:
campaignsCreator: campaignsCreator:
name: Campaigns Creator name: Campaigns Creator
description: In the respective namespace, the user has all permissions to create and manage templates and campaigns. description: In the respective namespace, the user has all permissions to create and manage templates and campaigns. The user can also read public data about send configurations and use Mosaico templates in the namespace.
permissions: [view, createTemplate, createCampaign] permissions: [view, createTemplate, createCampaign]
children: children:
sendConfiguration: [viewPublic] sendConfiguration: [viewPublic]
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, fetchRss] campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles] template: [view, edit, delete, share, viewFiles, manageFiles]
mosaicoTemplate: [view, viewFiles]
namespace: [view, createTemplate, createCampaign]
sendConfiguration: sendConfiguration:
master: master:
@ -378,5 +384,9 @@ defaultRoles:
name: Master name: Master
description: All permissions description: All permissions
permissions: [view, edit, delete, share, viewFiles, manageFiles] permissions: [view, edit, delete, share, viewFiles, manageFiles]
campaignsCreator:
name: Campaigns Creator
description: The user can use the Mosaico template, but cannot edit it or delete it.
permissions: [view, viewFiles]

View file

@ -73,7 +73,6 @@ async function create(context, entity) {
async function updateWithConsistencyCheck(context, entity) { async function updateWithConsistencyCheck(context, entity) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await shares.enforceGlobalPermission(context, 'createJavascriptWithROAccess');
await shares.enforceEntityPermissionTx(tx, context, 'mosaicoTemplate', entity.id, 'edit'); await shares.enforceEntityPermissionTx(tx, context, 'mosaicoTemplate', entity.id, 'edit');
const existing = await tx('mosaico_templates').where('id', entity.id).first(); const existing = await tx('mosaico_templates').where('id', entity.id).first();

View file

@ -35,12 +35,13 @@ router.putAsync('/shares', passport.loggedIn, async (req, res) => {
Accepts format: Accepts format:
{ {
XXX1: { XXX1: {
entityTypeId: ... entityTypeId: ...,
requiredOperations: [ ... ] requiredOperations: [ ... ]
}, },
XXX2: { XXX2: {
entityTypeId: ... entityTypeId: ...,
entityId: ...,
requiredOperations: [ ... ] requiredOperations: [ ... ]
} }
} }