Added 'sendToTestUsers' permission to templates to control if a user can send a template to test users. (Up till now this was permitted by default.)

Campaigns list is now by default ordered by 'Created' in descending order.

Fixed display bug - two clicks on main menu item made it disappear

Campaign Status is now protected by 'view' permission. (Up till now it was 'viewStats' permission.)

Fixes in campaign status to hide send buttons and test send button if a user does not have necessary permissions.

Templates, Mosaico templates and Campaigns (edit and content) are now displayed to user even if the user does have only 'view' permission (not 'edit'). A banner is displayed that the user cannot save any changes and buttons are removed from the edit pages. This is to allow users to copy settings and content from existing campaigns which they are not supposed to edit. A better solution would be to display the edit and content form in read-only mode, but this seems to be a bit complicated.
This commit is contained in:
Tomas Bures 2020-01-12 12:07:14 +01:00
parent 674399eb74
commit 7914077acb
16 changed files with 227 additions and 135 deletions

View file

@ -36,6 +36,7 @@ import {getMailerTypes} from "../send-configurations/helpers";
import {getCampaignLabels} from "./helpers"; import {getCampaignLabels} from "./helpers";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import interoperableErrors from "../../../shared/interoperable-errors"; import interoperableErrors from "../../../shared/interoperable-errors";
import {Trans} from "react-i18next";
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -104,6 +105,7 @@ export default class CUD extends Component {
this.nextListEntryId = 0; this.nextListEntryId = 0;
this.initForm({ this.initForm({
leaveConfirmation: !props.entity || props.entity.permissions.includes('edit'),
onChange: { onChange: {
send_configuration: ::this.onSendConfigurationChanged send_configuration: ::this.onSendConfigurationChanged
}, },
@ -535,6 +537,7 @@ export default class CUD extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
const isEdit = !!this.props.entity; const isEdit = !!this.props.entity;
const canModify = !isEdit || this.props.entity.permissions.includes('edit');
const canDelete = isEdit && this.props.entity.permissions.includes('delete'); const canDelete = isEdit && this.props.entity.permissions.includes('delete');
let extraSettings = null; let extraSettings = null;
@ -751,6 +754,12 @@ export default class CUD extends Component {
<Title>{isEdit ? this.editTitles[this.getFormValue('type')] : this.createTitles[this.getFormValue('type')]}</Title> <Title>{isEdit ? this.editTitles[this.getFormValue('type')] : this.createTitles[this.getFormValue('type')]}</Title>
{!canModify &&
<div className="alert alert-warning" role="alert">
<Trans><b>Warning!</b> You do not have necessary permissions to edit this campaign. Any changes that you perform here will be lost.</Trans>
</div>
}
{isEdit && this.props.entity.status === CampaignStatus.SENDING && {isEdit && this.props.entity.status === CampaignStatus.SENDING &&
<div className={`alert alert-info`} role="alert"> <div className={`alert alert-info`} role="alert">
{t('formCannotBeEditedBecauseTheCampaignIs')} {t('formCannotBeEditedBecauseTheCampaignIs')}
@ -808,6 +817,8 @@ export default class CUD extends Component {
{templateEdit} {templateEdit}
<ButtonRow> <ButtonRow>
{canModify &&
<>
{!isEdit && (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) ? {!isEdit && (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) ?
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndEditContent')}/> <Button type="submit" className="btn-primary" icon="check" label={t('saveAndEditContent')}/>
: :
@ -817,6 +828,8 @@ export default class CUD extends Component {
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndGoToStatus')} onClickAsync={async () => await this.submitHandler(CUD.AfterSubmitAction.STATUS)}/> <Button type="submit" className="btn-primary" icon="check" label={t('saveAndGoToStatus')} onClickAsync={async () => await this.submitHandler(CUD.AfterSubmitAction.STATUS)}/>
</> </>
} }
</>
}
{canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/campaigns/${this.props.entity.id}/delete`}/> } {canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/campaigns/${this.props.entity.id}/delete`}/> }
</ButtonRow> </ButtonRow>
</Form> </Form>

View file

@ -24,6 +24,7 @@ import {getUrl} from "../lib/urls";
import {TestSendModalDialog, TestSendModalDialogMode} from "./TestSendModalDialog"; import {TestSendModalDialog, TestSendModalDialogMode} from "./TestSendModalDialog";
import {withComponentMixins} from "../lib/decorator-helpers"; import {withComponentMixins} from "../lib/decorator-helpers";
import {ContentModalDialog} from "../lib/modals"; import {ContentModalDialog} from "../lib/modals";
import {Trans} from "react-i18next";
@withComponentMixins([ @withComponentMixins([
@ -62,6 +63,7 @@ export default class CustomContent extends Component {
}; };
this.initForm({ this.initForm({
leaveConfirmation: props.entity.permissions.includes('edit'),
getPreSubmitUpdater: ::this.getPreSubmitFormValuesUpdater, getPreSubmitUpdater: ::this.getPreSubmitFormValuesUpdater,
onChangeBeforeValidation: { onChangeBeforeValidation: {
data_sourceCustom_tag_language: ::this.onTagLanguageChanged data_sourceCustom_tag_language: ::this.onTagLanguageChanged
@ -249,8 +251,7 @@ export default class CustomContent extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
const canModify = this.props.entity.permissions.includes('edit');
// TODO: Toggle HTML preview
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type'); const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
@ -272,6 +273,12 @@ export default class CustomContent extends Component {
<Title>{t('editCustomContent')}</Title> <Title>{t('editCustomContent')}</Title>
{!canModify &&
<div className="alert alert-warning" role="alert">
<Trans><b>Warning!</b> You do not have necessary permissions to edit this campaign. Any changes that you perform here will be lost.</Trans>
</div>
}
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('customTemplateEditor')}> <StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('customTemplateEditor')}>
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName} {customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
@ -284,9 +291,13 @@ export default class CustomContent extends Component {
{customTemplateTypeKey && getEditForm(this, customTemplateTypeKey, 'data_sourceCustom_')} {customTemplateTypeKey && getEditForm(this, customTemplateTypeKey, 'data_sourceCustom_')}
<ButtonRow> <ButtonRow>
{canModify &&
<>
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/> <Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(CustomContent.AfterSubmitAction.LEAVE)}/> <Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(CustomContent.AfterSubmitAction.LEAVE)}/>
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndGoToStatus')} onClickAsync={async () => await this.submitHandler(CustomContent.AfterSubmitAction.STATUS)}/> <Button type="submit" className="btn-primary" icon="check" label={t('saveAndGoToStatus')} onClickAsync={async () => await this.submitHandler(CustomContent.AfterSubmitAction.STATUS)}/>
</>
}
<Button className="btn-success" icon="at" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/> <Button className="btn-success" icon="at" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/>
</ButtonRow> </ButtonRow>
</Form> </Form>

View file

@ -76,19 +76,21 @@ export default class List extends Component {
const status = data[5]; const status = data[5];
const campaignSource = data[7]; const campaignSource = data[7];
if (perms.includes('viewStats')) { if (perms.includes('view')) {
actions.push({ actions.push({
label: <Icon icon="envelope" title={t('status')}/>, label: <Icon icon="envelope" title={t('status')}/>,
link: `/campaigns/${data[0]}/status` link: `/campaigns/${data[0]}/status`
}); });
}
if (perms.includes('viewStats')) {
actions.push({ actions.push({
label: <Icon icon="signal" title={t('statistics')}/>, label: <Icon icon="signal" title={t('statistics')}/>,
link: `/campaigns/${data[0]}/statistics` link: `/campaigns/${data[0]}/statistics`
}); });
} }
if (perms.includes('edit')) { if (perms.includes('view') || perms.includes('edit')) {
actions.push({ actions.push({
label: <Icon icon="edit" title={t('edit')}/>, label: <Icon icon="edit" title={t('edit')}/>,
link: `/campaigns/${data[0]}/edit` link: `/campaigns/${data[0]}/edit`
@ -152,7 +154,7 @@ export default class List extends Component {
<Title>{t('campaigns')}</Title> <Title>{t('campaigns')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} /> <Table ref={node => this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} order={[5, 'desc']} />
</div> </div>
); );
} }

View file

@ -317,6 +317,9 @@ class SendControls extends Component {
const t = this.props.t; const t = this.props.t;
const entity = this.props.entity; const entity = this.props.entity;
const testSendPermitted = entity.permissions.includes('sendToTestUsers');
const sendPermitted = entity.permissions.includes('send');
const dialogs = ( const dialogs = (
<> <>
<TestSendModalDialog <TestSendModalDialog
@ -342,10 +345,57 @@ class SendControls extends Component {
const testButtons = ( const testButtons = (
<> <>
<Button className="btn-success" label={t('Preview')} onClickAsync={async () => this.setState({previewForTestUserVisible: true})}/> <Button className="btn-success" label={t('Preview')} onClickAsync={async () => this.setState({previewForTestUserVisible: true})}/>
<Button className="btn-success" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/> {testSendPermitted && <Button className="btn-success" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/>}
</> </>
); );
let sendStatus = null;
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
sendStatus = (
<AlignedRow label={t('sendStatus')}>
{entity.status === CampaignStatus.SCHEDULED ? t('campaignIsScheduledForDelivery') : t('campaignIsReadyToBeSentOut')}
</AlignedRow>
);
} else if (entity.status === CampaignStatus.PAUSING) {
sendStatus = (
<AlignedRow label={t('sendStatus')}>
{t('Campaign is being paused. Please wait.')}
</AlignedRow>
);
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
sendStatus = (
<AlignedRow label={t('sendStatus')}>
{t('campaignIsBeingSentOut')}
</AlignedRow>
);
} else if (entity.status === CampaignStatus.FINISHED) {
sendStatus = (
<AlignedRow label={t('sendStatus')}>
{sendPermitted ? t('allMessagesSent!HitContinueIfYouYouWant') : t('All messages sent!')}
</AlignedRow>
);
} else if (entity.status === CampaignStatus.INACTIVE) {
sendStatus = (
<AlignedRow label={t('sendStatus')}>
{sendPermitted ? t('yourCampaignIsCurrentlyDisabledClick') : t('Your campaign is currently disabled.')}
</AlignedRow>
);
} else if (entity.status === CampaignStatus.ACTIVE) {
sendStatus = (
<AlignedRow label={t('sendStatus')}>
{t('yourCampaignIsEnabledAndSendingMessages')}
</AlignedRow>
);
}
let content = null;
let sendButtons = null;
if (sendPermitted) {
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) { if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
const timezoneColumns = [ const timezoneColumns = [
@ -367,13 +417,7 @@ class SendControls extends Component {
} }
} }
content = (
return (
<div>{dialogs}
<AlignedRow label={t('sendStatus')}>
{entity.status === CampaignStatus.SCHEDULED ? t('campaignIsScheduledForDelivery') : t('campaignIsReadyToBeSentOut')}
</AlignedRow>
<Form stateOwner={this}> <Form stateOwner={this}>
<CheckBox id="sendLater" label={t('sendLater')} text={t('scheduleDeliveryAtAParticularDatetime')}/> <CheckBox id="sendLater" label={t('sendLater')} text={t('scheduleDeliveryAtAParticularDatetime')}/>
{this.getFormValue('sendLater') && {this.getFormValue('sendLater') &&
@ -387,7 +431,10 @@ class SendControls extends Component {
</div> </div>
} }
</Form> </Form>
<ButtonRow className={campaignsStyles.sendButtonRow}> );
sendButtons = (
<>
{this.getFormValue('sendLater') ? {this.getFormValue('sendLater') ?
<Button className="btn-primary" icon="play" label={entity.status === CampaignStatus.SCHEDULED ? t('rescheduleSend') : t('scheduleSend')} onClickAsync={::this.confirmSchedule}/> <Button className="btn-primary" icon="play" label={entity.status === CampaignStatus.SCHEDULED ? t('rescheduleSend') : t('scheduleSend')} onClickAsync={::this.confirmSchedule}/>
: :
@ -396,83 +443,61 @@ class SendControls extends Component {
{entity.status === CampaignStatus.SCHEDULED && <Button className="btn-primary" icon="pause" label={t('Pause')} onClickAsync={::this.stopAsync}/>} {entity.status === CampaignStatus.SCHEDULED && <Button className="btn-primary" icon="pause" label={t('Pause')} onClickAsync={::this.stopAsync}/>}
{entity.status === CampaignStatus.PAUSED && <Button className="btn-primary" icon="redo" label={t('reset')} onClickAsync={::this.resetAsync}/>} {entity.status === CampaignStatus.PAUSED && <Button className="btn-primary" icon="redo" label={t('reset')} onClickAsync={::this.resetAsync}/>}
{entity.status === CampaignStatus.PAUSED && <LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>} {entity.status === CampaignStatus.PAUSED && <LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>}
{testButtons} </>
</ButtonRow>
</div>
); );
} else if (entity.status === CampaignStatus.PAUSING) { } else if (entity.status === CampaignStatus.PAUSING) {
return ( sendButtons = (
<div>{dialogs} <>
<AlignedRow label={t('sendStatus')}>
{t('Campaign is being paused. Please wait.')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="pause" label={t('Pausing')} disabled={true}/> <Button className="btn-primary" icon="pause" label={t('Pausing')} disabled={true}/>
<LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/> <LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>
{testButtons} </>
</ButtonRow>
</div>
); );
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) { } else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
return ( sendButtons = (
<div>{dialogs} <>
<AlignedRow label={t('sendStatus')}>
{t('campaignIsBeingSentOut')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="pause" label={t('Pause')} onClickAsync={::this.stopAsync}/> <Button className="btn-primary" icon="pause" label={t('Pause')} onClickAsync={::this.stopAsync}/>
<LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/> <LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>
{testButtons} </>
</ButtonRow>
</div>
); );
} else if (entity.status === CampaignStatus.FINISHED) { } else if (entity.status === CampaignStatus.FINISHED) {
return ( sendButtons = (
<div>{dialogs} <>
<AlignedRow label={t('sendStatus')}>
{t('allMessagesSent!HitContinueIfYouYouWant')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="play" label={t('continue')} onClickAsync={::this.confirmStart}/> <Button className="btn-primary" icon="play" label={t('continue')} onClickAsync={::this.confirmStart}/>
<Button className="btn-primary" icon="redo" label={t('reset')} onClickAsync={::this.resetAsync}/> <Button className="btn-primary" icon="redo" label={t('reset')} onClickAsync={::this.resetAsync}/>
<LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/> <LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>
{testButtons} </>
</ButtonRow>
</div>
); );
} else if (entity.status === CampaignStatus.INACTIVE) { } else if (entity.status === CampaignStatus.INACTIVE) {
return ( sendButtons = (
<div>{dialogs} <>
<AlignedRow label={t('sendStatus')}>
{t('yourCampaignIsCurrentlyDisabledClick')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="play" label={t('enable')} onClickAsync={::this.enableAsync}/> <Button className="btn-primary" icon="play" label={t('enable')} onClickAsync={::this.enableAsync}/>
{testButtons} </>
</ButtonRow>
</div>
); );
} else if (entity.status === CampaignStatus.ACTIVE) { } else if (entity.status === CampaignStatus.ACTIVE) {
return ( sendButtons = (
<div>{dialogs} <>
<AlignedRow label={t('sendStatus')}>
{t('yourCampaignIsEnabledAndSendingMessages')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="stop" label={t('disable')} onClickAsync={::this.disableAsync}/> <Button className="btn-primary" icon="stop" label={t('disable')} onClickAsync={::this.disableAsync}/>
</>
);
}
}
return (
<div>
{dialogs}
{sendStatus}
{content}
<ButtonRow className={campaignsStyles.sendButtonRow}>
{sendButtons}
{testButtons} {testButtons}
</ButtonRow> </ButtonRow>
</div> </div>
); );
} else {
return null;
}
} }
} }
@ -508,11 +533,11 @@ export default class Status extends Component {
@withAsyncErrorHandler @withAsyncErrorHandler
async refreshEntity() { async refreshEntity() {
const newState = {} const newState = {};
let resp; let resp;
resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`)); resp = await axios.get(getUrl(`rest/campaigns-settings/${this.props.entity.id}`));
newState.entity = resp.data; newState.entity = resp.data;
try { try {
@ -598,7 +623,7 @@ export default class Status extends Component {
const campaignType = data[4]; const campaignType = data[4];
const campaignSource = data[7]; const campaignSource = data[7];
if (perms.includes('viewStats')) { if (perms.includes('view')) {
actions.push({ actions.push({
label: <Icon icon="send" title={t('status')}/>, label: <Icon icon="send" title={t('status')}/>,
link: `/campaigns/${data[0]}/status` link: `/campaigns/${data[0]}/status`

View file

@ -52,7 +52,8 @@ export class TestSendModalDialog extends Component {
mode: PropTypes.number.isRequired, mode: PropTypes.number.isRequired,
onHide: PropTypes.func.isRequired, onHide: PropTypes.func.isRequired,
getDataAsync: PropTypes.func, getDataAsync: PropTypes.func,
campaign: PropTypes.object campaign: PropTypes.object,
template: PropTypes.object
} }
onListChanged(mutStateData, key, oldValue, newValue) { onListChanged(mutStateData, key, oldValue, newValue) {
@ -101,6 +102,7 @@ export class TestSendModalDialog extends Component {
} }
if (mode === TestSendModalDialogMode.TEMPLATE) { if (mode === TestSendModalDialogMode.TEMPLATE) {
data.templateId = props.template.id;
data.listCid = this.getFormValue('listCid'); data.listCid = this.getFormValue('listCid');
data.subscriptionCid = this.getFormValue('testUserSubscriptionCid'); data.subscriptionCid = this.getFormValue('testUserSubscriptionCid');
data.sendConfigurationId = this.getFormValue('sendConfiguration'); data.sendConfigurationId = this.getFormValue('sendConfiguration');

View file

@ -48,7 +48,7 @@ function getMenus(t) {
status: { status: {
title: t('status'), title: t('status'),
link: params => `/campaigns/${params.campaignId}/status`, link: params => `/campaigns/${params.campaignId}/status`,
visible: resolved => resolved.campaign.permissions.includes('viewStats'), visible: resolved => resolved.campaign.permissions.includes('view'),
panelRender: props => <Status entity={props.resolved.campaign} /> panelRender: props => <Status entity={props.resolved.campaign} />
}, },
statistics: { statistics: {
@ -101,7 +101,7 @@ function getMenus(t) {
':action(edit|delete)': { ':action(edit|delete)': {
title: t('edit'), title: t('edit'),
link: params => `/campaigns/${params.campaignId}/edit`, link: params => `/campaigns/${params.campaignId}/edit`,
visible: resolved => resolved.campaign.permissions.includes('edit'), visible: resolved => resolved.campaign.permissions.includes('view') || resolved.campaign.permissions.includes('edit'),
panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} permissions={props.permissions} /> panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} permissions={props.permissions} />
}, },
content: { content: {
@ -110,7 +110,7 @@ function getMenus(t) {
resolve: { resolve: {
campaignContent: params => `rest/campaigns-content/${params.campaignId}` campaignContent: params => `rest/campaigns-content/${params.campaignId}`
}, },
visible: resolved => resolved.campaign.permissions.includes('edit') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN), visible: resolved => (resolved.campaign.permissions.includes('view') || resolved.campaign.permissions.includes('edit')) && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
panelRender: props => <Content entity={props.resolved.campaignContent} setPanelInFullScreen={props.setPanelInFullScreen} /> panelRender: props => <Content entity={props.resolved.campaignContent} setPanelInFullScreen={props.setPanelInFullScreen} />
}, },
files: { files: {

View file

@ -53,13 +53,15 @@ class Table extends Component {
onSelectionDataAsync: PropTypes.func, onSelectionDataAsync: PropTypes.func,
withHeader: PropTypes.bool, withHeader: PropTypes.bool,
refreshInterval: PropTypes.number, refreshInterval: PropTypes.number,
pageLength: PropTypes.number pageLength: PropTypes.number,
order: PropTypes.array
} }
static defaultProps = { static defaultProps = {
selectMode: TableSelectMode.NONE, selectMode: TableSelectMode.NONE,
selectionKeyIndex: 0, selectionKeyIndex: 0,
pageLength: 50 pageLength: 50,
order: [[0, 'asc']]
} }
refresh() { refresh() {
@ -277,6 +279,7 @@ class Table extends Component {
const dtOptions = { const dtOptions = {
columns, columns,
order: this.props.order,
autoWidth: false, autoWidth: false,
pageLength: this.props.pageLength, pageLength: this.props.pageLength,
dom: // This overrides Bootstrap 4 settings. It may need to be updated if there are updates in the DataTables Bootstrap 4 plugin. dom: // This overrides Bootstrap 4 settings. It may need to be updated if there are updates in the DataTables Bootstrap 4 plugin.

View file

@ -167,6 +167,7 @@ export default class CUD extends Component {
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd')); this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
} }
} catch (error) { } catch (error) {
console.log(error)
if (error instanceof interoperableErrors.DuplicitEmailError) { if (error instanceof interoperableErrors.DuplicitEmailError) {
this.setFormStatusMessage('danger', this.setFormStatusMessage('danger',
<span> <span>

View file

@ -5,5 +5,6 @@ $breadcrumb-bg: #f6f7f8;
$navbar-dark-color: rgba(#fff, .75) !default; $navbar-dark-color: rgba(#fff, .75) !default;
$navbar-dark-hover-color: #fff !default; $navbar-dark-hover-color: #fff !default;
$navbar-active-color: #fff;
@import "../../node_modules/@coreui/coreui/scss/_variables.scss"; @import "../../node_modules/@coreui/coreui/scss/_variables.scss";

View file

@ -2,6 +2,7 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Trans} from 'react-i18next';
import {withTranslation} from '../lib/i18n'; import {withTranslation} from '../lib/i18n';
import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page' import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page'
import { import {
@ -56,6 +57,7 @@ export default class CUD extends Component {
}; };
this.initForm({ this.initForm({
leaveConfirmation: !props.entity || props.entity.permissions.includes('edit'),
getPreSubmitUpdater: ::this.getPreSubmitFormValuesUpdater, getPreSubmitUpdater: ::this.getPreSubmitFormValuesUpdater,
onChangeBeforeValidation: { onChangeBeforeValidation: {
type: ::this.onTypeChanged, type: ::this.onTypeChanged,
@ -292,6 +294,7 @@ export default class CUD extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
const isEdit = !!this.props.entity; const isEdit = !!this.props.entity;
const canModify = !isEdit || this.props.entity.permissions.includes('edit');
const canDelete = isEdit && this.props.entity.permissions.includes('delete'); const canDelete = isEdit && this.props.entity.permissions.includes('delete');
const typeOptions = []; const typeOptions = [];
@ -332,7 +335,9 @@ export default class CUD extends Component {
mode={TestSendModalDialogMode.TEMPLATE} mode={TestSendModalDialogMode.TEMPLATE}
visible={this.state.showTestSendModal} visible={this.state.showTestSendModal}
onHide={() => this.setState({showTestSendModal: false})} onHide={() => this.setState({showTestSendModal: false})}
getDataAsync={this.sendModalGetDataHandler}/> getDataAsync={this.sendModalGetDataHandler}
template={this.props.entity}
/>
} }
{isEdit && {isEdit &&
<ContentModalDialog <ContentModalDialog
@ -354,6 +359,12 @@ export default class CUD extends Component {
<Title>{isEdit ? t('editTemplate') : t('createTemplate')}</Title> <Title>{isEdit ? t('editTemplate') : t('createTemplate')}</Title>
{!canModify &&
<div className="alert alert-warning" role="alert">
<Trans><b>Warning!</b> You do not have necessary permissions to edit this template. Any changes that you perform here will be lost.</Trans>
</div>
}
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('name')}/> <InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/> <TextArea id="description" label={t('description')}/>
@ -385,8 +396,12 @@ export default class CUD extends Component {
{editForm} {editForm}
<ButtonRow> <ButtonRow>
{canModify &&
<>
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/> <Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
{isEdit && <Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(true)}/>} {isEdit && <Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(true)}/>}
</>
}
{canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/templates/${this.props.entity.id}/delete`}/> } {canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/templates/${this.props.entity.id}/delete`}/> }
{isEdit && <Button className="btn-success" icon="at" label={t('testSend')} onClickAsync={async () => this.setState({showTestSendModal: true})}/> } {isEdit && <Button className="btn-success" icon="at" label={t('testSend')} onClickAsync={async () => this.setState({showTestSendModal: true})}/> }
</ButtonRow> </ButtonRow>

View file

@ -53,7 +53,7 @@ export default class List extends Component {
const actions = []; const actions = [];
const perms = data[7]; const perms = data[7];
if (perms.includes('edit')) { if (perms.includes('view') || perms.includes('edit')) {
actions.push({ actions.push({
label: <Icon icon="edit" title={t('edit')}/>, label: <Icon icon="edit" title={t('edit')}/>,
link: `/templates/${data[0]}/edit` link: `/templates/${data[0]}/edit`

View file

@ -26,6 +26,7 @@ import {getTemplateTypes, getTemplateTypesOrder} from "./helpers";
import {withComponentMixins} from "../../lib/decorator-helpers"; import {withComponentMixins} from "../../lib/decorator-helpers";
import styles from "../../lib/styles.scss"; import styles from "../../lib/styles.scss";
import {getTagLanguages} from "../helpers"; import {getTagLanguages} from "../helpers";
import {Trans} from "react-i18next";
@withComponentMixins([ @withComponentMixins([
withTranslation, withTranslation,
@ -51,7 +52,9 @@ export default class CUD extends Component {
this.state = {}; this.state = {};
this.initForm(); this.initForm({
leaveConfirmation: !props.entity || props.entity.permissions.includes('edit'),
});
} }
static propTypes = { static propTypes = {
@ -183,6 +186,7 @@ export default class CUD extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
const isEdit = !!this.props.entity; const isEdit = !!this.props.entity;
const canModify = !isEdit || this.props.entity.permissions.includes('edit');
const canDelete = isEdit && this.props.entity.permissions.includes('delete'); const canDelete = isEdit && this.props.entity.permissions.includes('delete');
const typeKey = this.getFormValue('type'); const typeKey = this.getFormValue('type');
@ -207,6 +211,12 @@ export default class CUD extends Component {
<Title>{isEdit ? t('editMosaicoTemplate') : t('createMosaicoTemplate')}</Title> <Title>{isEdit ? t('editMosaicoTemplate') : t('createMosaicoTemplate')}</Title>
{!canModify &&
<div className="alert alert-warning" role="alert">
<Trans><b>Warning!</b> You do not have necessary permissions to edit this Mosaico template. Any changes that you perform here will be lost.</Trans>
</div>
}
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('name')}/> <InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/> <TextArea id="description" label={t('description')}/>
@ -225,6 +235,8 @@ export default class CUD extends Component {
{isEdit && typeKey && this.templateTypes[typeKey].getForm(this)} {isEdit && typeKey && this.templateTypes[typeKey].getForm(this)}
<ButtonRow> <ButtonRow>
{canModify &&
<>
{isEdit ? {isEdit ?
<> <>
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/> <Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
@ -233,6 +245,8 @@ export default class CUD extends Component {
: :
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndEditContent')}/> <Button type="submit" className="btn-primary" icon="check" label={t('saveAndEditContent')}/>
} }
</>
}
{canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/templates/mosaico/${this.props.entity.id}/delete`}/>} {canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/templates/mosaico/${this.props.entity.id}/delete`}/>}
{isEdit && typeKey && this.templateTypes[typeKey].getButtons(this)} {isEdit && typeKey && this.templateTypes[typeKey].getButtons(this)}
</ButtonRow> </ButtonRow>

View file

@ -53,7 +53,7 @@ export default class List extends Component {
const actions = []; const actions = [];
const perms = data[7]; const perms = data[7];
if (perms.includes('edit')) { if (perms.includes('view') || perms.includes('edit')) {
actions.push({ actions.push({
label: <Icon icon="edit" title={t('edit')}/>, label: <Icon icon="edit" title={t('edit')}/>,
link: `/templates/mosaico/${data[0]}/edit` link: `/templates/mosaico/${data[0]}/edit`

View file

@ -43,7 +43,7 @@ function getMenus(t) {
':action(edit|delete)': { ':action(edit|delete)': {
title: t('edit'), title: t('edit'),
link: params => `/templates/${params.templateId}/edit`, link: params => `/templates/${params.templateId}/edit`,
visible: resolved => resolved.template.permissions.includes('edit'), visible: resolved => resolved.template.permissions.includes('view') || resolved.template.permissions.includes('edit'),
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} permissions={props.permissions} setPanelInFullScreen={props.setPanelInFullScreen} /> panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} permissions={props.permissions} setPanelInFullScreen={props.setPanelInFullScreen} />
}, },
files: { files: {
@ -82,7 +82,7 @@ function getMenus(t) {
':action(edit|delete)': { ':action(edit|delete)': {
title: t('edit'), title: t('edit'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`, link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'), visible: resolved => resolved.mosaicoTemplate.permissions.includes('view') || resolved.mosaicoTemplate.permissions.includes('edit'),
panelRender: props => <MosaicoCUD action={props.match.params.action} entity={props.resolved.mosaicoTemplate} permissions={props.permissions}/> panelRender: props => <MosaicoCUD action={props.match.params.action} entity={props.resolved.mosaicoTemplate} permissions={props.permissions}/>
}, },
files: { files: {

View file

@ -295,7 +295,7 @@ defaultRoles:
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers] list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
customForm: [view, edit, delete, share] customForm: [view, edit, delete, share]
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss] campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles] template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
report: [view, edit, delete, share, execute, viewContent, viewOutput] report: [view, edit, delete, share, execute, viewContent, viewOutput]
reportTemplate: [view, edit, delete, share, execute] reportTemplate: [view, edit, delete, share, execute]
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles] mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
@ -310,7 +310,7 @@ defaultRoles:
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers] list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
customForm: [view, edit, delete, share] customForm: [view, edit, delete, share]
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss] campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles] template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
report: [view, edit, delete, share, execute, viewContent, viewOutput] report: [view, edit, delete, share, execute, viewContent, viewOutput]
reportTemplate: [view, share, execute] reportTemplate: [view, share, execute]
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles] mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
@ -323,7 +323,7 @@ defaultRoles:
children: children:
sendConfiguration: [viewPublic] sendConfiguration: [viewPublic]
campaign: [view, edit, delete, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, viewStats, fetchRss] campaign: [view, edit, delete, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, viewStats, fetchRss]
template: [view, edit, delete, viewFiles, manageFiles] template: [view, edit, delete, viewFiles, manageFiles, sendToTestUsers]
mosaicoTemplate: [view, viewFiles] mosaicoTemplate: [view, viewFiles]
namespace: [view, createTemplate, createCampaign] namespace: [view, createTemplate, createCampaign]
@ -385,7 +385,7 @@ defaultRoles:
master: master:
name: Master name: Master
description: All permissions description: All permissions
permissions: [view, edit, delete, share, viewFiles, manageFiles] permissions: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
viewer: viewer:
name: Viewer name: Viewer
description: The user can view the template but cannot edit it. description: The user can view the template but cannot edit it.

View file

@ -1062,6 +1062,11 @@ async function testSend(context, data) {
const list = await lists.getByCidTx(tx, context, data.listCid); const list = await lists.getByCidTx(tx, context, data.listCid);
const subscriber = await subscriptions.getByCidTx(tx, context, list.id, data.subscriptionCid, true, true); const subscriber = await subscriptions.getByCidTx(tx, context, list.id, data.subscriptionCid, true, true);
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', data.sendConfigurationId, 'sendWithoutOverrides');
await shares.enforceEntityPermissionTx(tx, context, 'template', data.templateId, 'sendToTestUsers');
await shares.enforceEntityPermissionTx(tx, context, 'list', list.id, 'sendToTestUsers');
await processSubscriber(data.sendConfigurationId, list.id, subscriber.id, messageData); await processSubscriber(data.sendConfigurationId, list.id, subscriber.id, messageData);
} }
}); });