diff --git a/app-builder.js b/app-builder.js index c536f7e4..35156252 100644 --- a/app-builder.js +++ b/app-builder.js @@ -5,8 +5,6 @@ const log = require('npmlog'); const _ = require('./lib/translate')._; -const { nodeifyFunction } = require('./lib/nodeify'); - const express = require('express'); const bodyParser = require('body-parser'); const path = require('path'); @@ -22,7 +20,6 @@ const compression = require('compression'); const passport = require('./lib/passport'); const contextHelpers = require('./lib/context-helpers'); -const getSettings = nodeifyFunction(require('./models/settings').get); const api = require('./routes/api'); // These are routes for the new React-based client @@ -42,6 +39,7 @@ const triggersRest = require('./routes/rest/triggers'); const listsRest = require('./routes/rest/lists'); const formsRest = require('./routes/rest/forms'); const fieldsRest = require('./routes/rest/fields'); +const importsRest = require('./routes/rest/imports'); const sharesRest = require('./routes/rest/shares'); const segmentsRest = require('./routes/rest/segments'); const subscriptionsRest = require('./routes/rest/subscriptions'); @@ -284,6 +282,7 @@ function createApp(trusted) { app.use('/rest', listsRest); app.use('/rest', formsRest); app.use('/rest', fieldsRest); + app.use('/rest', importsRest); app.use('/rest', sharesRest); app.use('/rest', segmentsRest); app.use('/rest', subscriptionsRest); diff --git a/client/src/campaigns/CUD.js b/client/src/campaigns/CUD.js index 07f7b619..b4469854 100644 --- a/client/src/campaigns/CUD.js +++ b/client/src/campaigns/CUD.js @@ -500,8 +500,8 @@ export default class CUD extends Component { stateOwner={this} visible={this.props.action === 'delete'} deleteUrl={`rest/campaigns/${this.props.entity.id}`} - cudUrl={`/campaigns/${this.props.entity.id}/edit`} - listUrl="/campaigns" + backUrl={`/campaigns/${this.props.entity.id}/edit`} + successUrl="/campaigns" deletingMsg={t('Deleting campaign ...')} deletedMsg={t('Campaign deleted')}/> } diff --git a/client/src/campaigns/triggers/CUD.js b/client/src/campaigns/triggers/CUD.js index a016af78..462eff24 100644 --- a/client/src/campaigns/triggers/CUD.js +++ b/client/src/campaigns/triggers/CUD.js @@ -198,8 +198,8 @@ export default class CUD extends Component { stateOwner={this} visible={this.props.action === 'delete'} deleteUrl={`rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`} - cudUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`} - listUrl={`/campaigns/${this.props.campaign.id}/triggers`} + backUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`} + successUrl={`/campaigns/${this.props.campaign.id}/triggers`} deletingMsg={t('Deleting trigger ...')} deletedMsg={t('Trigger deleted')}/> } diff --git a/client/src/campaigns/triggers/List.js b/client/src/campaigns/triggers/List.js index c6d16935..f40c319e 100644 --- a/client/src/campaigns/triggers/List.js +++ b/client/src/campaigns/triggers/List.js @@ -14,6 +14,7 @@ import {withErrorHandling} from '../../lib/error-handling'; import {Table} from '../../lib/table'; import {getTriggerTypes} from './helpers'; import {Icon} from "../../lib/bootstrap-components"; +import mailtrainConfig from 'mailtrainConfig'; @translate() @withPageHelpers @@ -52,7 +53,7 @@ export default class List extends Component { actions: data => { const actions = []; - if (this.props.campaign.permissions.includes('manageTriggers')) { + if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers')) { actions.push({ label: , link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit` @@ -66,7 +67,7 @@ export default class List extends Component { return (
- {this.props.campaign.permissions.includes('manageTriggers') && + {mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers') && diff --git a/client/src/lib/form.js b/client/src/lib/form.js index 2900155a..8e3670cb 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -224,7 +224,12 @@ class StaticField extends Component { label: PropTypes.string, help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), className: PropTypes.string, - format: PropTypes.string + format: PropTypes.string, + withValidation: PropTypes.bool + } + + static contextTypes = { + formStateOwner: PropTypes.object.isRequired } render() { @@ -238,7 +243,7 @@ class StaticField extends Component { className += ' ' + props.className; } - return wrapInput(null, htmlId, owner, props.format, '', props.label, props.help, + return wrapInput(props.withValidation ? id : null, htmlId, owner, props.format, '', props.label, props.help,
{props.children}
); } @@ -1020,14 +1025,17 @@ function withForm(target) { const response = await axios.get(getUrl(url)); - const data = response.data; + let data = response.data; data.originalHash = data.hash; delete data.hash; if (mutator) { - // FIXME - change the interface such that if the mutator is provided, it is supposed to return which fields to keep in the form - mutator(data); + const newData = mutator(data); + + if (newData !== undefined) { + data = newData; + } } this.populateFormValues(data); @@ -1037,11 +1045,13 @@ function withForm(target) { await this.waitForFormServerValidated(); if (this.isFormWithoutErrors()) { - const data = this.getFormValues(); + let data = this.getFormValues(); if (mutator) { - // FIXME - change the interface such that the mutator is supposed to create the object to be submitted - mutator(data); + const newData = mutator(data); + if (newData !== undefined) { + data = newData; + } } const response = await axios.method(method, getUrl(url), data); diff --git a/client/src/lib/modals.js b/client/src/lib/modals.js index e44e85f2..cea106e6 100644 --- a/client/src/lib/modals.js +++ b/client/src/lib/modals.js @@ -68,8 +68,8 @@ class DeleteModalDialog extends Component { stateOwner: PropTypes.object.isRequired, visible: PropTypes.bool.isRequired, deleteUrl: PropTypes.string.isRequired, - cudUrl: PropTypes.string.isRequired, - listUrl: PropTypes.string.isRequired, + backUrl: PropTypes.string.isRequired, + successUrl: PropTypes.string.isRequired, deletingMsg: PropTypes.string.isRequired, deletedMsg: PropTypes.string.isRequired, onErrorAsync: PropTypes.func @@ -86,8 +86,8 @@ class DeleteModalDialog extends Component { visible={this.props.visible} actionMethod={HTTPMethod.DELETE} actionUrl={this.props.deleteUrl} - backUrl={this.props.cudUrl} - successUrl={this.props.listUrl} + backUrl={this.props.backUrl} + successUrl={this.props.successUrl} actionInProgressMsg={this.props.deletingMsg} actionDoneMsg={this.props.deletedMsg} onErrorAsync={this.props.onErrorAsync} diff --git a/client/src/lists/CUD.js b/client/src/lists/CUD.js index 1a9013a6..877cd3da 100644 --- a/client/src/lists/CUD.js +++ b/client/src/lists/CUD.js @@ -165,8 +165,8 @@ export default class CUD extends Component { stateOwner={this} visible={this.props.action === 'delete'} deleteUrl={`rest/lists/${this.props.entity.id}`} - cudUrl={`/lists/${this.props.entity.id}/edit`} - listUrl="/lists" + backUrl={`/lists/${this.props.entity.id}/edit`} + successUrl="/lists" deletingMsg={t('Deleting list ...')} deletedMsg={t('List deleted')}/> } diff --git a/client/src/lists/List.js b/client/src/lists/List.js index 351b47ff..03d98bf1 100644 --- a/client/src/lists/List.js +++ b/client/src/lists/List.js @@ -64,7 +64,6 @@ export default class List extends Component { const actions = []; const triggersCount = data[6]; const perms = data[7]; - console.log(data); if (perms.includes('viewSubscriptions')) { actions.push({ @@ -94,6 +93,13 @@ export default class List extends Component { }); } + if (perms.includes('viewImports')) { + actions.push({ + label: , + link: `/lists/${data[0]}/imports` + }); + } + if (triggersCount > 0) { actions.push({ label: , diff --git a/client/src/lists/TriggersList.js b/client/src/lists/TriggersList.js index 88964666..f1ca0224 100644 --- a/client/src/lists/TriggersList.js +++ b/client/src/lists/TriggersList.js @@ -12,6 +12,7 @@ import {withErrorHandling} from '../lib/error-handling'; import {Table} from '../lib/table'; import {getTriggerTypes} from '../campaigns/triggers/helpers'; import {Icon} from "../lib/bootstrap-components"; +import mailtrainConfig from 'mailtrainConfig'; @translate() @withPageHelpers @@ -52,7 +53,7 @@ export default class List extends Component { const perms = data[9]; const campaignId = data[8]; - if (perms.includes('manageTriggers')) { + if (mailtrainConfig.globalPermissions.includes('setupAutomation') && perms.includes('manageTriggers')) { actions.push({ label: , link: `/campaigns/${campaignId}/triggers/${data[0]}/edit` diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js index 1565d351..eda702af 100644 --- a/client/src/lists/fields/CUD.js +++ b/client/src/lists/fields/CUD.js @@ -425,8 +425,8 @@ export default class CUD extends Component { stateOwner={this} visible={this.props.action === 'delete'} deleteUrl={`rest/fields/${this.props.list.id}/${this.props.entity.id}`} - cudUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`} - listUrl={`/lists/${this.props.list.id}/fields`} + backUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`} + successUrl={`/lists/${this.props.list.id}/fields`} deletingMsg={t('Deleting field ...')} deletedMsg={t('Field deleted')}/> } diff --git a/client/src/lists/forms/CUD.js b/client/src/lists/forms/CUD.js index aecf068b..a6ad263f 100644 --- a/client/src/lists/forms/CUD.js +++ b/client/src/lists/forms/CUD.js @@ -375,8 +375,8 @@ export default class CUD extends Component { stateOwner={this} visible={this.props.action === 'delete'} deleteUrl={`rest/forms/${this.props.entity.id}`} - cudUrl={`/lists/forms/${this.props.entity.id}/edit`} - listUrl="/lists/forms" + backUrl={`/lists/forms/${this.props.entity.id}/edit`} + successUrl="/lists/forms" deletingMsg={t('Deleting form ...')} deletedMsg={t('Form deleted')}/> } diff --git a/client/src/lists/imports/CUD.js b/client/src/lists/imports/CUD.js new file mode 100644 index 00000000..faba4c82 --- /dev/null +++ b/client/src/lists/imports/CUD.js @@ -0,0 +1,195 @@ +'use strict'; + +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import {translate} from 'react-i18next'; +import { + NavButton, + requiresAuthenticatedUser, + Title, + withPageHelpers +} from '../../lib/page'; +import { + AlignedRow, + Button, + ButtonRow, + Dropdown, + Form, + FormSendMethod, + InputField, + StaticField, + TextArea, + withForm +} from '../../lib/form'; +import {withErrorHandling} from '../../lib/error-handling'; +import {DeleteModalDialog} from "../../lib/modals"; +import {getImportTypes} from './helpers'; +import styles from "../../lib/styles.scss"; +import {ImportType} from '../../../../shared/imports'; + +@translate() +@withForm +@withPageHelpers +@withErrorHandling +@requiresAuthenticatedUser +export default class CUD extends Component { + constructor(props) { + super(props); + + this.state = {}; + + const {importTypeLabels} = getImportTypes(props.t); + + this.importTypeLabels = importTypeLabels; + + this.importTypeOptions = [ + {key: ImportType.CSV_FILE, label: importTypeLabels[ImportType.CSV_FILE]}, + // {key: ImportType.LIST, label: importTypeLabels[ImportType.LIST]} + ]; + + this.initForm(); + } + + static propTypes = { + action: PropTypes.string.isRequired, + list: PropTypes.object, + entity: PropTypes.object + } + + componentDidMount() { + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity, data => { + data.settings = data.settings || {}; + + if (data.type === ImportType.CSV_FILE) { + data.csvFileName = data.settings.csv.originalname; + } + }); + + } else { + this.populateFormValues({ + name: '', + description: '', + type: ImportType.CSV_FILE, + csvFileName: '' + }); + } + } + + localValidateFormValues(state) { + const t = this.props.t; + const isEdit = !!this.props.entity; + const type = Number.parseInt(state.getIn(['type', 'value'])); + + for (const key of state.keys()) { + state.setIn([key, 'error'], null); + } + + if (!state.getIn(['name', 'value'])) { + state.setIn(['name', 'error'], t('Name must not be empty')); + } + + if (!isEdit && type === ImportType.CSV_FILE) { + if (!this.csvFile || this.csvFile.files.length === 0) { + state.setIn(['csvFileName', 'error'], t('File must be selected')); + } + } + } + + async submitHandler() { + const t = this.props.t; + const isEdit = !!this.props.entity; + + let sendMethod, url; + if (this.props.entity) { + sendMethod = FormSendMethod.PUT; + url = `rest/imports/${this.props.list.id}/${this.props.entity.id}` + } else { + sendMethod = FormSendMethod.POST; + url = `rest/imports/${this.props.list.id}` + } + + try { + this.disableForm(); + this.setFormStatusMessage('info', t('Saving ...')); + + const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { + data.type = Number.parseInt(data.type); + data.settings = {}; + + const formData = new FormData(); + if (!isEdit && data.type === ImportType.CSV_FILE) { + formData.append('csvFile', this.csvFile.files[0]); + } + + formData.append('entity', JSON.stringify(data)); + + return formData; + }); + + if (submitSuccessful) { + this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/imports`, 'success', t('Import saved')); + } else { + this.enableForm(); + this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); + } + } catch (error) { + throw error; + } + } + + onFileSelected() { + this.scheduleFormRevalidate(); + } + + render() { + const t = this.props.t; + const isEdit = !!this.props.entity; + + const type = Number.parseInt(this.getFormValue('type')); + + let settings = null; + if (type === ImportType.CSV_FILE) { + if (isEdit) { + settings = {this.getFormValue('csvFileName')}; + } else { + settings = this.csvFile = node} type="file" onChange={::this.onFileSelected}/>; + } + } + + return ( +
+ {isEdit && + + } + + {isEdit ? t('Edit Import') : t('Create Import')} + +
+ +