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
} from '../lib/form';
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 mailtrainConfig from 'mailtrainConfig';
import {getTagLanguages, getTemplateTypes, getTypeForm, ResourceType} from '../templates/helpers';
@ -109,6 +109,7 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
entity: PropTypes.object,
permissions: PropTypes.object,
type: PropTypes.number
}
@ -176,7 +177,12 @@ export default class CUD extends Component {
}
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 = [];
@ -297,7 +303,7 @@ export default class CUD extends Component {
lists: [lstUid],
send_configuration: null,
namespace: mailtrainConfig.user.namespace,
namespace: getDefaultNamespace(this.props.permissions),
subject: '',

View file

@ -4,15 +4,15 @@ import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n';
import {ButtonDropdown, Icon} from '../lib/bootstrap-components';
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 moment from 'moment';
import {CampaignSource, CampaignStatus, CampaignType} from "../../../shared/campaigns";
import {checkPermissions} from "../lib/permissions";
import {getCampaignLabels} from "./helpers";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers";
import styles from "./styles.scss";
import PropTypes from 'prop-types';
@withComponentMixins([
withTranslation,
@ -34,28 +34,16 @@ export default class List extends Component {
tableRestActionDialogInit(this);
}
@withAsyncErrorHandler
async fetchPermissions() {
const result = await checkPermissions({
createCampaign: {
entityTypeId: 'namespace',
requiredOperations: ['createCampaign']
}
});
this.setState({
createPermitted: result.data.createCampaign
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
static propTypes = {
permissions: PropTypes.object
}
render() {
const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createCampaign;
const columns = [
{ data: 1, title: t('name') },
{ 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>
{tableRestActionDialogRender(this)}
<Toolbar>
{this.state.createPermitted &&
{createPermitted &&
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createCampaign')}>
<DropdownLink to="/campaigns/create-regular">{t('regular')}</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 StatisticsLinkClicks from "./StatisticsLinkClicks";
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) {
const aggLabels = {
@ -28,7 +29,14 @@ function getMenus(t) {
'campaigns': {
title: t('campaigns'),
link: '/campaigns',
panelComponent: CampaignsList,
checkPermissions: {
createCampaign: {
entityTypeId: 'namespace',
requiredOperations: ['createCampaign']
},
...namespaceCheckPermissions('createCampaign')
},
panelRender: props => <CampaignsList permissions={props.permissions}/>,
children: {
':campaignId([0-9]+)': {
title: resolved => t('campaignName', {name: ellipsizeBreadcrumbLabel(resolved.campaign.name)}),
@ -94,7 +102,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/campaigns/${params.campaignId}/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: {
title: t('content'),
@ -153,15 +161,15 @@ function getMenus(t) {
},
'create-regular': {
title: t('createRegularCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.REGULAR} />
panelRender: props => <CampaignsCUD action="create" type={CampaignType.REGULAR} permissions={props.permissions} />
},
'create-rss': {
title: t('createRssCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.RSS} />
panelRender: props => <CampaignsCUD action="create" type={CampaignType.RSS} permissions={props.permissions} />
},
'create-triggered': {
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,
data: PropTypes.array,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
format: PropTypes.string
format: PropTypes.string,
}
async onSelectionChangedAsync(sel) {
@ -835,7 +835,7 @@ class TreeTableSelect extends Component {
const className = owner.addFormValidationClass('' , id);
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
<TreeTable className={className} data={props.data} dataUrl={props.dataUrl} selectMode={TreeSelectMode.SINGLE} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
<TreeTable className={className} data={props.data} dataUrl={props.dataUrl} selectMode={TreeSelectMode.SINGLE} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync} />
);
}
}

View file

@ -89,7 +89,7 @@ export class RestActionModalDialog extends Component {
const t = this.props.t;
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('yes'), className: 'btn-danger', onClickAsync: ::this.performAction }
]}>
@ -115,16 +115,23 @@ const entityTypeLabels = {
function _getDependencyErrorMessage(err, t, name) {
return (
<div>
<p>{t('cannoteDeleteNameDueToTheFollowing', {name})}</p>
<ul className={styles.errorsList}>
{err.data.dependencies.map(dep =>
dep.link ?
<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
<li key={dep.id}>{entityTypeLabels[dep.entityTypeId](t)}: [{dep.id}]</li>
)}
{err.data.andMore && <li>{t('andMore')}</li>}
</ul>
{err.data.dependencies.length > 0 ?
<>
<p>{t('cannoteDeleteNameDueToTheFollowing', {name})}</p>
<ul className={styles.errorsList}>
{err.data.dependencies.map(dep =>
dep.link ?
<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
<li key={dep.id}>{entityTypeLabels[dep.entityTypeId](t)}: [{dep.id}]</li>
)}
{err.data.andMore && <li>{t('andMore')}</li>}
</ul>
</>
:
<p>{t('Cannot delete {{name}} due to hidden dependencies', {name})}</p>
}
</div>
);
}
@ -144,10 +151,11 @@ export class DeleteModalDialog extends Component {
visible: PropTypes.bool.isRequired,
stateOwner: PropTypes.object.isRequired,
deleteUrl: PropTypes.string.isRequired,
backUrl: PropTypes.string,
successUrl: PropTypes.string,
backUrl: PropTypes.string.isRequired,
successUrl: PropTypes.string.isRequired,
deletingMsg: PropTypes.string.isRequired,
deletedMsg: PropTypes.string.isRequired
deletedMsg: PropTypes.string.isRequired,
name: PropTypes.string
}
async onErrorAsync(err) {
@ -172,7 +180,7 @@ export class DeleteModalDialog extends Component {
render() {
const t = this.props.t;
const owner = this.props.stateOwner;
const name = owner.getFormValue('name') || '';
const name = this.props.name || owner.getFormValue('name') || '';
return <RestActionModalDialog
title={t('confirmDeletion')}

View file

@ -9,7 +9,7 @@ import {withComponentMixins} from "./decorator-helpers";
@withComponentMixins([
withTranslation
])
class NamespaceSelect extends Component {
export class NamespaceSelect extends Component {
render() {
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'])) {
state.setIn(['namespace', 'error'], t('namespaceMustBeSelected'));
} else {
@ -27,7 +27,21 @@ function validateNamespace(t, state) {
}
}
export {
NamespaceSelect,
validateNamespace
};
export function getDefaultNamespace(permissions) {
return permissions.viewUsersNamespace && permissions.createEntityInUsersNamespace ? mailtrainConfig.user.namespace : null;
}
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,94 +9,142 @@ import {getUrl} from "./urls";
import {createComponentMixin, withComponentMixins} from "./decorator-helpers";
import {withTranslation} from "./i18n";
import shallowEqual from "shallowequal";
import {checkPermissions} from "./permissions";
async function resolve(route, match, prevResolvedByUrl) {
async function resolve(route, match, prevResolverState) {
const resolved = {};
const resolvedByUrl = {};
const keysToGo = new Set(Object.keys(route.resolve));
const permissions = {};
const resolverState = {
resolvedByUrl: {},
permissionsBySig: {}
};
prevResolvedByUrl = prevResolvedByUrl || {};
prevResolverState = prevResolverState || {
resolvedByUrl: {},
permissionsBySig: {}
};
while (keysToGo.size > 0) {
const urlsToResolve = [];
const keysToResolve = [];
async function processResolve() {
const keysToGo = new Set(Object.keys(route.resolve));
for (const key of keysToGo) {
const resolveEntry = route.resolve[key];
while (keysToGo.size > 0) {
const urlsToResolve = [];
const keysToResolve = [];
let allDepsSatisfied = true;
let urlFn = null;
for (const key of keysToGo) {
const resolveEntry = route.resolve[key];
if (typeof resolveEntry === 'function') {
urlFn = resolveEntry;
let allDepsSatisfied = true;
let urlFn = null;
} else {
if (resolveEntry.dependencies) {
for (const dep of resolveEntry.dependencies) {
if (!(dep in resolved)) {
allDepsSatisfied = false;
break;
if (typeof resolveEntry === 'function') {
urlFn = resolveEntry;
} else {
if (resolveEntry.dependencies) {
for (const dep of resolveEntry.dependencies) {
if (!(dep in resolved)) {
allDepsSatisfied = false;
break;
}
}
}
urlFn = resolveEntry.url;
}
urlFn = resolveEntry.url;
if (allDepsSatisfied) {
urlsToResolve.push(urlFn(match.params, resolved));
keysToResolve.push(key);
}
}
if (allDepsSatisfied) {
urlsToResolve.push(urlFn(match.params, resolved));
keysToResolve.push(key);
if (keysToResolve.length === 0) {
throw new Error('Cyclic dependency in "resolved" entries of ' + route.path);
}
}
if (keysToResolve.length === 0) {
throw new Error('Cyclic dependency in "resolved" entries of ' + route.path);
}
const urlsToResolveByRest = [];
const keysToResolveByRest = [];
const urlsToResolveByRest = [];
const keysToResolveByRest = [];
for (let idx = 0; idx < keysToResolve.length; idx++) {
const key = keysToResolve[idx];
const url = urlsToResolve[idx];
for (let idx = 0; idx < keysToResolve.length; idx++) {
const key = keysToResolve[idx];
const url = urlsToResolve[idx];
if (url in prevResolverState.resolvedByUrl) {
const entity = prevResolverState.resolvedByUrl[url];
resolved[key] = entity;
resolverState.resolvedByUrl[url] = entity;
if (url in prevResolvedByUrl) {
const entity = prevResolvedByUrl[url];
resolved[key] = entity;
resolvedByUrl[url] = entity;
} else {
urlsToResolveByRest.push(url);
keysToResolveByRest.push(key);
}
}
if (keysToResolveByRest.length > 0) {
const promises = urlsToResolveByRest.map(url => {
if (url) {
return axios.get(getUrl(url));
} else {
return Promise.resolve({data: null});
urlsToResolveByRest.push(url);
keysToResolveByRest.push(key);
}
});
const resolvedArr = await Promise.all(promises);
for (let idx = 0; idx < keysToResolveByRest.length; idx++) {
resolved[keysToResolveByRest[idx]] = resolvedArr[idx].data;
resolvedByUrl[urlsToResolveByRest[idx]] = resolvedArr[idx].data;
}
}
for (const key of keysToResolve) {
keysToGo.delete(key);
if (keysToResolveByRest.length > 0) {
const promises = urlsToResolveByRest.map(url => {
if (url) {
return axios.get(getUrl(url));
} else {
return Promise.resolve({data: null});
}
});
const resolvedArr = await Promise.all(promises);
for (let idx = 0; idx < keysToResolveByRest.length; idx++) {
resolved[keysToResolveByRest[idx]] = resolvedArr[idx].data;
resolverState.resolvedByUrl[urlsToResolveByRest[idx]] = resolvedArr[idx].data;
}
}
for (const key of keysToResolve) {
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) {
function _getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
function _getRoutes(urlPrefix, resolve, checkPermissions, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
let routes = [];
for (let routeKey in structure) {
const entry = structure[routeKey];
@ -115,6 +163,13 @@ export function getRoutes(structure, parentRoute) {
entryResolve = resolve;
}
let entryCheckPermissions;
if (entry.checkPermissions) {
entryCheckPermissions = Object.assign({}, checkPermissions, entry.checkPermissions);
} else {
entryCheckPermissions = checkPermissions;
}
let navKeys;
const entryNavs = [];
if (entry.navs) {
@ -145,6 +200,7 @@ export function getRoutes(structure, parentRoute) {
panelInFullScreen: entry.panelInFullScreen,
insideIframe: entry.insideIframe,
resolve: entryResolve,
checkPermissions: entryCheckPermissions,
parents,
navs: [...navs, ...entryNavs],
@ -167,12 +223,12 @@ export function getRoutes(structure, parentRoute) {
const childNavs = [...entryNavs];
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) {
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 || {}) }
};
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 {
return _getRoutes('', {}, [], { "": structure }, [], null, null);
return _getRoutes('', {}, {}, [], { "": structure }, [], null, null);
}
}
@ -209,11 +265,13 @@ export class Resolver extends Component {
this.state = {
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.permissions = {};
}
}
@ -228,28 +286,31 @@ export class Resolver extends Component {
async resolve(prevMatch) {
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({
resolved: {},
resolvedByUrl: {}
permissions: {},
resolverState: null
});
} else {
const prevResolvedByUrl = this.state.resolvedByUrl;
const prevResolverState = this.state.resolverState;
if (this.state.resolved) {
if (this.state.resolverState) {
this.setState({
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.
this.setState({
resolved,
resolvedByUrl
permissions,
resolverState
});
}
}
@ -272,7 +333,7 @@ export class Resolver extends Component {
}
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 params = this.props.match.params;
const render = resolved => {
if (resolved) {
const subStructure = route.structure(resolved, params);
const render = (resolved, permissions) => {
if (resolved && permissions) {
const subStructure = route.structure(resolved, permissions, params);
const routes = getRoutes(subStructure, route);
const _renderRoute = route => {

View file

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

View file

@ -3,10 +3,6 @@
import {getUrl} from "./urls";
import axios from "./axios";
async function checkPermissions(request) {
export async function checkPermissions(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.state = {
treeData: []
treeData: null
};
if (props.data) {
@ -48,7 +48,7 @@ class TreeTable extends Component {
}
static defaultProps = {
selectMode: TreeSelectMode.NONE
selectMode: TreeSelectMode.NONE
}
refresh() {
@ -99,16 +99,19 @@ class TreeTable extends Component {
// XSS protection
sanitizeTreeData(unsafeData) {
const data = [];
for (const unsafeEntry of unsafeData) {
const entry = Object.assign({}, unsafeEntry);
entry.unsanitizedTitle = entry.title;
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>);
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>);
if (entry.children) {
entry.children = this.sanitizeTreeData(entry.children);
if (unsafeData) {
for (const unsafeEntry of unsafeData) {
const entry = Object.assign({}, unsafeEntry);
entry.unsanitizedTitle = entry.title;
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>);
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>);
if (entry.children) {
entry.children = this.sanitizeTreeData(entry.children);
}
data.push(entry);
}
data.push(entry);
}
return data;
}
@ -193,6 +196,7 @@ class TreeTable extends Component {
createNode: createNodeFn,
checkbox: this.selectMode === TreeSelectMode.MULTI,
activate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null),
deactivate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null),
select: (this.selectMode === TreeSelectMode.MULTI ? ::this.onSelect : null),
};
@ -241,7 +245,22 @@ class TreeTable extends Component {
tree.enableUpdate(true);
} 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
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) {
// noinspection JSIgnoredPromiseFromCall
@ -298,7 +318,7 @@ class TreeTable extends Component {
if (updated) {
// noinspection JSIgnoredPromiseFromCall
this.onSelectionChanged(selection);
this.onSelectionChanged(newSel);
}
}

View file

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

View file

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

View file

@ -23,7 +23,7 @@ import {
withFormErrorHandlers
} from '../../lib/form';
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 mailtrainConfig from 'mailtrainConfig';
import {getTrustedUrl, getUrl} from "../../lib/urls";
@ -280,7 +280,8 @@ export default class CUD extends Component {
static propTypes = {
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,
existingEntity: null,
selectedTemplate: 'layout',
namespace: mailtrainConfig.user.namespace
namespace: getDefaultNamespace(this.props.permissions)
};
this.supplyDefaults(data);

View file

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

View file

@ -19,13 +19,29 @@ import ImportRunsStatus from './imports/RunStatus';
import Share from '../shares/Share';
import TriggersList from './TriggersList';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) {
return {
'lists': {
title: t('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: {
':listId([0-9]+)': {
title: resolved => t('listName', {name: ellipsizeBreadcrumbLabel(resolved.list.name)}),
@ -70,7 +86,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/lists/${params.listId}/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: {
title: t('fields'),
@ -191,12 +207,15 @@ function getMenus(t) {
},
create: {
title: t('create'),
panelRender: props => <ListsCUD action="create" />
panelRender: props => <ListsCUD action="create" permissions={props.permissions} />
},
forms: {
title: t('customForms-1'),
link: '/lists/forms',
panelComponent: FormsList,
checkPermissions: {
...namespaceCheckPermissions('createCustomForm')
},
panelRender: props => <FormsList permissions={props.permissions}/>,
children: {
':formsId([0-9]+)': {
title: resolved => t('customFormsName', {name: ellipsizeBreadcrumbLabel(resolved.forms.name)}),
@ -209,7 +228,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/lists/forms/${params.formsId}/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: {
title: t('share'),
@ -221,7 +240,7 @@ function getMenus(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 {getUrl} from "../lib/urls";
import {withComponentMixins} from "../lib/decorator-helpers";
import {getDefaultNamespace} from "../lib/namespace";
@withComponentMixins([
withTranslation,
@ -43,7 +44,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
entity: PropTypes.object
entity: PropTypes.object,
permissions: PropTypes.object
}
submitFormValuesMutator(data) {
@ -97,7 +99,7 @@ export default class CUD extends Component {
this.populateFormValues({
name: '',
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 {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
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 {checkPermissions} from "../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {getGlobalNamespaceId} from "../../../shared/namespaces";
import {withComponentMixins} from "../lib/decorator-helpers";
import mailtrainConfig from 'mailtrainConfig';
import PropTypes from 'prop-types';
@withComponentMixins([
withTranslation,
@ -26,28 +26,16 @@ export default class List extends Component {
tableRestActionDialogInit(this);
}
@withAsyncErrorHandler
async fetchPermissions() {
const result = await checkPermissions({
createNamespace: {
entityTypeId: 'namespace',
requiredOperations: ['createNamespace']
}
});
this.setState({
createPermitted: result.data.createNamespace
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
static propTypes = {
permissions: PropTypes.object
}
render() {
const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createNamespace;
const actions = node => {
const actions = [];
@ -76,7 +64,7 @@ export default class List extends Component {
return (
<div>
{tableRestActionDialogRender(this)}
{this.state.createPermitted &&
{createPermitted &&
<Toolbar>
<LinkButton to="/namespaces/create" className="btn-primary" icon="plus" label={t('createNamespace')}/>
</Toolbar>

View file

@ -5,13 +5,21 @@ import CUD from './CUD';
import List from './List';
import Share from '../shares/Share';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) {
return {
namespaces: {
title: t('namespaces'),
link: '/namespaces',
panelComponent: List,
checkPermissions: {
createNamespace: {
entityTypeId: 'namespace',
requiredOperations: ['createNamespace']
},
...namespaceCheckPermissions('createNamespace')
},
panelRender: props => <List permissions={props.permissions}/>,
children: {
':namespaceId([0-9]+)': {
title: resolved => t('namespaceName', {name: ellipsizeBreadcrumbLabel(resolved.namespace.name)}),
@ -24,7 +32,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/namespaces/${params.namespaceId}/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: {
title: t('share'),
@ -36,7 +44,7 @@ function getMenus(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 {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling';
import moment from 'moment';
import {NamespaceSelect, validateNamespace} from '../lib/namespace';
import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "../lib/urls";
import {withComponentMixins} from "../lib/decorator-helpers";
@ -49,7 +48,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
entity: PropTypes.object
entity: PropTypes.object,
permissions: PropTypes.object
}
@withAsyncErrorHandler
@ -97,7 +97,7 @@ export default class CUD extends Component {
name: '',
description: '',
report_template: null,
namespace: mailtrainConfig.user.namespace,
namespace: getDefaultNamespace(this.props.permissions),
user_fields: null
});
}

View file

@ -9,10 +9,10 @@ import moment from 'moment';
import axios from '../lib/axios';
import {ReportState} from '../../../shared/reports';
import {Icon} from "../lib/bootstrap-components";
import {checkPermissions} from "../lib/permissions";
import {getUrl} from "../lib/urls";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([
withTranslation,
@ -28,36 +28,8 @@ export default class List extends Component {
tableRestActionDialogInit(this);
}
@withAsyncErrorHandler
async fetchPermissions() {
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();
static propTypes = {
permissions: PropTypes.object
}
@withAsyncErrorHandler
@ -75,6 +47,10 @@ export default class List extends Component {
render() {
const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createReport && permissions.executeReportTemplate;
const templatesPermitted = permissions.createReportTemplate || permissions.viewReportTemplate;
const columns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('template') },
@ -176,10 +152,10 @@ export default class List extends Component {
<div>
{tableRestActionDialogRender(this)}
<Toolbar>
{this.state.createPermitted &&
{createPermitted &&
<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')}/>
}
</Toolbar>

View file

@ -10,6 +10,7 @@ import Share from '../shares/Share';
import {ReportState} from '../../../shared/reports';
import mailtrainConfig from 'mailtrainConfig';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) {
@ -17,7 +18,26 @@ function getMenus(t) {
'reports': {
title: t('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: {
':reportId([0-9]+)': {
title: resolved => t('reportName', {name: ellipsizeBreadcrumbLabel(resolved.report.name)}),
@ -30,7 +50,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/reports/${params.reportId}/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: {
title: t('view'),
@ -59,12 +79,15 @@ function getMenus(t) {
},
create: {
title: t('create'),
panelRender: props => <ReportsCUD action="create" />
panelRender: props => <ReportsCUD action="create" permissions={props.permissions} />
},
templates: {
title: t('templates'),
link: '/reports/templates',
panelComponent: ReportTemplatesList,
checkPermissions: {
...namespaceCheckPermissions('createReportTemplate')
},
panelRender: props => <ReportTemplatesList permissions={props.permissions}/>,
children: {
':templateId([0-9]+)': {
title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.template.name)}),
@ -77,7 +100,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/reports/templates/${params.templateId}/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: {
title: t('share'),
@ -90,7 +113,7 @@ function getMenus(t) {
create: {
title: t('create'),
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
} from '../../lib/form';
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 mailtrainConfig from 'mailtrainConfig';
import 'brace/mode/javascript';
import 'brace/mode/json';
import 'brace/mode/handlebars';
@ -46,7 +45,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
wizard: PropTypes.string,
entity: PropTypes.object
entity: PropTypes.object,
permissions: PropTypes.object
}
submitFormValuesMutator(data) {
@ -64,7 +64,7 @@ export default class CUD extends Component {
this.populateFormValues({
name: '',
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',
user_fields:
'[\n' +
@ -114,7 +114,7 @@ export default class CUD extends Component {
this.populateFormValues({
name: '',
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',
user_fields:
'[\n' +
@ -144,7 +144,7 @@ export default class CUD extends Component {
this.populateFormValues({
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.)',
namespace: mailtrainConfig.user.namespace,
namespace: getDefaultNamespace(this.props.permissions),
mime_type: 'text/html',
user_fields:
'[\n' +
@ -215,7 +215,7 @@ export default class CUD extends Component {
this.populateFormValues({
name: '',
description: '',
namespace: mailtrainConfig.user.namespace,
namespace: getDefaultNamespace(this.props.permissions),
mime_type: 'text/html',
user_fields: '',
js: '',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,13 +4,13 @@ import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n';
import {Icon} from '../lib/bootstrap-components';
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 moment from 'moment';
import {getTemplateTypes, getTagLanguages} from './helpers';
import {checkPermissions} from "../lib/permissions";
import {getTagLanguages, getTemplateTypes} from './helpers';
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([
@ -30,37 +30,17 @@ export default class List extends Component {
tableRestActionDialogInit(this);
}
@withAsyncErrorHandler
async fetchPermissions() {
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();
static propTypes = {
permissions: PropTypes.object
}
render() {
const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createTemplate;
const mosaicoTemplatesPermitted = permissions.createMosaicoTemplate || permissions.viewMosaicoTemplate;
const columns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
@ -105,10 +85,10 @@ export default class List extends Component {
<div>
{tableRestActionDialogRender(this)}
<Toolbar>
{this.state.createPermitted &&
{createPermitted &&
<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')}/>
}
</Toolbar>

View file

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

View file

@ -4,14 +4,14 @@ import React, {Component} from 'react';
import {withTranslation} from '../../lib/i18n';
import {ButtonDropdown, Icon} from '../../lib/bootstrap-components';
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 moment from 'moment';
import {getTemplateTypes} from './helpers';
import {getTagLanguages} from '../helpers';
import {checkPermissions} from "../../lib/permissions";
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
import {withComponentMixins} from "../../lib/decorator-helpers";
import PropTypes from 'prop-types';
@withComponentMixins([
@ -31,28 +31,16 @@ export default class List extends Component {
tableRestActionDialogInit(this);
}
@withAsyncErrorHandler
async fetchPermissions() {
const result = await checkPermissions({
createMosaicoTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createMosaicoTemplate']
}
});
this.setState({
createPermitted: result.data.createMosaicoTemplate
});
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.fetchPermissions();
static propTypes = {
permissions: PropTypes.object
}
render() {
const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createMosaicoTemplate;
const columns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
@ -103,7 +91,7 @@ export default class List extends Component {
return (
<div>
{tableRestActionDialogRender(this)}
{this.state.createPermitted &&
{createPermitted &&
<Toolbar>
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createMosaicoTemplate')}>
<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 MosaicoList from './mosaico/List';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) {
return {
'templates': {
title: t('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: {
':templateId([0-9]+)': {
title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.template.name)}),
@ -29,7 +44,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/templates/${params.templateId}/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: {
title: t('files'),
@ -47,12 +62,15 @@ function getMenus(t) {
},
create: {
title: t('create'),
panelRender: props => <TemplatesCUD action="create" />
panelRender: props => <TemplatesCUD action="create" permissions={props.permissions} />
},
mosaico: {
title: t('mosaicoTemplates'),
link: '/templates/mosaico',
panelComponent: MosaicoList,
checkPermissions: {
...namespaceCheckPermissions('createMosaicoTemplate')
},
panelRender: props => <MosaicoList permissions={props.permissions}/>,
children: {
':mosaiceTemplateId([0-9]+)': {
title: resolved => t('mosaicoTemplateName', {name: ellipsizeBreadcrumbLabel(resolved.mosaicoTemplate.name)}),
@ -65,7 +83,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/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: {
title: t('files'),
@ -90,7 +108,7 @@ function getMenus(t) {
create: {
title: t('create'),
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 passwordValidator from '../../../shared/password-validator';
import mailtrainConfig from 'mailtrainConfig';
import {NamespaceSelect, validateNamespace} from '../lib/namespace';
import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers";
@ -49,7 +49,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
entity: PropTypes.object
entity: PropTypes.object,
permissions: PropTypes.object
}
getFormValuesMutator(data) {
@ -71,7 +72,7 @@ export default class CUD extends Component {
email: '',
password: '',
password2: '',
namespace: mailtrainConfig.user.namespace,
namespace: getDefaultNamespace(this.props.permissions),
role: null
});
}

View file

@ -5,12 +5,17 @@ import CUD from './CUD';
import List from './List';
import UserShares from '../shares/UserShares';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
import {namespaceCheckPermissions} from "../lib/namespace";
import MosaicoCUD from "../templates/mosaico/CUD";
function getMenus(t) {
return {
'users': {
title: t('users'),
link: '/users',
checkPermissions: {
...namespaceCheckPermissions('manageUsers')
},
panelComponent: List,
children: {
':userId([0-9]+)': {
@ -23,7 +28,7 @@ function getMenus(t) {
':action(edit|delete)': {
title: t('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: {
title: t('shares'),
@ -34,7 +39,7 @@ function getMenus(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
# 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:
global:
master:
@ -308,12 +312,14 @@ defaultRoles:
campaignsCreator:
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]
children:
sendConfiguration: [viewPublic]
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles]
mosaicoTemplate: [view, viewFiles]
namespace: [view, createTemplate, createCampaign]
sendConfiguration:
master:
@ -378,5 +384,9 @@ defaultRoles:
name: Master
description: All permissions
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) {
await knex.transaction(async tx => {
await shares.enforceGlobalPermission(context, 'createJavascriptWithROAccess');
await shares.enforceEntityPermissionTx(tx, context, 'mosaicoTemplate', entity.id, 'edit');
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:
{
XXX1: {
entityTypeId: ...
entityTypeId: ...,
requiredOperations: [ ... ]
},
XXX2: {
entityTypeId: ...
entityTypeId: ...,
entityId: ...,
requiredOperations: [ ... ]
}
}