Improved files to distinguish subtypes (allows multiple different files tabs at a entity)
Attachments via the improved files Block thumbnails at mosaico templates as a separate files tab Some fixes All not tested yet
This commit is contained in:
parent
ade0fc87f2
commit
32cad03f4f
32 changed files with 683 additions and 346 deletions
|
@ -34,7 +34,6 @@ import {
|
||||||
import {DeleteModalDialog} from "../lib/modals";
|
import {DeleteModalDialog} from "../lib/modals";
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
import {
|
import {
|
||||||
getEditForm,
|
|
||||||
getTemplateTypes,
|
getTemplateTypes,
|
||||||
getTypeForm
|
getTypeForm
|
||||||
} from '../templates/helpers';
|
} from '../templates/helpers';
|
||||||
|
@ -43,11 +42,13 @@ import styles from "../lib/styles.scss";
|
||||||
import {getUrl} from "../lib/urls";
|
import {getUrl} from "../lib/urls";
|
||||||
import {
|
import {
|
||||||
CampaignSource,
|
CampaignSource,
|
||||||
|
CampaignStatus,
|
||||||
CampaignType
|
CampaignType
|
||||||
} from "../../../shared/campaigns";
|
} from "../../../shared/campaigns";
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {getMailerTypes} from "../send-configurations/helpers";
|
import {getMailerTypes} from "../send-configurations/helpers";
|
||||||
import {ResourceType} from "../lib/mosaico";
|
import {ResourceType} from "../lib/mosaico";
|
||||||
|
import {getCampaignTypeLabels} from "./helpers";
|
||||||
|
|
||||||
const overridables = ['from_name', 'from_email', 'reply_to', 'subject'];
|
const overridables = ['from_name', 'from_email', 'reply_to', 'subject'];
|
||||||
|
|
||||||
|
@ -65,6 +66,8 @@ export default class CUD extends Component {
|
||||||
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN);
|
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN);
|
||||||
this.mailerTypes = getMailerTypes(props.t);
|
this.mailerTypes = getMailerTypes(props.t);
|
||||||
|
|
||||||
|
this.campaignTypes = getCampaignTypeLabels(t);
|
||||||
|
|
||||||
this.createTitles = {
|
this.createTitles = {
|
||||||
[CampaignType.REGULAR]: t('Create Regular Campaign'),
|
[CampaignType.REGULAR]: t('Create Regular Campaign'),
|
||||||
[CampaignType.RSS]: t('Create RSS Campaign'),
|
[CampaignType.RSS]: t('Create RSS Campaign'),
|
||||||
|
@ -79,7 +82,8 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
this.sourceLabels = {
|
this.sourceLabels = {
|
||||||
[CampaignSource.TEMPLATE]: t('Template'),
|
[CampaignSource.TEMPLATE]: t('Template'),
|
||||||
[CampaignSource.CUSTOM_FROM_TEMPLATE]: t('Custom content'),
|
[CampaignSource.CUSTOM_FROM_TEMPLATE]: t('Custom content cloned from template'),
|
||||||
|
[CampaignSource.CUSTOM_FROM_CAMPAIGN]: t('Custom content cloned from another campaign'),
|
||||||
[CampaignSource.CUSTOM]: t('Custom content'),
|
[CampaignSource.CUSTOM]: t('Custom content'),
|
||||||
[CampaignSource.URL]: t('URL')
|
[CampaignSource.URL]: t('URL')
|
||||||
};
|
};
|
||||||
|
@ -95,8 +99,6 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showMergeTagReference: false,
|
|
||||||
elementInFullscreen: false,
|
|
||||||
sendConfiguration: null
|
sendConfiguration: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -112,7 +114,6 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
action: PropTypes.string.isRequired,
|
action: PropTypes.string.isRequired,
|
||||||
wizard: PropTypes.string,
|
|
||||||
entity: PropTypes.object,
|
entity: PropTypes.object,
|
||||||
type: PropTypes.number
|
type: PropTypes.number
|
||||||
}
|
}
|
||||||
|
@ -144,37 +145,17 @@ export default class CUD extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.entity) {
|
if (this.props.entity) {
|
||||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||||
if (data.source === CampaignSource.TEMPLATE || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
// The source cannot be changed once campaign is created. Thus we don't have to initialize fields for all other sources
|
||||||
|
if (data.source === CampaignSource.TEMPLATE) {
|
||||||
data.data_sourceTemplate = data.data.sourceTemplate;
|
data.data_sourceTemplate = data.data.sourceTemplate;
|
||||||
} else {
|
|
||||||
data.data_sourceTemplate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
|
||||||
data.data_sourceCustom_type = data.data.sourceCustom.type;
|
|
||||||
data.data_sourceCustom_data = data.data.sourceCustom.data;
|
|
||||||
data.data_sourceCustom_html = data.data.sourceCustom.html;
|
|
||||||
data.data_sourceCustom_text = data.data.sourceCustom.text;
|
|
||||||
|
|
||||||
this.templateTypes[data.data.sourceCustom.type].afterLoad(data);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
data.data_sourceCustom_type = null;
|
|
||||||
data.data_sourceCustom_data = {};
|
|
||||||
data.data_sourceCustom_html = '';
|
|
||||||
data.data_sourceCustom_text = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.source === CampaignSource.URL) {
|
if (data.source === CampaignSource.URL) {
|
||||||
data.data_sourceUrl = data.data.sourceUrl;
|
data.data_sourceUrl = data.data.sourceUrl;
|
||||||
} else {
|
|
||||||
data.data_sourceUrl = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === CampaignType.RSS) {
|
if (data.type === CampaignType.RSS) {
|
||||||
data.data_feedUrl = data.data.feedUrl;
|
data.data_feedUrl = data.data.feedUrl;
|
||||||
} else {
|
|
||||||
data.data_feedUrl = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.useSegmentation = !!data.segment;
|
data.useSegmentation = !!data.segment;
|
||||||
|
@ -211,11 +192,14 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
source: CampaignSource.TEMPLATE,
|
source: CampaignSource.TEMPLATE,
|
||||||
|
|
||||||
// This is for CampaignSource.TEMPLATE
|
// This is for CampaignSource.TEMPLATE and CampaignSource.CUSTOM_FROM_TEMPLATE
|
||||||
data_sourceTemplate: null,
|
data_sourceTemplate: null,
|
||||||
|
|
||||||
|
// This is for CampaignSource.CUSTOM_FROM_CAMPAIGN
|
||||||
|
data_sourceCampaign: null,
|
||||||
|
|
||||||
// This is for CampaignSource.CUSTOM
|
// This is for CampaignSource.CUSTOM
|
||||||
data_sourceCustom_type: null,
|
data_sourceCustom_type: mailtrainConfig.editors[0],
|
||||||
data_sourceCustom_data: {},
|
data_sourceCustom_data: {},
|
||||||
data_sourceCustom_html: '',
|
data_sourceCustom_html: '',
|
||||||
data_sourceCustom_text: '',
|
data_sourceCustom_text: '',
|
||||||
|
@ -252,6 +236,12 @@ export default class CUD extends Component {
|
||||||
state.setIn(['segment', 'error'], null);
|
state.setIn(['segment', 'error'], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.getIn(['send_configuration', 'value'])) {
|
||||||
|
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['send_configuration', 'error'], null);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) {
|
if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) {
|
||||||
state.setIn(['from_email_override', 'error'], t('"From" email must not be empty'));
|
state.setIn(['from_email_override', 'error'], t('"From" email must not be empty'));
|
||||||
} else {
|
} else {
|
||||||
|
@ -261,22 +251,29 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
const campaignTypeKey = state.getIn(['type', 'value']);
|
const campaignTypeKey = state.getIn(['type', 'value']);
|
||||||
|
|
||||||
const sourceTypeKey = state.getIn(['source', 'value']);
|
const sourceTypeKey = Number.parseInt(state.getIn(['source', 'value']));
|
||||||
|
|
||||||
|
for (const key of state.keys()) {
|
||||||
|
if (key.startsWith('data_')) {
|
||||||
|
state.setIn([key, 'error'], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceTypeKey === CampaignSource.TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
if (sourceTypeKey === CampaignSource.TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||||
if (!state.getIn(['data_sourceTemplate', 'value'])) {
|
if (!state.getIn(['data_sourceTemplate', 'value'])) {
|
||||||
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
|
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
|
||||||
} else {
|
}
|
||||||
state.setIn(['data_sourceTemplate', 'error'], null);
|
|
||||||
|
} else if (sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||||
|
if (!state.getIn(['data_sourceCampaign', 'value'])) {
|
||||||
|
state.setIn(['data_sourceCampaign', 'error'], t('Campaign must be selected'));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (sourceTypeKey === CampaignSource.CUSTOM) {
|
} else if (sourceTypeKey === CampaignSource.CUSTOM) {
|
||||||
// The type is used only in create form. In case of CUSTOM_FROM_TEMPLATE, it is determined by the source template, so no need to check it here
|
// The type is used only in create form. In case of CUSTOM_FROM_TEMPLATE or CUSTOM_FROM_CAMPAIGN, it is determined by the source template, so no need to check it here
|
||||||
const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
|
const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
|
||||||
if (!customTemplateTypeKey) {
|
if (!customTemplateTypeKey) {
|
||||||
state.setIn(['data_sourceCustom_type', 'error'], t('Type must be selected'));
|
state.setIn(['data_sourceCustom_type', 'error'], t('Type must be selected'));
|
||||||
} else {
|
|
||||||
state.setIn(['data_sourceCustom_type', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customTemplateTypeKey) {
|
if (customTemplateTypeKey) {
|
||||||
|
@ -286,16 +283,12 @@ export default class CUD extends Component {
|
||||||
} else if (sourceTypeKey === CampaignSource.URL) {
|
} else if (sourceTypeKey === CampaignSource.URL) {
|
||||||
if (!state.getIn(['data_sourceUrl', 'value'])) {
|
if (!state.getIn(['data_sourceUrl', 'value'])) {
|
||||||
state.setIn(['data_sourceUrl', 'error'], t('URL must not be empty'));
|
state.setIn(['data_sourceUrl', 'error'], t('URL must not be empty'));
|
||||||
} else {
|
|
||||||
state.setIn(['data_sourceUrl', 'error'], null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (campaignTypeKey === CampaignType.RSS) {
|
if (campaignTypeKey === CampaignType.RSS) {
|
||||||
if (!state.getIn(['data_feedUrl', 'value'])) {
|
if (!state.getIn(['data_feedUrl', 'value'])) {
|
||||||
state.setIn(['data_feedUrl', 'error'], t('RSS feed URL must be given'));
|
state.setIn(['data_feedUrl', 'error'], t('RSS feed URL must be given'));
|
||||||
} else {
|
|
||||||
state.setIn(['data_feedUrl', 'error'], null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,14 +299,6 @@ export default class CUD extends Component {
|
||||||
async submitHandler() {
|
async submitHandler() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
if (this.props.entity) {
|
|
||||||
const sourceTypeKey = this.getFormValue('source');
|
|
||||||
if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
|
||||||
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
|
||||||
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sendMethod, url;
|
let sendMethod, url;
|
||||||
if (this.props.entity) {
|
if (this.props.entity) {
|
||||||
sendMethod = FormSendMethod.PUT;
|
sendMethod = FormSendMethod.PUT;
|
||||||
|
@ -337,7 +322,11 @@ export default class CUD extends Component {
|
||||||
data.data.sourceTemplate = data.data_sourceTemplate;
|
data.data.sourceTemplate = data.data_sourceTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
if (data.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||||
|
data.data.sourceCampaign = data.data_sourceCampaign;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.source === CampaignSource.CUSTOM) {
|
||||||
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
|
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
|
||||||
|
|
||||||
data.data.sourceCustom = {
|
data.data.sourceCustom = {
|
||||||
|
@ -382,116 +371,18 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractPlainText() {
|
|
||||||
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
|
||||||
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
|
||||||
|
|
||||||
const html = this.getFormValue('data_sourceCustom_html');
|
|
||||||
if (!html) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isFormDisabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.disableForm();
|
|
||||||
|
|
||||||
console.log(html);
|
|
||||||
|
|
||||||
const response = await axios.post(getUrl('rest/html-to-text', { html }));
|
|
||||||
|
|
||||||
this.updateFormValue('data_sourceCustom_text', response.data.text);
|
|
||||||
|
|
||||||
this.enableForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggleMergeTagReference() {
|
|
||||||
this.setState({
|
|
||||||
showMergeTagReference: !this.state.showMergeTagReference
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setElementInFullscreen(elementInFullscreen) {
|
|
||||||
this.setState({
|
|
||||||
elementInFullscreen
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||||
|
|
||||||
let templateEdit = null;
|
|
||||||
let extraSettings = null;
|
let extraSettings = null;
|
||||||
|
|
||||||
const sourceTypeKey = this.getFormValue('source');
|
const sourceTypeKey = Number.parseInt(this.getFormValue('source'));
|
||||||
const campaignTypeKey = this.getFormValue('type');
|
const campaignTypeKey = this.getFormValue('type');
|
||||||
|
|
||||||
|
|
||||||
let sourceEdit;
|
|
||||||
if (isEdit) {
|
|
||||||
sourceEdit = <StaticField id="source" className={styles.formDisabled} label={t('Content source')}>{this.sourceLabels[sourceTypeKey]}</StaticField>;
|
|
||||||
} else {
|
|
||||||
sourceEdit = <Dropdown id="source" label={t('Content source')} options={this.sourceOptions}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
|
|
||||||
const templatesColumns = [
|
|
||||||
{ data: 1, title: t('Name') },
|
|
||||||
{ data: 2, title: t('Description') },
|
|
||||||
{ data: 3, title: t('Type'), render: data => this.templateTypes[data].typeName },
|
|
||||||
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
|
|
||||||
{ data: 5, title: t('Namespace') },
|
|
||||||
];
|
|
||||||
|
|
||||||
let help = null;
|
|
||||||
if (sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
|
||||||
help = t('Selecting a template creates a campaign specific copy from it.');
|
|
||||||
}
|
|
||||||
|
|
||||||
templateEdit = <TableSelect id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
|
|
||||||
|
|
||||||
} else if (sourceTypeKey === CampaignSource.CUSTOM || (isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
|
|
||||||
// TODO: Toggle HTML preview
|
|
||||||
|
|
||||||
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
|
||||||
|
|
||||||
let customTemplateEditForm = null;
|
|
||||||
let customTemplateTypeForm = null;
|
|
||||||
|
|
||||||
if (customTemplateTypeKey) {
|
|
||||||
customTemplateTypeForm = getTypeForm(this, customTemplateTypeKey, isEdit);
|
|
||||||
|
|
||||||
if (isEdit) {
|
|
||||||
customTemplateEditForm = getEditForm(this, customTemplateTypeKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templateEdit = <div>
|
|
||||||
{isEdit
|
|
||||||
?
|
|
||||||
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('Custom template editor')}>
|
|
||||||
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
|
|
||||||
</StaticField>
|
|
||||||
:
|
|
||||||
<Dropdown id="data_sourceCustom_type" label={t('Type')} options={this.customTemplateTypeOptions}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{customTemplateTypeForm}
|
|
||||||
|
|
||||||
{customTemplateEditForm}
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
} else if (sourceTypeKey === CampaignSource.URL) {
|
|
||||||
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (campaignTypeKey === CampaignType.RSS) {
|
if (campaignTypeKey === CampaignType.RSS) {
|
||||||
extraSettings = <InputField id="data_feedUrl" label={t('RSS Feed Url')}/>
|
extraSettings = <InputField id="data_feedUrl" label={t('RSS Feed Url')}/>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const listsColumns = [
|
const listsColumns = [
|
||||||
|
@ -545,8 +436,64 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let sourceEdit = null;
|
||||||
|
if (isEdit) {
|
||||||
|
if (!(sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
|
||||||
|
sourceEdit = <StaticField id="source" className={styles.formDisabled} label={t('Content source')}>{this.sourceLabels[sourceTypeKey]}</StaticField>;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sourceEdit = <Dropdown id="source" label={t('Content source')} options={this.sourceOptions}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
let templateEdit = null;
|
||||||
|
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
|
||||||
|
const templatesColumns = [
|
||||||
|
{ data: 1, title: t('Name') },
|
||||||
|
{ data: 2, title: t('Description') },
|
||||||
|
{ data: 3, title: t('Type'), render: data => this.templateTypes[data].typeName },
|
||||||
|
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
|
||||||
|
{ data: 5, title: t('Namespace') },
|
||||||
|
];
|
||||||
|
|
||||||
|
let help = null;
|
||||||
|
if (sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||||
|
help = t('Selecting a template creates a campaign specific copy from it.');
|
||||||
|
}
|
||||||
|
|
||||||
|
templateEdit = <TableSelect id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
|
||||||
|
|
||||||
|
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||||
|
const campaignsColumns = [
|
||||||
|
{ data: 1, title: t('Name') },
|
||||||
|
{ data: 2, title: t('Description') },
|
||||||
|
{ data: 3, title: t('Type'), render: data => this.campaignTypes[data] },
|
||||||
|
{ data: 7, title: t('Created'), render: data => moment(data).fromNow() },
|
||||||
|
{ data: 8, title: t('Namespace') }
|
||||||
|
];
|
||||||
|
|
||||||
|
templateEdit = <TableSelect id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>;
|
||||||
|
|
||||||
|
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) {
|
||||||
|
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||||
|
|
||||||
|
let customTemplateTypeForm = null;
|
||||||
|
|
||||||
|
if (customTemplateTypeKey) {
|
||||||
|
customTemplateTypeForm = getTypeForm(this, customTemplateTypeKey, isEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
templateEdit = <div>
|
||||||
|
<Dropdown id="data_sourceCustom_type" label={t('Type')} options={this.customTemplateTypeOptions}/>
|
||||||
|
{customTemplateTypeForm}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
} else if (sourceTypeKey === CampaignSource.URL) {
|
||||||
|
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
<div>
|
||||||
{canDelete &&
|
{canDelete &&
|
||||||
<DeleteModalDialog
|
<DeleteModalDialog
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
|
@ -588,7 +535,7 @@ export default class CUD extends Component {
|
||||||
<CheckBox id="open_trackings_disabled" text={t('Disable opened tracking')}/>
|
<CheckBox id="open_trackings_disabled" text={t('Disable opened tracking')}/>
|
||||||
<CheckBox id="click_tracking_disabled" text={t('Disable clicked tracking')}/>
|
<CheckBox id="click_tracking_disabled" text={t('Disable clicked tracking')}/>
|
||||||
|
|
||||||
<hr/>
|
{sourceEdit && <hr/> }
|
||||||
|
|
||||||
{sourceEdit}
|
{sourceEdit}
|
||||||
|
|
||||||
|
|
195
client/src/campaigns/Content.js
Normal file
195
client/src/campaigns/Content.js
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {translate} from 'react-i18next';
|
||||||
|
import {
|
||||||
|
requiresAuthenticatedUser,
|
||||||
|
Title,
|
||||||
|
withPageHelpers
|
||||||
|
} from '../lib/page'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonRow,
|
||||||
|
Form,
|
||||||
|
FormSendMethod,
|
||||||
|
StaticField,
|
||||||
|
withForm
|
||||||
|
} from '../lib/form';
|
||||||
|
import {withErrorHandling} from '../lib/error-handling';
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
import {
|
||||||
|
getEditForm,
|
||||||
|
getTemplateTypes,
|
||||||
|
getTypeForm
|
||||||
|
} from '../templates/helpers';
|
||||||
|
import axios from '../lib/axios';
|
||||||
|
import styles from "../lib/styles.scss";
|
||||||
|
import {getUrl} from "../lib/urls";
|
||||||
|
import {ResourceType} from "../lib/mosaico";
|
||||||
|
|
||||||
|
const overridables = ['from_name', 'from_email', 'reply_to', 'subject'];
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withForm
|
||||||
|
@withPageHelpers
|
||||||
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
|
export default class CustomContent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const t = props.t;
|
||||||
|
|
||||||
|
console.log(props);
|
||||||
|
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN);
|
||||||
|
|
||||||
|
this.customTemplateTypeOptions = [];
|
||||||
|
for (const key of mailtrainConfig.editors) {
|
||||||
|
this.customTemplateTypeOptions.push({key, label: this.templateTypes[key].typeName});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showMergeTagReference: false,
|
||||||
|
elementInFullscreen: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.initForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
entity: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||||
|
data.data_sourceCustom_type = data.data.sourceCustom.type;
|
||||||
|
data.data_sourceCustom_data = data.data.sourceCustom.data;
|
||||||
|
data.data_sourceCustom_html = data.data.sourceCustom.html;
|
||||||
|
data.data_sourceCustom_text = data.data.sourceCustom.text;
|
||||||
|
|
||||||
|
this.templateTypes[data.data.sourceCustom.type].afterLoad(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localValidateFormValues(state) {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
|
||||||
|
|
||||||
|
if (customTemplateTypeKey) {
|
||||||
|
this.templateTypes[customTemplateTypeKey].validate(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitHandler() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||||
|
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
||||||
|
|
||||||
|
let sendMethod, url;
|
||||||
|
if (this.props.entity) {
|
||||||
|
sendMethod = FormSendMethod.PUT;
|
||||||
|
url = `rest/campaigns/${this.props.entity.id}`
|
||||||
|
} else {
|
||||||
|
sendMethod = FormSendMethod.POST;
|
||||||
|
url = 'rest/campaigns'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disableForm();
|
||||||
|
this.setFormStatusMessage('info', t('Saving ...'));
|
||||||
|
|
||||||
|
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||||
|
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
|
||||||
|
|
||||||
|
data.data.sourceCustom = {
|
||||||
|
type: data.data_sourceCustom_type,
|
||||||
|
data: data.data_sourceCustom_data,
|
||||||
|
html: data.data_sourceCustom_html,
|
||||||
|
text: data.data_sourceCustom_text
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (key.startsWith('data_')) {
|
||||||
|
delete data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (submitResponse) {
|
||||||
|
if (this.props.entity) {
|
||||||
|
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
|
||||||
|
} else {
|
||||||
|
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.enableForm();
|
||||||
|
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractPlainText() {
|
||||||
|
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||||
|
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
||||||
|
|
||||||
|
const html = this.getFormValue('data_sourceCustom_html');
|
||||||
|
if (!html) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isFormDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disableForm();
|
||||||
|
|
||||||
|
const response = await axios.post(getUrl('rest/html-to-text', { html }));
|
||||||
|
|
||||||
|
this.updateFormValue('data_sourceCustom_text', response.data.text);
|
||||||
|
|
||||||
|
this.enableForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleMergeTagReference() {
|
||||||
|
this.setState({
|
||||||
|
showMergeTagReference: !this.state.showMergeTagReference
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setElementInFullscreen(elementInFullscreen) {
|
||||||
|
this.setState({
|
||||||
|
elementInFullscreen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
// TODO: Toggle HTML preview
|
||||||
|
|
||||||
|
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||||
|
|
||||||
|
// FIXME - data_sourceCustom_type is initialized only after first render
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
||||||
|
<Title>{t('Edit Custom Content')}</Title>
|
||||||
|
|
||||||
|
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||||
|
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('Custom template editor')}>
|
||||||
|
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
|
||||||
|
</StaticField>
|
||||||
|
|
||||||
|
{customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)}
|
||||||
|
|
||||||
|
{customTemplateTypeKey && getEditForm(this, customTemplateTypeKey)}
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
|
</ButtonRow>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import {
|
||||||
CampaignType
|
CampaignType
|
||||||
} from "../../../shared/campaigns";
|
} from "../../../shared/campaigns";
|
||||||
import {checkPermissions} from "../lib/permissions";
|
import {checkPermissions} from "../lib/permissions";
|
||||||
|
import {getCampaignTypeLabels} from "./helpers";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
|
@ -44,11 +45,7 @@ export default class List extends Component {
|
||||||
[CampaignStatus.ACTIVE]: t('Active')
|
[CampaignStatus.ACTIVE]: t('Active')
|
||||||
};
|
};
|
||||||
|
|
||||||
this.campaignTypes = {
|
this.campaignTypes = getCampaignTypeLabels(t);
|
||||||
[CampaignType.REGULAR]: t('Regular'),
|
|
||||||
[CampaignType.TRIGGERED]: t('Triggered'),
|
|
||||||
[CampaignType.RSS]: t('RSS')
|
|
||||||
};
|
|
||||||
|
|
||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
@ -109,14 +106,26 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (perms.includes('manageFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
|
if (perms.includes('edit') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="align-center" title={t('Content')}/>,
|
||||||
|
link: `/campaigns/${data[0]}/content`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (perms.includes('viewFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="hdd" title={t('Files')}/>,
|
label: <Icon icon="hdd" title={t('Files')}/>,
|
||||||
link: `/campaigns/${data[0]}/files`
|
link: `/campaigns/${data[0]}/files`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: add attachments
|
if (perms.includes('viewAttachments')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="paperclip" title={t('Attachments')}/>,
|
||||||
|
link: `/campaigns/${data[0]}/attachments`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (perms.includes('share')) {
|
if (perms.includes('share')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
|
|
14
client/src/campaigns/helpers.js
Normal file
14
client/src/campaigns/helpers.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {CampaignType} from "../../../shared/campaigns";
|
||||||
|
|
||||||
|
export function getCampaignTypeLabels(t) {
|
||||||
|
|
||||||
|
const campaignTypeLabels = {
|
||||||
|
[CampaignType.REGULAR]: t('Regular'),
|
||||||
|
[CampaignType.TRIGGERED]: t('Triggered'),
|
||||||
|
[CampaignType.RSS]: t('RSS')
|
||||||
|
};
|
||||||
|
|
||||||
|
return campaignTypeLabels;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import CampaignsCUD from './CUD';
|
import CampaignsCUD from './CUD';
|
||||||
|
import Content from './Content';
|
||||||
import CampaignsList from './List';
|
import CampaignsList from './List';
|
||||||
import Share from '../shares/Share';
|
import Share from '../shares/Share';
|
||||||
import Files from "../lib/files";
|
import Files from "../lib/files";
|
||||||
|
@ -29,13 +30,24 @@ function getMenus(t) {
|
||||||
visible: resolved => resolved.campaign.permissions.includes('edit'),
|
visible: resolved => resolved.campaign.permissions.includes('edit'),
|
||||||
panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} />
|
panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} />
|
||||||
},
|
},
|
||||||
|
content: {
|
||||||
|
title: t('Content'),
|
||||||
|
link: params => `/campaigns/${params.campaignId}/content`,
|
||||||
|
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),
|
||||||
|
panelRender: props => <Content entity={props.resolved.campaign} />
|
||||||
|
},
|
||||||
files: {
|
files: {
|
||||||
title: t('Files'),
|
title: t('Files'),
|
||||||
link: params => `/campaigns/${params.campaignId}/files`,
|
link: params => `/campaigns/${params.campaignId}/files`,
|
||||||
visible: resolved => resolved.campaign.permissions.includes('edit') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE),
|
visible: resolved => resolved.campaign.permissions.includes('viewFiles') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
|
||||||
panelRender: props => <Files title={t('Files')} entity={props.resolved.campaign} entityTypeId="campaign" />
|
panelRender: props => <Files title={t('Files')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="file" managePermission="manageFiles"/>
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
title: t('Attachments'),
|
||||||
|
link: params => `/campaigns/${params.campaignId}/attachments`,
|
||||||
|
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
|
||||||
|
panelRender: props => <Files title={t('Attachments')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
|
||||||
},
|
},
|
||||||
// FIXME: add attachments
|
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/campaigns/${params.campaignId}/share`,
|
link: params => `/campaigns/${params.campaignId}/share`,
|
||||||
|
|
|
@ -32,8 +32,10 @@ export default class Files extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
entity: PropTypes.object,
|
entity: PropTypes.object.isRequired,
|
||||||
entityTypeId: PropTypes.string,
|
entityTypeId: PropTypes.string.isRequired,
|
||||||
|
entitySubTypeId: PropTypes.string.isRequired,
|
||||||
|
managePermission: PropTypes.string.isRequired,
|
||||||
usePublicDownloadUrls: PropTypes.bool
|
usePublicDownloadUrls: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +67,7 @@ export default class Files extends Component {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
data.append('files[]', file)
|
data.append('files[]', file)
|
||||||
}
|
}
|
||||||
axios.post(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entity.id}`), data)
|
axios.post(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}`), data)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.filesTable.refresh();
|
this.filesTable.refresh();
|
||||||
const message = this.getFilesUploadedMessage(res);
|
const message = this.getFilesUploadedMessage(res);
|
||||||
|
@ -93,7 +95,7 @@ export default class Files extends Component {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.setFlashMessage('info', t('Deleting file ...'));
|
this.setFlashMessage('info', t('Deleting file ...'));
|
||||||
await axios.delete(getUrl(`rest/files/${this.props.entityTypeId}/${fileToDeleteId}`));
|
await axios.delete(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${fileToDeleteId}`));
|
||||||
this.filesTable.refresh();
|
this.filesTable.refresh();
|
||||||
this.setFlashMessage('info', t('File deleted'));
|
this.setFlashMessage('info', t('File deleted'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -110,24 +112,26 @@ export default class Files extends Component {
|
||||||
{ data: 3, title: "Size" },
|
{ data: 3, title: "Size" },
|
||||||
{
|
{
|
||||||
actions: data => {
|
actions: data => {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
let downloadUrl;
|
let downloadUrl;
|
||||||
if (this.props.usePublicDownloadUrls) {
|
if (this.props.usePublicDownloadUrls) {
|
||||||
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entity.id}/${data[2]}`;
|
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}/${data[2]}`;
|
||||||
} else {
|
} else {
|
||||||
downloadUrl =`rest/files/${this.props.entityTypeId}/${data[0]}`;
|
downloadUrl =`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${data[0]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = [
|
actions.push({
|
||||||
{
|
label: <Icon icon="download" title={t('Download')}/>,
|
||||||
label: <Icon icon="download" title={t('Download')}/>,
|
href: downloadUrl
|
||||||
href: downloadUrl
|
});
|
||||||
},
|
|
||||||
{
|
if (this.props.entity.permissions.includes(this.props.managePermission)) {
|
||||||
|
actions.push({
|
||||||
label: <Icon icon="remove" title={t('Delete')}/>,
|
label: <Icon icon="remove" title={t('Delete')}/>,
|
||||||
action: () => this.deleteFile(data[0], data[1])
|
action: () => this.deleteFile(data[0], data[1])
|
||||||
}
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
@ -146,10 +150,15 @@ export default class Files extends Component {
|
||||||
]}>
|
]}>
|
||||||
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
|
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
|
|
||||||
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
{
|
||||||
</Dropzone>
|
this.props.entity.permissions.includes(this.props.managePermission) &&
|
||||||
<Table withHeader ref={node => this.filesTable = node} dataUrl={`rest/files-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={columns} />
|
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
|
||||||
|
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
||||||
|
</Dropzone>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Table withHeader ref={node => this.filesTable = node} dataUrl={`rest/files-table/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}`} columns={columns} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,10 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonRow {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonRow > * {
|
.buttonRow > * {
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (perms.includes('manageFiles')) {
|
if (perms.includes('viewFiles')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="hdd" title={t('Files')}/>,
|
label: <Icon icon="hdd" title={t('Files')}/>,
|
||||||
link: `/templates/${data[0]}/files`
|
link: `/templates/${data[0]}/files`
|
||||||
|
|
|
@ -323,8 +323,6 @@ export function getEditForm(owner, typeKey, prefix = '') {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTypeForm(owner, typeKey, isEdit) {
|
export function getTypeForm(owner, typeKey, isEdit) {
|
||||||
return <div>
|
return owner.templateTypes[typeKey].getTypeForm(owner, isEdit);
|
||||||
{owner.templateTypes[typeKey].getTypeForm(owner, isEdit)}
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,20 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (perms.includes('viewFiles')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="hdd" title={t('Files')}/>,
|
||||||
|
link: `/templates/mosaico/${data[0]}/files`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (perms.includes('viewFiles')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="th-large" title={t('Block thumbnails')}/>,
|
||||||
|
link: `/templates/mosaico/${data[0]}/blocks`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (perms.includes('share')) {
|
if (perms.includes('share')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||||
|
|
|
@ -33,8 +33,8 @@ function getMenus(t) {
|
||||||
files: {
|
files: {
|
||||||
title: t('Files'),
|
title: t('Files'),
|
||||||
link: params => `/templates/${params.templateId}/files`,
|
link: params => `/templates/${params.templateId}/files`,
|
||||||
visible: resolved => resolved.template.permissions.includes('edit'),
|
visible: resolved => resolved.template.permissions.includes('viewFiles'),
|
||||||
panelRender: props => <Files title={t('Files')} entity={props.resolved.template} entityTypeId="template" />
|
panelRender: props => <Files title={t('Files')} entity={props.resolved.template} entityTypeId="template" entitySubTypeId="file" managePermission="manageFiles"/>
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
|
@ -69,8 +69,14 @@ function getMenus(t) {
|
||||||
files: {
|
files: {
|
||||||
title: t('Files'),
|
title: t('Files'),
|
||||||
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`,
|
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`,
|
||||||
visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'),
|
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
|
||||||
panelRender: props => <Files title={t('Files')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" />
|
panelRender: props => <Files title={t('Files')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="file" managePermission="manageFiles" />
|
||||||
|
},
|
||||||
|
blocks: {
|
||||||
|
title: t('Block thumbnails'),
|
||||||
|
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/blocks`,
|
||||||
|
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
|
||||||
|
panelRender: props => <Files title={t('Block thumbnails')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="block" managePermission="manageFiles" />
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
|
|
|
@ -10,9 +10,9 @@ const multer = require('multer')({
|
||||||
dest: uploadedFilesDir
|
dest: uploadedFilesDir
|
||||||
});
|
});
|
||||||
|
|
||||||
function installUploadHandler(router, url, getUrl = null, dontReplace = false) {
|
function installUploadHandler(router, url, replacementBehavior, type = null, subType = null) {
|
||||||
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
|
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
|
||||||
return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, getUrl, dontReplace));
|
return res.json(await files.createFiles(req.context, type || req.params.type, subType || req.params.subType, req.params.entityId, req.files, replacementBehavior));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,36 @@ const entityTypes = {
|
||||||
entitiesTable: 'campaigns',
|
entitiesTable: 'campaigns',
|
||||||
sharesTable: 'shares_campaign',
|
sharesTable: 'shares_campaign',
|
||||||
permissionsTable: 'permissions_campaign',
|
permissionsTable: 'permissions_campaign',
|
||||||
filesTable: 'files_campaign'
|
files: {
|
||||||
|
file: {
|
||||||
|
table: 'files_campaign_file',
|
||||||
|
permissions: {
|
||||||
|
view: 'viewFiles',
|
||||||
|
manage: 'manageFiles'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attachment: {
|
||||||
|
table: 'files_campaign_attachment',
|
||||||
|
permissions: {
|
||||||
|
view: 'viewAttachments',
|
||||||
|
manage: 'manageAttachments'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
entitiesTable: 'templates',
|
entitiesTable: 'templates',
|
||||||
sharesTable: 'shares_template',
|
sharesTable: 'shares_template',
|
||||||
permissionsTable: 'permissions_template',
|
permissionsTable: 'permissions_template',
|
||||||
filesTable: 'files_template'
|
files: {
|
||||||
|
file: {
|
||||||
|
table: 'files_template_file',
|
||||||
|
permissions: {
|
||||||
|
view: 'viewFiles',
|
||||||
|
manage: 'manageFiles'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
sendConfiguration: {
|
sendConfiguration: {
|
||||||
entitiesTable: 'send_configurations',
|
entitiesTable: 'send_configurations',
|
||||||
|
@ -47,7 +70,22 @@ const entityTypes = {
|
||||||
entitiesTable: 'mosaico_templates',
|
entitiesTable: 'mosaico_templates',
|
||||||
sharesTable: 'shares_mosaico_template',
|
sharesTable: 'shares_mosaico_template',
|
||||||
permissionsTable: 'permissions_mosaico_template',
|
permissionsTable: 'permissions_mosaico_template',
|
||||||
filesTable: 'files_mosaico_template'
|
files: {
|
||||||
|
file: {
|
||||||
|
table: 'files_mosaico_template_file',
|
||||||
|
permissions: {
|
||||||
|
view: 'viewFiles',
|
||||||
|
manage: 'manageFiles'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
table: 'files_mosaico_template_block',
|
||||||
|
permissions: {
|
||||||
|
view: 'viewFiles',
|
||||||
|
manage: 'manageFiles'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ module.exports.start = async (reportId) => {
|
||||||
if (!workers[reportId]) {
|
if (!workers[reportId]) {
|
||||||
log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
|
log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
|
||||||
await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, last_run: null});
|
await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, last_run: null});
|
||||||
tryStartWorkers();
|
await tryStartWorkers();
|
||||||
} else {
|
} else {
|
||||||
log.info('ReportProcessor', 'Worker for report id: %s is already running.', reportId);
|
log.info('ReportProcessor', 'Worker for report id: %s is already running.', reportId);
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ module.exports.stop = async reportId => {
|
||||||
module.exports.init = async () => {
|
module.exports.init = async () => {
|
||||||
try {
|
try {
|
||||||
await reports.bulkChangeState(reports.ReportState.PROCESSING, reports.ReportState.SCHEDULED);
|
await reports.bulkChangeState(reports.ReportState.PROCESSING, reports.ReportState.SCHEDULED);
|
||||||
tryStartWorkers();
|
await tryStartWorkers();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('ReportProcessor', err);
|
log.error('ReportProcessor', err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ const { enforce, filterObject } = require('../lib/helpers');
|
||||||
const shares = require('./shares');
|
const shares = require('./shares');
|
||||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||||
const files = require('./files');
|
const files = require('./files');
|
||||||
|
const templates = require('./templates');
|
||||||
const { CampaignSource, CampaignType} = require('../shared/campaigns');
|
const { CampaignSource, CampaignType} = require('../shared/campaigns');
|
||||||
const segments = require('./segments');
|
const segments = require('./segments');
|
||||||
|
|
||||||
|
@ -33,16 +34,22 @@ async function listDTAjax(context, params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getById(context, id) {
|
async function getByIdTx(tx, context, id, withPermissions = true) {
|
||||||
return await knex.transaction(async tx => {
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
|
const entity = await tx('campaigns').where('id', id).first();
|
||||||
const entity = await tx('campaigns').where('id', id).first();
|
|
||||||
|
|
||||||
|
if (withPermissions) {
|
||||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id);
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id);
|
||||||
|
}
|
||||||
|
|
||||||
entity.data = JSON.parse(entity.data);
|
entity.data = JSON.parse(entity.data);
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getById(context, id, withPermissions = true) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
return await getByIdTx(tx, context, id, withPermissions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +57,7 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
|
||||||
await namespaceHelpers.validateEntity(tx, entity);
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
|
|
||||||
if (isCreate) {
|
if (isCreate) {
|
||||||
enforce(entity.type === CampaignType.REGULAR && entity.type === CampaignType.RSS && entity.type === CampaignType.TRIGGERED, 'Unknown campaign type');
|
enforce(entity.type === CampaignType.REGULAR || entity.type === CampaignType.RSS || entity.type === CampaignType.TRIGGERED, 'Unknown campaign type');
|
||||||
|
|
||||||
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||||
|
@ -66,6 +73,8 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
|
||||||
await segments.getByIdTx(tx, context, entity.list, entity.segment);
|
await segments.getByIdTx(tx, context, entity.list, entity.segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'send_configuration', entity.send_configuration, 'viewPublic');
|
||||||
|
|
||||||
entity.data = JSON.stringify(entity.data);
|
entity.data = JSON.stringify(entity.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,9 +82,30 @@ async function create(context, entity) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
|
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
|
||||||
|
|
||||||
let copyFilesFromTemplateId;
|
let copyFilesFrom = null;
|
||||||
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||||
copyFilesFromTemplateId = entity.data.sourceTemplate;
|
copyFilesFrom = {
|
||||||
|
entityType: 'template',
|
||||||
|
entityId: entity.data.sourceTemplate
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = await templates.getByIdTx(tx, context, entity.data.sourceTemplate, false);
|
||||||
|
|
||||||
|
entity.data.sourceCustom = {
|
||||||
|
type: template.type,
|
||||||
|
data: template.data,
|
||||||
|
html: template.html,
|
||||||
|
text: template.text
|
||||||
|
};
|
||||||
|
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||||
|
copyFilesFrom = {
|
||||||
|
entityType: 'campaign',
|
||||||
|
entityId: entity.data.sourceCampaign
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceCampaign = await getByIdTx(tx, context, entity.data.sourceCampaign, false);
|
||||||
|
|
||||||
|
entity.data.sourceCustom = sourceCampaign.data.sourceCustom;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, context, entity, true);
|
await _validateAndPreprocess(tx, context, entity, true);
|
||||||
|
@ -119,8 +149,8 @@ async function create(context, entity) {
|
||||||
|
|
||||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
||||||
|
|
||||||
if (copyFilesFromTemplateId) {
|
if (copyFilesFrom) {
|
||||||
files.copyAllTx(tx, context, 'template', copyFilesFromTemplateId, 'campaign', id);
|
await files.copyAllTx(tx, context, copyFilesFrom.entityType, copyFilesFrom.entityId, 'campaign', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
@ -168,6 +198,7 @@ async function remove(context, id) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hash,
|
hash,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
|
getByIdTx,
|
||||||
getById,
|
getById,
|
||||||
create,
|
create,
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
|
|
193
models/files.js
193
models/files.js
|
@ -10,49 +10,63 @@ const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
const permissions = require('../lib/permissions');
|
const permissions = require('../lib/permissions');
|
||||||
const {getTrustedUrl} = require('../lib/urls');
|
const {getTrustedUrl} = require('../lib/urls');
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const bluebird = require('bluebird');
|
||||||
|
const cryptoPseudoRandomBytes = bluebird.promisify(crypto.pseudoRandomBytes);
|
||||||
|
|
||||||
const entityTypes = permissions.getEntityTypes();
|
const entityTypes = permissions.getEntityTypes();
|
||||||
|
|
||||||
const filesDir = path.join(__dirname, '..', 'files');
|
const filesDir = path.join(__dirname, '..', 'files');
|
||||||
|
|
||||||
function enforceTypePermitted(type) {
|
const ReplacementBehavior = {
|
||||||
enforce(type in entityTypes && entityTypes[type].filesTable);
|
NONE: 0,
|
||||||
|
REPLACE: 1,
|
||||||
|
RENAME: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
function enforceTypePermitted(type, subType) {
|
||||||
|
enforce(type in entityTypes && entityTypes[type].files && entityTypes[type].files[subType]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilePath(type, entityId, filename) {
|
function getFilePath(type, subType, entityId, filename) {
|
||||||
return path.join(path.join(filesDir, type, entityId.toString()), filename);
|
return path.join(path.join(filesDir, type, subType, entityId.toString()), filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileUrl(context, type, entityId, filename) {
|
function getFileUrl(context, type, subType, entityId, filename) {
|
||||||
return getTrustedUrl(`files/${type}/${entityId}/${filename}`, context)
|
return getTrustedUrl(`files/${type}/${subType}/${entityId}/${filename}`, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilesTable(type) {
|
function getFilesTable(type, subType) {
|
||||||
return entityTypes[type].filesTable;
|
return entityTypes[type].files[subType].table;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listDTAjax(context, type, entityId, params) {
|
function getFilesPermission(type, subType, operation) {
|
||||||
enforceTypePermitted(type);
|
return entityTypes[type].files[subType].permissions[operation];
|
||||||
await shares.enforceEntityPermission(context, type, entityId, 'viewFiles');
|
}
|
||||||
|
|
||||||
|
async function listDTAjax(context, type, subType, entityId, params) {
|
||||||
|
enforceTypePermitted(type, subType);
|
||||||
|
await shares.enforceEntityPermission(context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||||
return await dtHelpers.ajaxList(
|
return await dtHelpers.ajaxList(
|
||||||
params,
|
params,
|
||||||
builder => builder.from(getFilesTable(type)).where({entity: entityId}),
|
builder => builder.from(getFilesTable(type, subType)).where({entity: entityId}),
|
||||||
['id', 'originalname', 'filename', 'size', 'created']
|
['id', 'originalname', 'filename', 'size', 'created']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function list(context, type, entityId) {
|
async function list(context, type, subType, entityId) {
|
||||||
enforceTypePermitted(type);
|
enforceTypePermitted(type, subType);
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'viewFiles');
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||||
return await tx(getFilesTable(type)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
return await tx(getFilesTable(type, subType)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileById(context, type, id) {
|
async function getFileById(context, type, subType, id) {
|
||||||
enforceTypePermitted(type);
|
enforceTypePermitted(type, subType);
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
const file = await tx(getFilesTable(type)).where('id', id).first();
|
const file = await tx(getFilesTable(type, subType)).where('id', id).first();
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'viewFiles');
|
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'view'));
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,15 +77,15 @@ async function getFileById(context, type, id) {
|
||||||
return {
|
return {
|
||||||
mimetype: file.mimetype,
|
mimetype: file.mimetype,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
path: getFilePath(type, file.entity, file.filename)
|
path: getFilePath(type, subType, file.entity, file.filename)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _getFileBy(context, type, entityId, key, value) {
|
async function _getFileBy(context, type, subType, entityId, key, value) {
|
||||||
enforceTypePermitted(type);
|
enforceTypePermitted(type, subType);
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'viewFiles');
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||||
const file = await tx(getFilesTable(type)).where({entity: entityId, [key]: value}).first();
|
const file = await tx(getFilesTable(type, subType)).where({entity: entityId, [key]: value}).first();
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,30 +96,31 @@ async function _getFileBy(context, type, entityId, key, value) {
|
||||||
return {
|
return {
|
||||||
mimetype: file.mimetype,
|
mimetype: file.mimetype,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
path: getFilePath(type, file.entity, file.filename)
|
path: getFilePath(type, subType, file.entity, file.filename)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileByOriginalName(context, type, entityId, name) {
|
async function getFileByOriginalName(context, type, subType, entityId, name) {
|
||||||
return await _getFileBy(context, type, entityId, 'originalname', name)
|
return await _getFileBy(context, type, subType, entityId, 'originalname', name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileByFilename(context, type, entityId, name) {
|
async function getFileByFilename(context, type, subType, entityId, name) {
|
||||||
return await _getFileBy(context, type, entityId, 'filename', name)
|
return await _getFileBy(context, type, subType, entityId, 'filename', name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileByUrl(context, type, entityId, url) {
|
async function getFileByUrl(context, type, subType, entityId, url) {
|
||||||
const urlPrefix = getTrustedUrl(`files/${type}/${entityId}/`, context);
|
const urlPrefix = getTrustedUrl(`files/${type}/${subType}/${entityId}/`, context);
|
||||||
if (url.startsWith(urlPrefix)) {
|
if (url.startsWith(urlPrefix)) {
|
||||||
const name = url.substring(urlPrefix.length);
|
const name = url.substring(urlPrefix.length);
|
||||||
return await getFileByFilename(context, type, entityId, name);
|
return await getFileByFilename(context, type, subType, entityId, name);
|
||||||
} else {
|
} else {
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFiles(context, type, entityId, files, getUrl = null, dontReplace = false) {
|
// Adds files to an entity. The source data can be either a file (then it's path is contained in file.path) or in-memory data (then it's content is in file.data).
|
||||||
enforceTypePermitted(type);
|
async function createFiles(context, type, subType, entityId, files, replacementBehavior) {
|
||||||
|
enforceTypePermitted(type, subType);
|
||||||
if (files.length == 0) {
|
if (files.length == 0) {
|
||||||
// No files uploaded
|
// No files uploaded
|
||||||
return {uploaded: 0};
|
return {uploaded: 0};
|
||||||
|
@ -118,31 +133,39 @@ async function createFiles(context, type, entityId, files, getUrl = null, dontRe
|
||||||
const filesRet = [];
|
const filesRet = [];
|
||||||
|
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'manageFiles');
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'manage'));
|
||||||
|
|
||||||
const existingNamesRows = await tx(getFilesTable(type)).where('entity', entityId).select(['filename', 'originalname']);
|
const existingNamesRows = await tx(getFilesTable(type, subType)).where('entity', entityId).select(['id', 'filename', 'originalname']);
|
||||||
const existingNameMap = new Map();
|
|
||||||
|
const existingNameSet = new Set();
|
||||||
for (const row of existingNamesRows) {
|
for (const row of existingNamesRows) {
|
||||||
existingNameMap.set(row.originalname, row);
|
existingNameSet.add(row.originalname);
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalNameSet = new Set();
|
// The processedNameSet holds originalnames of entries which have been already processed in the upload batch. It prevents uploading two files with the same originalname
|
||||||
|
const processedNameSet = new Set();
|
||||||
|
|
||||||
|
|
||||||
// Create entities for files
|
// Create entities for files
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const parsedOriginalName = path.parse(file.originalname);
|
const parsedOriginalName = path.parse(file.originalname);
|
||||||
let originalName = parsedOriginalName.base;
|
let originalName = parsedOriginalName.base;
|
||||||
|
|
||||||
if (dontReplace) {
|
if (!file.filename) {
|
||||||
|
// This is taken from multer/storage/disk.js and adapted for async/await
|
||||||
|
file.filename = (await cryptoPseudoRandomBytes(16)).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replacementBehavior === ReplacementBehavior.RENAME) {
|
||||||
let suffix = 1;
|
let suffix = 1;
|
||||||
while (existingNameMap.has(originalName) || originalNameSet.has(originalName)) {
|
while (existingNameSet.has(originalName) || processedNameSet.has(originalName)) {
|
||||||
originalName = parsedOriginalName.name + '-' + suffix + parsedOriginalName.ext;
|
originalName = parsedOriginalName.name + '-' + suffix + parsedOriginalName.ext;
|
||||||
suffix++;
|
suffix++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (originalNameSet.has(originalName)) {
|
if (replacementBehavior === ReplacementBehavior.NONE && (existingNameSet.has(originalName) || processedNameSet.has(originalName))) {
|
||||||
// The file has an original name same as another file
|
// The file has an original name same as another file in the same upload batch or it has an original name same as another already existing file
|
||||||
ignoredFiles.push(file);
|
ignoredFiles.push(file);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -161,46 +184,61 @@ async function createFiles(context, type, entityId, files, getUrl = null, dontRe
|
||||||
name: file.filename,
|
name: file.filename,
|
||||||
originalName: originalName,
|
originalName: originalName,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
type: file.mimetype,
|
type: file.mimetype
|
||||||
};
|
};
|
||||||
|
|
||||||
filesRetEntry.url = getFileUrl(context, type, entityId, file.filename);
|
filesRetEntry.url = getFileUrl(context, type, subType, entityId, file.filename);
|
||||||
filesRetEntry.thumbnailUrl = getFileUrl(context, type, entityId, file.filename); // TODO - use smaller thumbnails
|
|
||||||
|
if (file.mimetype.startsWith('image/')) {
|
||||||
|
filesRetEntry.thumbnailUrl = getFileUrl(context, type, subType, entityId, file.filename); // TODO - use smaller thumbnails,
|
||||||
|
}
|
||||||
|
|
||||||
filesRet.push(filesRetEntry);
|
filesRet.push(filesRetEntry);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingNameMap.has(originalName)) {
|
processedNameSet.add(originalName);
|
||||||
removedFiles.push(existingNameMap.get(originalName));
|
}
|
||||||
|
|
||||||
|
if (replacementBehavior === ReplacementBehavior.REPLACE) {
|
||||||
|
for (const row of existingNamesRows) {
|
||||||
|
const idsToRemove = [];
|
||||||
|
if (processedNameSet.has(row.originalname)) {
|
||||||
|
removedFiles.push(row);
|
||||||
|
idsToRemove.push(row.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
originalNameSet.add(originalName);
|
await tx(getFilesTable(type, subType)).where('entity', entityId).whereIn('id', idsToRemove).del();
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalNameArray = Array.from(originalNameSet);
|
|
||||||
await tx(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).del();
|
|
||||||
|
|
||||||
if (fileEntities) {
|
if (fileEntities) {
|
||||||
await tx(getFilesTable(type)).insert(fileEntities);
|
await tx(getFilesTable(type, subType)).insert(fileEntities);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move new files from upload directory to files directory
|
// Move new files from upload directory to files directory
|
||||||
for (const file of filesToMove) {
|
for (const file of filesToMove) {
|
||||||
const filePath = getFilePath(type, entityId, file.filename);
|
const filePath = getFilePath(type, subType, entityId, file.filename);
|
||||||
// The names should be unique, so overwrite is disabled
|
|
||||||
// The directory is created if it does not exist
|
if (file.path) {
|
||||||
// Empty options argument is passed, otherwise fails
|
// The names should be unique, so overwrite is disabled
|
||||||
await fs.moveAsync(file.path, filePath, {});
|
// The directory is created if it does not exist
|
||||||
|
// Empty options argument is passed, otherwise fails
|
||||||
|
await fs.moveAsync(file.path, filePath, {});
|
||||||
|
} else if (file.data) {
|
||||||
|
await fs.outputFile(filePath, file.data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Remove replaced files from files directory
|
// Remove replaced files from files directory
|
||||||
for (const file of removedFiles) {
|
for (const file of removedFiles) {
|
||||||
const filePath = getFilePath(type, entityId, file.filename);
|
const filePath = getFilePath(type, subType, entityId, file.filename);
|
||||||
await fs.removeAsync(filePath);
|
await fs.removeAsync(filePath);
|
||||||
}
|
}
|
||||||
// Remove ignored files from upload directory
|
// Remove ignored files from upload directory
|
||||||
for (const file of ignoredFiles) {
|
for (const file of ignoredFiles) {
|
||||||
await fs.removeAsync(file.path);
|
if (file.path) {
|
||||||
|
await fs.removeAsync(file.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -212,38 +250,38 @@ async function createFiles(context, type, entityId, files, getUrl = null, dontRe
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFile(context, type, id) {
|
async function removeFile(context, type, subType, id) {
|
||||||
enforceTypePermitted(type);
|
enforceTypePermitted(type, subType);
|
||||||
|
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
const file = await tx(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
|
const file = await tx(getFilesTable(type, subType)).where('id', id).select('entity', 'filename').first();
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'manage'));
|
||||||
await tx(getFilesTable(type)).where('id', id).del();
|
await tx(getFilesTable(type, subType)).where('id', id).del();
|
||||||
return {filename: file.filename, entity: file.entity};
|
return {filename: file.filename, entity: file.entity};
|
||||||
});
|
});
|
||||||
|
|
||||||
const filePath = getFilePath(type, file.entity, file.filename);
|
const filePath = getFilePath(type, subType, file.entity, file.filename);
|
||||||
await fs.removeAsync(filePath);
|
await fs.removeAsync(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyAllTx(tx, context, fromType, fromEntityId, toType, toEntityId) {
|
async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toType, toSubType, toEntityId) {
|
||||||
enforceTypePermitted(fromType);
|
enforceTypePermitted(fromType, fromSubType);
|
||||||
await shares.enforceEntityPermissionTx(tx, context, fromType, fromEntityId, 'viewFiles');
|
await shares.enforceEntityPermissionTx(tx, context, fromType, fromEntityId, getFilesPermission(fromType, fromSubType, 'view'));
|
||||||
|
|
||||||
enforceTypePermitted(toType);
|
enforceTypePermitted(toType, toSubType);
|
||||||
await shares.enforceEntityPermissionTx(tx, context, toType, toEntityId, 'manageFiles');
|
await shares.enforceEntityPermissionTx(tx, context, toType, toEntityId, getFilesPermission(toType, toSubType, 'manage'));
|
||||||
|
|
||||||
const rows = await tx(getFilesTable(fromType)).where({entity: fromEntityId});
|
const rows = await tx(getFilesTable(fromType, fromSubType)).where({entity: fromEntityId});
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const fromFilePath = getFilePath(fromType, fromEntityId, row.filename);
|
const fromFilePath = getFilePath(fromType, fromSubType, fromEntityId, row.filename);
|
||||||
const toFilePath = getFilePath(toType, toEntityId, row.filename);
|
const toFilePath = getFilePath(toType, toSubType, toEntityId, row.filename);
|
||||||
await fs.copyAsync(fromFilePath, toFilePath, {});
|
await fs.copyAsync(fromFilePath, toFilePath, {});
|
||||||
|
|
||||||
delete row.id;
|
delete row.id;
|
||||||
row.entity = toEntityId;
|
row.entity = toEntityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx(getFilesTable(toType)).insert(rows);
|
await tx(getFilesTable(toType, toSubType)).insert(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -259,5 +297,6 @@ module.exports = {
|
||||||
removeFile,
|
removeFile,
|
||||||
getFileUrl,
|
getFileUrl,
|
||||||
getFilePath,
|
getFilePath,
|
||||||
copyAllTx
|
copyAllTx,
|
||||||
|
ReplacementBehavior
|
||||||
};
|
};
|
|
@ -15,17 +15,21 @@ function hash(entity) {
|
||||||
return hasher.hash(filterObject(entity, allowedKeys));
|
return hasher.hash(filterObject(entity, allowedKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getByIdTx(tx, context, id, withPermissions = true) {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'view');
|
||||||
|
const entity = await tx('templates').where('id', id).first();
|
||||||
|
entity.data = JSON.parse(entity.data);
|
||||||
|
|
||||||
|
if (withPermissions) {
|
||||||
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
async function getById(context, id, withPermissions = true) {
|
async function getById(context, id, withPermissions = true) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'view');
|
return await getByIdTx(tx, context, id, withPermissions);
|
||||||
const entity = await tx('templates').where('id', id).first();
|
|
||||||
entity.data = JSON.parse(entity.data);
|
|
||||||
|
|
||||||
if (withPermissions) {
|
|
||||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +104,7 @@ async function remove(context, id) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hash,
|
hash,
|
||||||
|
getByIdTx,
|
||||||
getById,
|
getById,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
create,
|
create,
|
||||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -1308,6 +1308,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crypto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
|
||||||
|
},
|
||||||
"csrf": {
|
"csrf": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.6.tgz",
|
||||||
|
|
|
@ -57,6 +57,7 @@
|
||||||
"connect-redis": "^3.3.0",
|
"connect-redis": "^3.3.0",
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
"cors": "^2.8.4",
|
"cors": "^2.8.4",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"csurf": "^1.9.0",
|
"csurf": "^1.9.0",
|
||||||
"csv-parse": "^1.2.3",
|
"csv-parse": "^1.2.3",
|
||||||
"device": "^0.3.8",
|
"device": "^0.3.8",
|
||||||
|
|
|
@ -135,10 +135,9 @@ function getRouter(trusted) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mosaico looks for block thumbnails in edres folder relative to index.html of the template. We respond to such requests here.
|
// Mosaico looks for block thumbnails in edres folder relative to index.html of the template. We respond to such requests here.
|
||||||
// We use the following naming convention in Mosaico templates for the block thumbnails: edres/xxx -> edres-xxx
|
|
||||||
router.getAsync('/templates/:mosaicoTemplateId/edres/:fileName', async (req, res, next) => {
|
router.getAsync('/templates/:mosaicoTemplateId/edres/:fileName', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const file = await files.getFileByOriginalName(contextHelpers.getAdminContext(), 'mosaicoTemplate', req.params.mosaicoTemplateId, req.params.fileName);
|
const file = await files.getFileByOriginalName(contextHelpers.getAdminContext(), 'mosaicoTemplate', 'block', req.params.mosaicoTemplateId, req.params.fileName);
|
||||||
res.type(file.mimetype);
|
res.type(file.mimetype);
|
||||||
return res.download(file.path, file.name);
|
return res.download(file.path, file.name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -154,18 +153,18 @@ function getRouter(trusted) {
|
||||||
router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', 'client', 'public', 'mosaico', 'templates', 'versafix-1', 'edres')));
|
router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', 'client', 'public', 'mosaico', 'templates', 'versafix-1', 'edres')));
|
||||||
|
|
||||||
|
|
||||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', getSandboxUrl, true);
|
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file');
|
||||||
|
|
||||||
router.getAsync('/upload/:type/:entityId', passport.loggedIn, async (req, res) => {
|
router.getAsync('/upload/:type/:entityId', passport.loggedIn, async (req, res) => {
|
||||||
const entries = await files.list(req.context, req.params.type, req.params.entityId);
|
const entries = await files.list(req.context, req.params.type, 'file', req.params.entityId);
|
||||||
|
|
||||||
const filesOut = [];
|
const filesOut = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
filesOut.push({
|
filesOut.push({
|
||||||
name: entry.originalname,
|
name: entry.originalname,
|
||||||
url: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename),
|
url: files.getFileUrl(req.context, req.params.type, 'file', req.params.entityId, entry.filename),
|
||||||
size: entry.size,
|
size: entry.size,
|
||||||
thumbnailUrl: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename) // TODO - use smaller thumbnails
|
thumbnailUrl: files.getFileUrl(req.context, req.params.type, 'file', req.params.entityId, entry.filename) // TODO - use smaller thumbnails
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,9 +174,6 @@ function getRouter(trusted) {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||||
const resourceType = req.query.type;
|
|
||||||
const resourceId = req.query.id;
|
|
||||||
|
|
||||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, trusted);
|
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, trusted);
|
||||||
|
|
||||||
let languageStrings = null;
|
let languageStrings = null;
|
||||||
|
@ -229,7 +225,7 @@ function getRouter(trusted) {
|
||||||
if (url.startsWith(mosaicoLegacyUrlPrefix)) {
|
if (url.startsWith(mosaicoLegacyUrlPrefix)) {
|
||||||
filePath = path.join(__dirname, '..', 'client', 'public' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length));
|
filePath = path.join(__dirname, '..', 'client', 'public' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length));
|
||||||
} else {
|
} else {
|
||||||
const file = await files.getFileByUrl(contextHelpers.getAdminContext(), req.params.type, req.params.entityId, url);
|
const file = await files.getFileByUrl(contextHelpers.getAdminContext(), req.params.type, 'file', req.params.entityId, url);
|
||||||
filePath = file.path;
|
filePath = file.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@ const files = require('../../models/files');
|
||||||
const router = require('../../lib/router-async').create();
|
const router = require('../../lib/router-async').create();
|
||||||
const fileHelpers = require('../../lib/file-helpers');
|
const fileHelpers = require('../../lib/file-helpers');
|
||||||
|
|
||||||
router.postAsync('/files-table/:type/:entityId', passport.loggedIn, async (req, res) => {
|
router.postAsync('/files-table/:type/:subType/:entityId', passport.loggedIn, async (req, res) => {
|
||||||
return res.json(await files.listDTAjax(req.context, req.params.type, req.params.entityId, req.body));
|
return res.json(await files.listDTAjax(req.context, req.params.type, req.params.subType, req.params.entityId, req.body));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
router.getAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
const file = await files.getFileById(req.context, req.params.type, req.params.fileId);
|
const file = await files.getFileById(req.context, req.params.type, req.params.subType, req.params.fileId);
|
||||||
res.type(file.mimetype);
|
res.type(file.mimetype);
|
||||||
return res.download(file.path, file.name);
|
return res.download(file.path, file.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.deleteAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
router.deleteAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
await files.removeFile(req.context, req.params.type, req.params.fileId);
|
await files.removeFile(req.context, req.params.type, req.params.subType, req.params.fileId);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
fileHelpers.installUploadHandler(router, '/files/:type/:entityId');
|
fileHelpers.installUploadHandler(router, '/files/:type/:subType/:entityId', files.ReplacementBehavior.REPLACE);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
|
@ -69,7 +69,7 @@ router.postAsync('/permissions-check', passport.loggedIn, async (req, res) => {
|
||||||
|
|
||||||
router.postAsync('/permissions-rebuild', passport.loggedIn, async (req, res) => {
|
router.postAsync('/permissions-rebuild', passport.loggedIn, async (req, res) => {
|
||||||
shares.enforceGlobalPermission(req.context, 'rebuildPermissions');
|
shares.enforceGlobalPermission(req.context, 'rebuildPermissions');
|
||||||
shares.rebuildPermissions();
|
await shares.rebuildPermissions();
|
||||||
return res.json(result);
|
return res.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -505,7 +505,7 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
|
||||||
const autoUnsubscribe = req.query.auto === 'yes';
|
const autoUnsubscribe = req.query.auto === 'yes';
|
||||||
|
|
||||||
if (autoUnsubscribe) {
|
if (autoUnsubscribe) {
|
||||||
handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
|
await handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||||
|
|
||||||
} else if (req.query.formTest ||
|
} else if (req.query.formTest ||
|
||||||
list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
||||||
|
|
|
@ -120,5 +120,4 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
// return knex.schema.dropTable('users');
|
|
||||||
})();
|
})();
|
|
@ -31,5 +31,4 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
await knex.schema.dropTable('namespaces');
|
|
||||||
})();
|
})();
|
|
@ -32,9 +32,4 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
for (const entityType of shareableEntityTypes) {
|
|
||||||
await knex.schema
|
|
||||||
.dropTable(`shares_${entityType}`)
|
|
||||||
.dropTable(`permissions_${entityType}`);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,25 +1,39 @@
|
||||||
const entityTypesWithFiles = ['template', 'campaign'];
|
const entityTypesWithFiles = {
|
||||||
|
campaign: {
|
||||||
|
file: 'files_campaign_file',
|
||||||
|
attachment: 'files_campaign_attachment',
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
file: 'files_template_file'
|
||||||
|
},
|
||||||
|
mosaicoTemplate: {
|
||||||
|
file: 'files_mosaico_template_file',
|
||||||
|
block: 'files_mosaico_template_block'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
for (const entityType of entityTypesWithFiles) {
|
for (const type in entityTypesWithFiles) {
|
||||||
|
const typeEntry = entityTypesWithFiles[type];
|
||||||
|
|
||||||
await knex.schema.createTable(`files_${entityType}`, table => {
|
for (const subType in typeEntry) {
|
||||||
table.increments('id').primary();
|
const subTypeEntry = typeEntry[subType];
|
||||||
table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`);
|
|
||||||
table.string('filename');
|
|
||||||
table.string('originalname');
|
|
||||||
table.string('mimetype');
|
|
||||||
table.string('encoding');
|
|
||||||
table.integer('size');
|
|
||||||
table.timestamp('created').defaultTo(knex.fn.now());
|
|
||||||
table.index(['entity', 'originalname'])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
await knex.schema.createTable(subTypeEntry, table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('entity').unsigned().notNullable().references(`${type}s.id`);
|
||||||
|
table.string('filename');
|
||||||
|
table.string('originalname');
|
||||||
|
table.string('mimetype');
|
||||||
|
table.string('encoding');
|
||||||
|
table.integer('size');
|
||||||
|
table.timestamp('created').defaultTo(knex.fn.now());
|
||||||
|
table.index(['entity', 'originalname'])
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
for (const entityType of entityTypesWithFiles) {
|
|
||||||
await knex.schema.dropTable(`files_${entityType}`);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -52,10 +52,4 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
await knex.schema
|
|
||||||
.dropTable('shares_mosaico_template')
|
|
||||||
.dropTable('permissions_mosaico_template')
|
|
||||||
.dropTable('files_mosaico_template')
|
|
||||||
.dropTable('mosaico_templates')
|
|
||||||
;
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -121,9 +121,4 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
|
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
await knex.schema
|
|
||||||
.dropTable('shares_send_configuration')
|
|
||||||
.dropTable('permissions_send_configuration')
|
|
||||||
.dropTable('send_configurations')
|
|
||||||
;
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -56,6 +56,8 @@ scheduled - used only for campaign type NORMAL
|
||||||
|
|
||||||
const { getSystemSendConfigurationId } = require('../../../shared/send-configurations');
|
const { getSystemSendConfigurationId } = require('../../../shared/send-configurations');
|
||||||
const { CampaignSource, CampaignType} = require('../../../shared/campaigns');
|
const { CampaignSource, CampaignType} = require('../../../shared/campaigns');
|
||||||
|
const files = require('../../../models/files');
|
||||||
|
const contextHelpers = require('../../../lib/context-helpers');
|
||||||
|
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
|
|
||||||
|
@ -77,11 +79,11 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
let editorType = campaign.editor_name;
|
let editorType = campaign.editor_name;
|
||||||
const editorData = JSON.parse(campaign.editor_data || '{}');
|
const editorData = JSON.parse(campaign.editor_data || '{}');
|
||||||
|
|
||||||
if (editorType == 'summernote') {
|
if (editorType === 'summernote') {
|
||||||
editorType = 'ckeditor';
|
editorType = 'ckeditor';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editorType == 'mosaico') {
|
if (editorType === 'mosaico') {
|
||||||
editorType = 'mosaicoWithFsTemplate';
|
editorType = 'mosaicoWithFsTemplate';
|
||||||
editorData.mosaicoFsTemplate = editorData.template;
|
editorData.mosaicoFsTemplate = editorData.template;
|
||||||
delete editorData.template;
|
delete editorData.template;
|
||||||
|
@ -115,6 +117,20 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
campaign.data = JSON.stringify(data);
|
campaign.data = JSON.stringify(data);
|
||||||
|
|
||||||
await knex('campaigns').where('id', campaign.id).update(campaign);
|
await knex('campaigns').where('id', campaign.id).update(campaign);
|
||||||
|
|
||||||
|
const attachments = await knex('attachments').where('campaign', campaign.id);
|
||||||
|
const attachmentFiles = [];
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
attachmentFiles.push({
|
||||||
|
originalname: attachment.filename,
|
||||||
|
mimetype: attachment.content_type,
|
||||||
|
// encoding: file.encoding,
|
||||||
|
size: attachment.size,
|
||||||
|
created: attachment.created,
|
||||||
|
data: attachment.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await files.createFiles(contextHelpers.getAdminContext(), 'campaign', 'attachment', campaign.id, attachmentFiles, files.ReplacementBehavior.NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
await knex.schema.table('campaigns', table => {
|
await knex.schema.table('campaigns', table => {
|
||||||
|
@ -138,6 +154,7 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
|
|
||||||
await knex.schema.dropTableIfExists('campaign');
|
await knex.schema.dropTableIfExists('campaign');
|
||||||
await knex.schema.dropTableIfExists('campaign_tracker');
|
await knex.schema.dropTableIfExists('campaign_tracker');
|
||||||
|
await knex.schema.dropTableIfExists('attachments');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
|
|
|
@ -6,8 +6,9 @@ const CampaignSource = {
|
||||||
TEMPLATE: 1,
|
TEMPLATE: 1,
|
||||||
CUSTOM: 2,
|
CUSTOM: 2,
|
||||||
CUSTOM_FROM_TEMPLATE: 3,
|
CUSTOM_FROM_TEMPLATE: 3,
|
||||||
URL: 4,
|
CUSTOM_FROM_CAMPAIGN: 4,
|
||||||
RSS: 5,
|
URL: 5,
|
||||||
|
RSS: 6,
|
||||||
|
|
||||||
MAX: 6
|
MAX: 6
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue