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:
parent
674399eb74
commit
7914077acb
16 changed files with 227 additions and 135 deletions
|
@ -36,6 +36,7 @@ import {getMailerTypes} from "../send-configurations/helpers";
|
|||
import {getCampaignLabels} from "./helpers";
|
||||
import {withComponentMixins} from "../lib/decorator-helpers";
|
||||
import interoperableErrors from "../../../shared/interoperable-errors";
|
||||
import {Trans} from "react-i18next";
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
|
@ -104,6 +105,7 @@ export default class CUD extends Component {
|
|||
this.nextListEntryId = 0;
|
||||
|
||||
this.initForm({
|
||||
leaveConfirmation: !props.entity || props.entity.permissions.includes('edit'),
|
||||
onChange: {
|
||||
send_configuration: ::this.onSendConfigurationChanged
|
||||
},
|
||||
|
@ -535,6 +537,7 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canModify = !isEdit || this.props.entity.permissions.includes('edit');
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
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>
|
||||
|
||||
{!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 &&
|
||||
<div className={`alert alert-info`} role="alert">
|
||||
{t('formCannotBeEditedBecauseTheCampaignIs')}
|
||||
|
@ -808,13 +817,17 @@ export default class CUD extends Component {
|
|||
{templateEdit}
|
||||
|
||||
<ButtonRow>
|
||||
{!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')}/>
|
||||
:
|
||||
{canModify &&
|
||||
<>
|
||||
<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(CUD.AfterSubmitAction.LEAVE)}/>
|
||||
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndGoToStatus')} onClickAsync={async () => await this.submitHandler(CUD.AfterSubmitAction.STATUS)}/>
|
||||
{!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('save')}/>
|
||||
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(CUD.AfterSubmitAction.LEAVE)}/>
|
||||
<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`}/> }
|
||||
|
|
|
@ -24,6 +24,7 @@ import {getUrl} from "../lib/urls";
|
|||
import {TestSendModalDialog, TestSendModalDialogMode} from "./TestSendModalDialog";
|
||||
import {withComponentMixins} from "../lib/decorator-helpers";
|
||||
import {ContentModalDialog} from "../lib/modals";
|
||||
import {Trans} from "react-i18next";
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
|
@ -62,6 +63,7 @@ export default class CustomContent extends Component {
|
|||
};
|
||||
|
||||
this.initForm({
|
||||
leaveConfirmation: props.entity.permissions.includes('edit'),
|
||||
getPreSubmitUpdater: ::this.getPreSubmitFormValuesUpdater,
|
||||
onChangeBeforeValidation: {
|
||||
data_sourceCustom_tag_language: ::this.onTagLanguageChanged
|
||||
|
@ -249,8 +251,7 @@ export default class CustomContent extends Component {
|
|||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
// TODO: Toggle HTML preview
|
||||
const canModify = this.props.entity.permissions.includes('edit');
|
||||
|
||||
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||
|
||||
|
@ -272,6 +273,12 @@ export default class CustomContent extends Component {
|
|||
|
||||
<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}>
|
||||
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('customTemplateEditor')}>
|
||||
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
|
||||
|
@ -284,9 +291,13 @@ export default class CustomContent extends Component {
|
|||
{customTemplateTypeKey && getEditForm(this, customTemplateTypeKey, 'data_sourceCustom_')}
|
||||
|
||||
<ButtonRow>
|
||||
<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('saveAndGoToStatus')} onClickAsync={async () => await this.submitHandler(CustomContent.AfterSubmitAction.STATUS)}/>
|
||||
{canModify &&
|
||||
<>
|
||||
<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('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})}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
|
|
|
@ -76,19 +76,21 @@ export default class List extends Component {
|
|||
const status = data[5];
|
||||
const campaignSource = data[7];
|
||||
|
||||
if (perms.includes('viewStats')) {
|
||||
if (perms.includes('view')) {
|
||||
actions.push({
|
||||
label: <Icon icon="envelope" title={t('status')}/>,
|
||||
link: `/campaigns/${data[0]}/status`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('viewStats')) {
|
||||
actions.push({
|
||||
label: <Icon icon="signal" title={t('statistics')}/>,
|
||||
link: `/campaigns/${data[0]}/statistics`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('edit')) {
|
||||
if (perms.includes('view') || perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('edit')}/>,
|
||||
link: `/campaigns/${data[0]}/edit`
|
||||
|
@ -152,7 +154,7 @@ export default class List extends Component {
|
|||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -317,6 +317,9 @@ class SendControls extends Component {
|
|||
const t = this.props.t;
|
||||
const entity = this.props.entity;
|
||||
|
||||
const testSendPermitted = entity.permissions.includes('sendToTestUsers');
|
||||
const sendPermitted = entity.permissions.includes('send');
|
||||
|
||||
const dialogs = (
|
||||
<>
|
||||
<TestSendModalDialog
|
||||
|
@ -342,52 +345,96 @@ class SendControls extends Component {
|
|||
const testButtons = (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
|
||||
const timezoneColumns = [
|
||||
{ data: 0, title: t('Timezone') }
|
||||
];
|
||||
} else if (entity.status === CampaignStatus.PAUSING) {
|
||||
sendStatus = (
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('Campaign is being paused. Please wait.')}
|
||||
</AlignedRow>
|
||||
);
|
||||
|
||||
const dateValue = (this.getFormValue('date') || '').trim();
|
||||
const timeValue = (this.getFormValue('time') || '').trim();
|
||||
const timezone = this.getFormValue('timezone');
|
||||
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
|
||||
sendStatus = (
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('campaignIsBeingSentOut')}
|
||||
</AlignedRow>
|
||||
);
|
||||
|
||||
let dateTimeHelp = t('Select date, time and a timezone to display the date and time with offset');
|
||||
let dateTimeAlert = null;
|
||||
if (moment(dateValue, 'YYYY-MM-DD', true).isValid() && moment(timeValue, 'HH:mm', true).isValid() && timezone) {
|
||||
const dateTime = moment.tz(dateValue + ' ' + timeValue, 'YYYY-MM-DD HH:mm', timezone);
|
||||
} else if (entity.status === CampaignStatus.FINISHED) {
|
||||
sendStatus = (
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{sendPermitted ? t('allMessagesSent!HitContinueIfYouYouWant') : t('All messages sent!')}
|
||||
</AlignedRow>
|
||||
);
|
||||
|
||||
dateTimeHelp = dateTime.toString();
|
||||
if (!moment().isBefore(dateTime)) {
|
||||
dateTimeAlert = <div className="alert alert-danger" role="alert">{t('Scheduled date/time seems to be in the past. If you schedule the send, campaign will be sent immediately.')}</div>;
|
||||
} 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)) {
|
||||
|
||||
const timezoneColumns = [
|
||||
{ data: 0, title: t('Timezone') }
|
||||
];
|
||||
|
||||
const dateValue = (this.getFormValue('date') || '').trim();
|
||||
const timeValue = (this.getFormValue('time') || '').trim();
|
||||
const timezone = this.getFormValue('timezone');
|
||||
|
||||
let dateTimeHelp = t('Select date, time and a timezone to display the date and time with offset');
|
||||
let dateTimeAlert = null;
|
||||
if (moment(dateValue, 'YYYY-MM-DD', true).isValid() && moment(timeValue, 'HH:mm', true).isValid() && timezone) {
|
||||
const dateTime = moment.tz(dateValue + ' ' + timeValue, 'YYYY-MM-DD HH:mm', timezone);
|
||||
|
||||
dateTimeHelp = dateTime.toString();
|
||||
if (!moment().isBefore(dateTime)) {
|
||||
dateTimeAlert = <div className="alert alert-danger" role="alert">{t('Scheduled date/time seems to be in the past. If you schedule the send, campaign will be sent immediately.')}</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>{dialogs}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{entity.status === CampaignStatus.SCHEDULED ? t('campaignIsScheduledForDelivery') : t('campaignIsReadyToBeSentOut')}
|
||||
</AlignedRow>
|
||||
|
||||
content = (
|
||||
<Form stateOwner={this}>
|
||||
<CheckBox id="sendLater" label={t('sendLater')} text={t('scheduleDeliveryAtAParticularDatetime')}/>
|
||||
{this.getFormValue('sendLater') &&
|
||||
<div>
|
||||
<DatePicker id="date" label={t('date')} />
|
||||
<InputField id="time" label={t('time')} help={t('enter24HourTimeInFormatHhmmEg1348')}/>
|
||||
<TableSelect id="timezone" label={t('Timezone')} dropdown columns={timezoneColumns} selectionKeyIndex={0} selectionLabelIndex={0} data={this.timezoneOptions}
|
||||
help={dateTimeHelp}
|
||||
/>
|
||||
{dateTimeAlert && <AlignedRow>{dateTimeAlert}</AlignedRow>}
|
||||
</div>
|
||||
<div>
|
||||
<DatePicker id="date" label={t('date')} />
|
||||
<InputField id="time" label={t('time')} help={t('enter24HourTimeInFormatHhmmEg1348')}/>
|
||||
<TableSelect id="timezone" label={t('Timezone')} dropdown columns={timezoneColumns} selectionKeyIndex={0} selectionLabelIndex={0} data={this.timezoneOptions}
|
||||
help={dateTimeHelp}
|
||||
/>
|
||||
{dateTimeAlert && <AlignedRow>{dateTimeAlert}</AlignedRow>}
|
||||
</div>
|
||||
}
|
||||
</Form>
|
||||
<ButtonRow className={campaignsStyles.sendButtonRow}>
|
||||
);
|
||||
|
||||
sendButtons = (
|
||||
<>
|
||||
{this.getFormValue('sendLater') ?
|
||||
<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.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`}/>}
|
||||
{testButtons}
|
||||
</ButtonRow>
|
||||
</div>
|
||||
);
|
||||
</>
|
||||
);
|
||||
|
||||
} else if (entity.status === CampaignStatus.PAUSING) {
|
||||
return (
|
||||
<div>{dialogs}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('Campaign is being paused. Please wait.')}
|
||||
</AlignedRow>
|
||||
<ButtonRow>
|
||||
} else if (entity.status === CampaignStatus.PAUSING) {
|
||||
sendButtons = (
|
||||
<>
|
||||
<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`}/>
|
||||
{testButtons}
|
||||
</ButtonRow>
|
||||
</div>
|
||||
);
|
||||
</>
|
||||
);
|
||||
|
||||
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
|
||||
return (
|
||||
<div>{dialogs}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('campaignIsBeingSentOut')}
|
||||
</AlignedRow>
|
||||
<ButtonRow>
|
||||
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
|
||||
sendButtons = (
|
||||
<>
|
||||
<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`}/>
|
||||
{testButtons}
|
||||
</ButtonRow>
|
||||
</div>
|
||||
);
|
||||
</>
|
||||
);
|
||||
|
||||
} else if (entity.status === CampaignStatus.FINISHED) {
|
||||
return (
|
||||
<div>{dialogs}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('allMessagesSent!HitContinueIfYouYouWant')}
|
||||
</AlignedRow>
|
||||
<ButtonRow>
|
||||
} else if (entity.status === CampaignStatus.FINISHED) {
|
||||
sendButtons = (
|
||||
<>
|
||||
<Button className="btn-primary" icon="play" label={t('continue')} onClickAsync={::this.confirmStart}/>
|
||||
<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`}/>
|
||||
{testButtons}
|
||||
</ButtonRow>
|
||||
</div>
|
||||
);
|
||||
</>
|
||||
);
|
||||
|
||||
} else if (entity.status === CampaignStatus.INACTIVE) {
|
||||
return (
|
||||
<div>{dialogs}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('yourCampaignIsCurrentlyDisabledClick')}
|
||||
</AlignedRow>
|
||||
<ButtonRow>
|
||||
} else if (entity.status === CampaignStatus.INACTIVE) {
|
||||
sendButtons = (
|
||||
<>
|
||||
<Button className="btn-primary" icon="play" label={t('enable')} onClickAsync={::this.enableAsync}/>
|
||||
{testButtons}
|
||||
</ButtonRow>
|
||||
</div>
|
||||
);
|
||||
</>
|
||||
);
|
||||
|
||||
} else if (entity.status === CampaignStatus.ACTIVE) {
|
||||
return (
|
||||
<div>{dialogs}
|
||||
<AlignedRow label={t('sendStatus')}>
|
||||
{t('yourCampaignIsEnabledAndSendingMessages')}
|
||||
</AlignedRow>
|
||||
<ButtonRow>
|
||||
} else if (entity.status === CampaignStatus.ACTIVE) {
|
||||
sendButtons = (
|
||||
<>
|
||||
<Button className="btn-primary" icon="stop" label={t('disable')} onClickAsync={::this.disableAsync}/>
|
||||
{testButtons}
|
||||
</ButtonRow>
|
||||
</div>
|
||||
);
|
||||
|
||||
} else {
|
||||
return null;
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{dialogs}
|
||||
{sendStatus}
|
||||
{content}
|
||||
<ButtonRow className={campaignsStyles.sendButtonRow}>
|
||||
{sendButtons}
|
||||
{testButtons}
|
||||
</ButtonRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -508,11 +533,11 @@ export default class Status extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async refreshEntity() {
|
||||
const newState = {}
|
||||
const newState = {};
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
|
@ -598,7 +623,7 @@ export default class Status extends Component {
|
|||
const campaignType = data[4];
|
||||
const campaignSource = data[7];
|
||||
|
||||
if (perms.includes('viewStats')) {
|
||||
if (perms.includes('view')) {
|
||||
actions.push({
|
||||
label: <Icon icon="send" title={t('status')}/>,
|
||||
link: `/campaigns/${data[0]}/status`
|
||||
|
|
|
@ -52,7 +52,8 @@ export class TestSendModalDialog extends Component {
|
|||
mode: PropTypes.number.isRequired,
|
||||
onHide: PropTypes.func.isRequired,
|
||||
getDataAsync: PropTypes.func,
|
||||
campaign: PropTypes.object
|
||||
campaign: PropTypes.object,
|
||||
template: PropTypes.object
|
||||
}
|
||||
|
||||
onListChanged(mutStateData, key, oldValue, newValue) {
|
||||
|
@ -101,6 +102,7 @@ export class TestSendModalDialog extends Component {
|
|||
}
|
||||
|
||||
if (mode === TestSendModalDialogMode.TEMPLATE) {
|
||||
data.templateId = props.template.id;
|
||||
data.listCid = this.getFormValue('listCid');
|
||||
data.subscriptionCid = this.getFormValue('testUserSubscriptionCid');
|
||||
data.sendConfigurationId = this.getFormValue('sendConfiguration');
|
||||
|
|
|
@ -48,7 +48,7 @@ function getMenus(t) {
|
|||
status: {
|
||||
title: t('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} />
|
||||
},
|
||||
statistics: {
|
||||
|
@ -101,7 +101,7 @@ function getMenus(t) {
|
|||
':action(edit|delete)': {
|
||||
title: t('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} />
|
||||
},
|
||||
content: {
|
||||
|
@ -110,7 +110,7 @@ function getMenus(t) {
|
|||
resolve: {
|
||||
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} />
|
||||
},
|
||||
files: {
|
||||
|
|
|
@ -53,13 +53,15 @@ class Table extends Component {
|
|||
onSelectionDataAsync: PropTypes.func,
|
||||
withHeader: PropTypes.bool,
|
||||
refreshInterval: PropTypes.number,
|
||||
pageLength: PropTypes.number
|
||||
pageLength: PropTypes.number,
|
||||
order: PropTypes.array
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
selectMode: TableSelectMode.NONE,
|
||||
selectionKeyIndex: 0,
|
||||
pageLength: 50
|
||||
pageLength: 50,
|
||||
order: [[0, 'asc']]
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
@ -277,6 +279,7 @@ class Table extends Component {
|
|||
|
||||
const dtOptions = {
|
||||
columns,
|
||||
order: this.props.order,
|
||||
autoWidth: false,
|
||||
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.
|
||||
|
|
|
@ -167,6 +167,7 @@ export default class CUD extends Component {
|
|||
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
if (error instanceof interoperableErrors.DuplicitEmailError) {
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
|
|
|
@ -5,5 +5,6 @@ $breadcrumb-bg: #f6f7f8;
|
|||
|
||||
$navbar-dark-color: rgba(#fff, .75) !default;
|
||||
$navbar-dark-hover-color: #fff !default;
|
||||
$navbar-active-color: #fff;
|
||||
|
||||
@import "../../node_modules/@coreui/coreui/scss/_variables.scss";
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Trans} from 'react-i18next';
|
||||
import {withTranslation} from '../lib/i18n';
|
||||
import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page'
|
||||
import {
|
||||
|
@ -56,6 +57,7 @@ export default class CUD extends Component {
|
|||
};
|
||||
|
||||
this.initForm({
|
||||
leaveConfirmation: !props.entity || props.entity.permissions.includes('edit'),
|
||||
getPreSubmitUpdater: ::this.getPreSubmitFormValuesUpdater,
|
||||
onChangeBeforeValidation: {
|
||||
type: ::this.onTypeChanged,
|
||||
|
@ -292,6 +294,7 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canModify = !isEdit || this.props.entity.permissions.includes('edit');
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
const typeOptions = [];
|
||||
|
@ -332,7 +335,9 @@ export default class CUD extends Component {
|
|||
mode={TestSendModalDialogMode.TEMPLATE}
|
||||
visible={this.state.showTestSendModal}
|
||||
onHide={() => this.setState({showTestSendModal: false})}
|
||||
getDataAsync={this.sendModalGetDataHandler}/>
|
||||
getDataAsync={this.sendModalGetDataHandler}
|
||||
template={this.props.entity}
|
||||
/>
|
||||
}
|
||||
{isEdit &&
|
||||
<ContentModalDialog
|
||||
|
@ -354,6 +359,12 @@ export default class CUD extends Component {
|
|||
|
||||
<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}>
|
||||
<InputField id="name" label={t('name')}/>
|
||||
<TextArea id="description" label={t('description')}/>
|
||||
|
@ -385,8 +396,12 @@ export default class CUD extends Component {
|
|||
{editForm}
|
||||
|
||||
<ButtonRow>
|
||||
<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)}/>}
|
||||
{canModify &&
|
||||
<>
|
||||
<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)}/>}
|
||||
</>
|
||||
}
|
||||
{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})}/> }
|
||||
</ButtonRow>
|
||||
|
|
|
@ -53,7 +53,7 @@ export default class List extends Component {
|
|||
const actions = [];
|
||||
const perms = data[7];
|
||||
|
||||
if (perms.includes('edit')) {
|
||||
if (perms.includes('view') || perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('edit')}/>,
|
||||
link: `/templates/${data[0]}/edit`
|
||||
|
|
|
@ -26,6 +26,7 @@ import {getTemplateTypes, getTemplateTypesOrder} from "./helpers";
|
|||
import {withComponentMixins} from "../../lib/decorator-helpers";
|
||||
import styles from "../../lib/styles.scss";
|
||||
import {getTagLanguages} from "../helpers";
|
||||
import {Trans} from "react-i18next";
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
|
@ -51,7 +52,9 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {};
|
||||
|
||||
this.initForm();
|
||||
this.initForm({
|
||||
leaveConfirmation: !props.entity || props.entity.permissions.includes('edit'),
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
|
@ -183,6 +186,7 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canModify = !isEdit || this.props.entity.permissions.includes('edit');
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
const typeKey = this.getFormValue('type');
|
||||
|
@ -207,6 +211,12 @@ export default class CUD extends Component {
|
|||
|
||||
<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}>
|
||||
<InputField id="name" label={t('name')}/>
|
||||
<TextArea id="description" label={t('description')}/>
|
||||
|
@ -225,13 +235,17 @@ export default class CUD extends Component {
|
|||
{isEdit && typeKey && this.templateTypes[typeKey].getForm(this)}
|
||||
|
||||
<ButtonRow>
|
||||
{isEdit ?
|
||||
<>
|
||||
<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(true)}/>
|
||||
</>
|
||||
:
|
||||
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndEditContent')}/>
|
||||
{canModify &&
|
||||
<>
|
||||
{isEdit ?
|
||||
<>
|
||||
<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(true)}/>
|
||||
</>
|
||||
:
|
||||
<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`}/>}
|
||||
{isEdit && typeKey && this.templateTypes[typeKey].getButtons(this)}
|
||||
|
|
|
@ -53,7 +53,7 @@ export default class List extends Component {
|
|||
const actions = [];
|
||||
const perms = data[7];
|
||||
|
||||
if (perms.includes('edit')) {
|
||||
if (perms.includes('view') || perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('edit')}/>,
|
||||
link: `/templates/mosaico/${data[0]}/edit`
|
||||
|
|
|
@ -43,7 +43,7 @@ function getMenus(t) {
|
|||
':action(edit|delete)': {
|
||||
title: t('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} />
|
||||
},
|
||||
files: {
|
||||
|
@ -82,7 +82,7 @@ function getMenus(t) {
|
|||
':action(edit|delete)': {
|
||||
title: t('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}/>
|
||||
},
|
||||
files: {
|
||||
|
|
|
@ -295,7 +295,7 @@ defaultRoles:
|
|||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
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]
|
||||
reportTemplate: [view, edit, delete, share, execute]
|
||||
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]
|
||||
customForm: [view, edit, delete, share]
|
||||
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]
|
||||
reportTemplate: [view, share, execute]
|
||||
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
|
@ -323,7 +323,7 @@ defaultRoles:
|
|||
children:
|
||||
sendConfiguration: [viewPublic]
|
||||
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]
|
||||
namespace: [view, createTemplate, createCampaign]
|
||||
|
||||
|
@ -385,7 +385,7 @@ defaultRoles:
|
|||
master:
|
||||
name: Master
|
||||
description: All permissions
|
||||
permissions: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
permissions: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
|
||||
viewer:
|
||||
name: Viewer
|
||||
description: The user can view the template but cannot edit it.
|
||||
|
|
|
@ -1062,6 +1062,11 @@ async function testSend(context, data) {
|
|||
|
||||
const list = await lists.getByCidTx(tx, context, data.listCid);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue