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} />
},
}
}