First part of the UI for file import (upload of csv file to the server)
This commit is contained in:
parent
965f30cea7
commit
6648028270
31 changed files with 672 additions and 51 deletions
|
@ -5,8 +5,6 @@ const log = require('npmlog');
|
||||||
|
|
||||||
const _ = require('./lib/translate')._;
|
const _ = require('./lib/translate')._;
|
||||||
|
|
||||||
const { nodeifyFunction } = require('./lib/nodeify');
|
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
@ -22,7 +20,6 @@ const compression = require('compression');
|
||||||
const passport = require('./lib/passport');
|
const passport = require('./lib/passport');
|
||||||
const contextHelpers = require('./lib/context-helpers');
|
const contextHelpers = require('./lib/context-helpers');
|
||||||
|
|
||||||
const getSettings = nodeifyFunction(require('./models/settings').get);
|
|
||||||
const api = require('./routes/api');
|
const api = require('./routes/api');
|
||||||
|
|
||||||
// These are routes for the new React-based client
|
// 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 listsRest = require('./routes/rest/lists');
|
||||||
const formsRest = require('./routes/rest/forms');
|
const formsRest = require('./routes/rest/forms');
|
||||||
const fieldsRest = require('./routes/rest/fields');
|
const fieldsRest = require('./routes/rest/fields');
|
||||||
|
const importsRest = require('./routes/rest/imports');
|
||||||
const sharesRest = require('./routes/rest/shares');
|
const sharesRest = require('./routes/rest/shares');
|
||||||
const segmentsRest = require('./routes/rest/segments');
|
const segmentsRest = require('./routes/rest/segments');
|
||||||
const subscriptionsRest = require('./routes/rest/subscriptions');
|
const subscriptionsRest = require('./routes/rest/subscriptions');
|
||||||
|
@ -284,6 +282,7 @@ function createApp(trusted) {
|
||||||
app.use('/rest', listsRest);
|
app.use('/rest', listsRest);
|
||||||
app.use('/rest', formsRest);
|
app.use('/rest', formsRest);
|
||||||
app.use('/rest', fieldsRest);
|
app.use('/rest', fieldsRest);
|
||||||
|
app.use('/rest', importsRest);
|
||||||
app.use('/rest', sharesRest);
|
app.use('/rest', sharesRest);
|
||||||
app.use('/rest', segmentsRest);
|
app.use('/rest', segmentsRest);
|
||||||
app.use('/rest', subscriptionsRest);
|
app.use('/rest', subscriptionsRest);
|
||||||
|
|
|
@ -500,8 +500,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/campaigns/${this.props.entity.id}`}
|
deleteUrl={`rest/campaigns/${this.props.entity.id}`}
|
||||||
cudUrl={`/campaigns/${this.props.entity.id}/edit`}
|
backUrl={`/campaigns/${this.props.entity.id}/edit`}
|
||||||
listUrl="/campaigns"
|
successUrl="/campaigns"
|
||||||
deletingMsg={t('Deleting campaign ...')}
|
deletingMsg={t('Deleting campaign ...')}
|
||||||
deletedMsg={t('Campaign deleted')}/>
|
deletedMsg={t('Campaign deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,8 +198,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`}
|
deleteUrl={`rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`}
|
||||||
cudUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`}
|
backUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`}
|
||||||
listUrl={`/campaigns/${this.props.campaign.id}/triggers`}
|
successUrl={`/campaigns/${this.props.campaign.id}/triggers`}
|
||||||
deletingMsg={t('Deleting trigger ...')}
|
deletingMsg={t('Deleting trigger ...')}
|
||||||
deletedMsg={t('Trigger deleted')}/>
|
deletedMsg={t('Trigger deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {withErrorHandling} from '../../lib/error-handling';
|
||||||
import {Table} from '../../lib/table';
|
import {Table} from '../../lib/table';
|
||||||
import {getTriggerTypes} from './helpers';
|
import {getTriggerTypes} from './helpers';
|
||||||
import {Icon} from "../../lib/bootstrap-components";
|
import {Icon} from "../../lib/bootstrap-components";
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
|
@ -52,7 +53,7 @@ export default class List extends Component {
|
||||||
actions: data => {
|
actions: data => {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
if (this.props.campaign.permissions.includes('manageTriggers')) {
|
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
|
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
|
||||||
|
@ -66,7 +67,7 @@ export default class List extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.props.campaign.permissions.includes('manageTriggers') &&
|
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers') &&
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
|
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
|
@ -224,7 +224,12 @@ class StaticField extends Component {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
format: PropTypes.string
|
format: PropTypes.string,
|
||||||
|
withValidation: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
formStateOwner: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -238,7 +243,7 @@ class StaticField extends Component {
|
||||||
className += ' ' + props.className;
|
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,
|
||||||
<div id={htmlId} className={className} aria-describedby={htmlId + '_help'}>{props.children}</div>
|
<div id={htmlId} className={className} aria-describedby={htmlId + '_help'}>{props.children}</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1020,14 +1025,17 @@ function withForm(target) {
|
||||||
|
|
||||||
const response = await axios.get(getUrl(url));
|
const response = await axios.get(getUrl(url));
|
||||||
|
|
||||||
const data = response.data;
|
let data = response.data;
|
||||||
|
|
||||||
data.originalHash = data.hash;
|
data.originalHash = data.hash;
|
||||||
delete data.hash;
|
delete data.hash;
|
||||||
|
|
||||||
if (mutator) {
|
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
|
const newData = mutator(data);
|
||||||
mutator(data);
|
|
||||||
|
if (newData !== undefined) {
|
||||||
|
data = newData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.populateFormValues(data);
|
this.populateFormValues(data);
|
||||||
|
@ -1037,11 +1045,13 @@ function withForm(target) {
|
||||||
await this.waitForFormServerValidated();
|
await this.waitForFormServerValidated();
|
||||||
|
|
||||||
if (this.isFormWithoutErrors()) {
|
if (this.isFormWithoutErrors()) {
|
||||||
const data = this.getFormValues();
|
let data = this.getFormValues();
|
||||||
|
|
||||||
if (mutator) {
|
if (mutator) {
|
||||||
// FIXME - change the interface such that the mutator is supposed to create the object to be submitted
|
const newData = mutator(data);
|
||||||
mutator(data);
|
if (newData !== undefined) {
|
||||||
|
data = newData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.method(method, getUrl(url), data);
|
const response = await axios.method(method, getUrl(url), data);
|
||||||
|
|
|
@ -68,8 +68,8 @@ class DeleteModalDialog extends Component {
|
||||||
stateOwner: PropTypes.object.isRequired,
|
stateOwner: PropTypes.object.isRequired,
|
||||||
visible: PropTypes.bool.isRequired,
|
visible: PropTypes.bool.isRequired,
|
||||||
deleteUrl: PropTypes.string.isRequired,
|
deleteUrl: PropTypes.string.isRequired,
|
||||||
cudUrl: PropTypes.string.isRequired,
|
backUrl: PropTypes.string.isRequired,
|
||||||
listUrl: PropTypes.string.isRequired,
|
successUrl: PropTypes.string.isRequired,
|
||||||
deletingMsg: PropTypes.string.isRequired,
|
deletingMsg: PropTypes.string.isRequired,
|
||||||
deletedMsg: PropTypes.string.isRequired,
|
deletedMsg: PropTypes.string.isRequired,
|
||||||
onErrorAsync: PropTypes.func
|
onErrorAsync: PropTypes.func
|
||||||
|
@ -86,8 +86,8 @@ class DeleteModalDialog extends Component {
|
||||||
visible={this.props.visible}
|
visible={this.props.visible}
|
||||||
actionMethod={HTTPMethod.DELETE}
|
actionMethod={HTTPMethod.DELETE}
|
||||||
actionUrl={this.props.deleteUrl}
|
actionUrl={this.props.deleteUrl}
|
||||||
backUrl={this.props.cudUrl}
|
backUrl={this.props.backUrl}
|
||||||
successUrl={this.props.listUrl}
|
successUrl={this.props.successUrl}
|
||||||
actionInProgressMsg={this.props.deletingMsg}
|
actionInProgressMsg={this.props.deletingMsg}
|
||||||
actionDoneMsg={this.props.deletedMsg}
|
actionDoneMsg={this.props.deletedMsg}
|
||||||
onErrorAsync={this.props.onErrorAsync}
|
onErrorAsync={this.props.onErrorAsync}
|
||||||
|
|
|
@ -165,8 +165,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/lists/${this.props.entity.id}`}
|
deleteUrl={`rest/lists/${this.props.entity.id}`}
|
||||||
cudUrl={`/lists/${this.props.entity.id}/edit`}
|
backUrl={`/lists/${this.props.entity.id}/edit`}
|
||||||
listUrl="/lists"
|
successUrl="/lists"
|
||||||
deletingMsg={t('Deleting list ...')}
|
deletingMsg={t('Deleting list ...')}
|
||||||
deletedMsg={t('List deleted')}/>
|
deletedMsg={t('List deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,6 @@ export default class List extends Component {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const triggersCount = data[6];
|
const triggersCount = data[6];
|
||||||
const perms = data[7];
|
const perms = data[7];
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
if (perms.includes('viewSubscriptions')) {
|
if (perms.includes('viewSubscriptions')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
|
@ -94,6 +93,13 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (perms.includes('viewImports')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="arrow-down" title={t('Imports')}/>,
|
||||||
|
link: `/lists/${data[0]}/imports`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (triggersCount > 0) {
|
if (triggersCount > 0) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="flash" title={t('Triggers')}/>,
|
label: <Icon icon="flash" title={t('Triggers')}/>,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {withErrorHandling} from '../lib/error-handling';
|
||||||
import {Table} from '../lib/table';
|
import {Table} from '../lib/table';
|
||||||
import {getTriggerTypes} from '../campaigns/triggers/helpers';
|
import {getTriggerTypes} from '../campaigns/triggers/helpers';
|
||||||
import {Icon} from "../lib/bootstrap-components";
|
import {Icon} from "../lib/bootstrap-components";
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withPageHelpers
|
@withPageHelpers
|
||||||
|
@ -52,7 +53,7 @@ export default class List extends Component {
|
||||||
const perms = data[9];
|
const perms = data[9];
|
||||||
const campaignId = data[8];
|
const campaignId = data[8];
|
||||||
|
|
||||||
if (perms.includes('manageTriggers')) {
|
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && perms.includes('manageTriggers')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
|
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
|
||||||
|
|
|
@ -425,8 +425,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/fields/${this.props.list.id}/${this.props.entity.id}`}
|
deleteUrl={`rest/fields/${this.props.list.id}/${this.props.entity.id}`}
|
||||||
cudUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`}
|
backUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`}
|
||||||
listUrl={`/lists/${this.props.list.id}/fields`}
|
successUrl={`/lists/${this.props.list.id}/fields`}
|
||||||
deletingMsg={t('Deleting field ...')}
|
deletingMsg={t('Deleting field ...')}
|
||||||
deletedMsg={t('Field deleted')}/>
|
deletedMsg={t('Field deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -375,8 +375,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/forms/${this.props.entity.id}`}
|
deleteUrl={`rest/forms/${this.props.entity.id}`}
|
||||||
cudUrl={`/lists/forms/${this.props.entity.id}/edit`}
|
backUrl={`/lists/forms/${this.props.entity.id}/edit`}
|
||||||
listUrl="/lists/forms"
|
successUrl="/lists/forms"
|
||||||
deletingMsg={t('Deleting form ...')}
|
deletingMsg={t('Deleting form ...')}
|
||||||
deletedMsg={t('Form deleted')}/>
|
deletedMsg={t('Form deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
195
client/src/lists/imports/CUD.js
Normal file
195
client/src/lists/imports/CUD.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 {
|
||||||
|
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 = <StaticField id="csvFileName" className={styles.formDisabled} label={t('File')}>{this.getFormValue('csvFileName')}</StaticField>;
|
||||||
|
} else {
|
||||||
|
settings = <StaticField withValidation id="csvFileName" label={t('File')}><input ref={node => this.csvFile = node} type="file" onChange={::this.onFileSelected}/></StaticField>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isEdit &&
|
||||||
|
<DeleteModalDialog
|
||||||
|
stateOwner={this}
|
||||||
|
visible={this.props.action === 'delete'}
|
||||||
|
deleteUrl={`rest/imports/${this.props.list.id}/${this.props.entity.id}`}
|
||||||
|
backUrl={`/lists/${this.props.list.id}/imports/${this.props.entity.id}/edit`}
|
||||||
|
successUrl={`/lists/${this.props.list.id}/imports`}
|
||||||
|
deletingMsg={t('Deleting import ...')}
|
||||||
|
deletedMsg={t('Field deleted')}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Title>{isEdit ? t('Edit Import') : t('Create Import')}</Title>
|
||||||
|
|
||||||
|
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||||
|
<InputField id="name" label={t('Name')}/>
|
||||||
|
<TextArea id="description" label={t('Description')}/>
|
||||||
|
|
||||||
|
{isEdit ?
|
||||||
|
<StaticField id="type" className={styles.formDisabled} label={t('Type')}>{this.importTypeLabels[this.getFormValue('type')]}</StaticField>
|
||||||
|
:
|
||||||
|
<Dropdown id="type" label={t('Type')} options={this.importTypeOptions}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{settings}
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
|
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/imports/${this.props.entity.id}/delete`}/>}
|
||||||
|
</ButtonRow>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
75
client/src/lists/imports/List.js
Normal file
75
client/src/lists/imports/List.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { translate } from 'react-i18next';
|
||||||
|
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
|
||||||
|
import { withErrorHandling } from '../../lib/error-handling';
|
||||||
|
import { Table } from '../../lib/table';
|
||||||
|
import { getImportTypes } from './helpers';
|
||||||
|
import {Icon} from "../../lib/bootstrap-components";
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withPageHelpers
|
||||||
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
|
export default class List extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
|
||||||
|
const {importTypeLabels, importStatusLabels} = getImportTypes(props.t);
|
||||||
|
this.importTypeLabels = importTypeLabels;
|
||||||
|
this.importStatusLabels = importStatusLabels;
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
list: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ data: 1, title: t('Name') },
|
||||||
|
{ data: 2, title: t('Description') },
|
||||||
|
{ data: 3, title: t('Source'), render: data => this.importTypeLabels[data].label, sortable: false, searchable: false },
|
||||||
|
{ data: 4, title: t('Status'), render: data => this.importStatusLabels[data].label, sortable: false, searchable: false },
|
||||||
|
{ data: 5, title: t('Last run'), render: data => moment(data).fromNow() },
|
||||||
|
{
|
||||||
|
actions: data => {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
|
link: `/lists/${this.props.list.id}/imports/${data[0]}/edit`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') &&
|
||||||
|
<Toolbar>
|
||||||
|
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
|
||||||
|
</Toolbar>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Title>{t('Imports')}</Title>
|
||||||
|
|
||||||
|
<Table withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
client/src/lists/imports/helpers.js
Normal file
25
client/src/lists/imports/helpers.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {ImportType, ImportStatus} from '../../../../shared/imports';
|
||||||
|
|
||||||
|
export function getImportTypes(t) {
|
||||||
|
|
||||||
|
const importTypeLabels = {
|
||||||
|
[ImportType.CSV_FILE]: t('CSV file'),
|
||||||
|
[ImportType.LIST]: t('List'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const importStatusLabels = {
|
||||||
|
[ImportStatus.NOT_READY]: t('Preparing'),
|
||||||
|
[ImportStatus.RUNNING]: t('Running'),
|
||||||
|
[ImportStatus.SCHEDULED]: t('Scheduled'),
|
||||||
|
[ImportStatus.FINISHED]: t('Finished')
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
importStatusLabels,
|
||||||
|
importTypeLabels
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import SubscriptionsList from './subscriptions/List';
|
||||||
import SubscriptionsCUD from './subscriptions/CUD';
|
import SubscriptionsCUD from './subscriptions/CUD';
|
||||||
import SegmentsList from './segments/List';
|
import SegmentsList from './segments/List';
|
||||||
import SegmentsCUD from './segments/CUD';
|
import SegmentsCUD from './segments/CUD';
|
||||||
|
import ImportsList from './imports/List';
|
||||||
|
import ImportsCUD from './imports/CUD';
|
||||||
import Share from '../shares/Share';
|
import Share from '../shares/Share';
|
||||||
import TriggersList from './TriggersList';
|
import TriggersList from './TriggersList';
|
||||||
|
|
||||||
|
@ -127,6 +129,32 @@ function getMenus(t) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
imports: {
|
||||||
|
title: t('Imports'),
|
||||||
|
link: params => `/lists/${params.listId}/imports/`,
|
||||||
|
visible: resolved => resolved.list.permissions.includes('viewImports'),
|
||||||
|
panelRender: props => <ImportsList list={props.resolved.list} />,
|
||||||
|
children: {
|
||||||
|
':importId([0-9]+)': {
|
||||||
|
title: resolved => t('Import "{{name}}"', {name: resolved.import.name}),
|
||||||
|
resolve: {
|
||||||
|
import: params => `rest/imports/${params.listId}/${params.importId}`,
|
||||||
|
},
|
||||||
|
link: params => `/lists/${params.listId}/imports/${params.importId}/edit`,
|
||||||
|
navs: {
|
||||||
|
':action(edit|delete)': {
|
||||||
|
title: t('Edit'),
|
||||||
|
link: params => `/lists/${params.listId}/imports/${params.importId}/edit`,
|
||||||
|
panelRender: props => <ImportsCUD action={props.match.params.action} entity={props.resolved.import} list={props.resolved.list} imports={props.resolved.imports} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
title: t('Create'),
|
||||||
|
panelRender: props => <ImportsCUD action="create" list={props.resolved.list} imports={props.resolved.imports} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
triggers: {
|
triggers: {
|
||||||
title: t('Triggers'),
|
title: t('Triggers'),
|
||||||
link: params => `/lists/${params.listId}/triggers`,
|
link: params => `/lists/${params.listId}/triggers`,
|
||||||
|
|
|
@ -333,8 +333,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/segments/${this.props.list.id}/${this.props.entity.id}`}
|
deleteUrl={`rest/segments/${this.props.list.id}/${this.props.entity.id}`}
|
||||||
cudUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
|
backUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
|
||||||
listUrl={`/lists/${this.props.list.id}/segments`}
|
successUrl={`/lists/${this.props.list.id}/segments`}
|
||||||
deletingMsg={t('Deleting segment ...')}
|
deletingMsg={t('Deleting segment ...')}
|
||||||
deletedMsg={t('Segment deleted')}/>
|
deletedMsg={t('Segment deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,8 +184,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/namespaces/${this.props.entity.id}`}
|
deleteUrl={`rest/namespaces/${this.props.entity.id}`}
|
||||||
cudUrl={`/namespaces/${this.props.entity.id}/edit`}
|
backUrl={`/namespaces/${this.props.entity.id}/edit`}
|
||||||
listUrl="/namespaces"
|
successUrl="/namespaces"
|
||||||
deletingMsg={t('Deleting namespace ...')}
|
deletingMsg={t('Deleting namespace ...')}
|
||||||
deletedMsg={t('Namespace deleted')}
|
deletedMsg={t('Namespace deleted')}
|
||||||
onErrorAsync={::this.onDeleteError}/>
|
onErrorAsync={::this.onDeleteError}/>
|
||||||
|
|
|
@ -220,8 +220,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/reports/${this.props.entity.id}`}
|
deleteUrl={`rest/reports/${this.props.entity.id}`}
|
||||||
cudUrl={`/reports/${this.props.entity.id}/edit`}
|
backUrl={`/reports/${this.props.entity.id}/edit`}
|
||||||
listUrl="/reports"
|
successUrl="/reports"
|
||||||
deletingMsg={t('Deleting report ...')}
|
deletingMsg={t('Deleting report ...')}
|
||||||
deletedMsg={t('Report deleted')}/>
|
deletedMsg={t('Report deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -299,8 +299,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/reports/templates/${this.props.entity.id}`}
|
deleteUrl={`rest/reports/templates/${this.props.entity.id}`}
|
||||||
cudUrl={`/reports/templates/${this.props.entity.id}/edit`}
|
backUrl={`/reports/templates/${this.props.entity.id}/edit`}
|
||||||
listUrl="/reports/templates"
|
successUrl="/reports/templates"
|
||||||
deletingMsg={t('Deleting report template ...')}
|
deletingMsg={t('Deleting report template ...')}
|
||||||
deletedMsg={t('Report template deleted')}/>
|
deletedMsg={t('Report template deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,9 +84,9 @@ class Root extends Component {
|
||||||
<DropdownMenuItem label={t('Administration')}>
|
<DropdownMenuItem label={t('Administration')}>
|
||||||
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</MenuLink>
|
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</MenuLink>
|
||||||
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</MenuLink>
|
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</MenuLink>
|
||||||
<MenuLink to="/settings"><Icon icon='cog'/> {t('Global Settings')}</MenuLink>
|
{mailtrainConfig.globalPermissions.includes('manageSettings') && <MenuLink to="/settings"><Icon icon='cog'/> {t('Global Settings')}</MenuLink>}
|
||||||
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('Send Configurations')}</MenuLink>
|
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('Send Configurations')}</MenuLink>
|
||||||
<MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>
|
{mailtrainConfig.globalPermissions.includes('manageBlacklist') && <MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>}
|
||||||
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('API')}</MenuLink>
|
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('API')}</MenuLink>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -177,8 +177,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/send-configurations/${this.props.entity.id}`}
|
deleteUrl={`rest/send-configurations/${this.props.entity.id}`}
|
||||||
cudUrl={`/send-configurations/${this.props.entity.id}/edit`}
|
backUrl={`/send-configurations/${this.props.entity.id}/edit`}
|
||||||
listUrl="/send-configurations"
|
successUrl="/send-configurations"
|
||||||
deletingMsg={t('Deleting send configuration ...')}
|
deletingMsg={t('Deleting send configuration ...')}
|
||||||
deletedMsg={t('Send configuration deleted')}/>
|
deletedMsg={t('Send configuration deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,8 +216,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/templates/${this.props.entity.id}`}
|
deleteUrl={`rest/templates/${this.props.entity.id}`}
|
||||||
cudUrl={`/templates/${this.props.entity.id}/edit`}
|
backUrl={`/templates/${this.props.entity.id}/edit`}
|
||||||
listUrl="/templates"
|
successUrl="/templates"
|
||||||
deletingMsg={t('Deleting template ...')}
|
deletingMsg={t('Deleting template ...')}
|
||||||
deletedMsg={t('Template deleted')}/>
|
deletedMsg={t('Template deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,6 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeSave: data => {
|
beforeSave: data => {
|
||||||
console.log(data);
|
|
||||||
data[prefix + 'data'] = {
|
data[prefix + 'data'] = {
|
||||||
mosaicoTemplate: data[prefix + 'mosaicoTemplate'],
|
mosaicoTemplate: data[prefix + 'mosaicoTemplate'],
|
||||||
metadata: data[prefix + 'mosaicoData'].metadata,
|
metadata: data[prefix + 'mosaicoData'].metadata,
|
||||||
|
|
|
@ -171,8 +171,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/mosaico-templates/${this.props.entity.id}`}
|
deleteUrl={`rest/mosaico-templates/${this.props.entity.id}`}
|
||||||
cudUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
|
backUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
|
||||||
listUrl="/templates/mosaico"
|
successUrl="/templates/mosaico"
|
||||||
deletingMsg={t('Deleting Mosaico template ...')}
|
deletingMsg={t('Deleting Mosaico template ...')}
|
||||||
deletedMsg={t('Mosaico template deleted')}/>
|
deletedMsg={t('Mosaico template deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,8 +197,8 @@ export default class CUD extends Component {
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
deleteUrl={`rest/users/${this.props.entity.id}`}
|
deleteUrl={`rest/users/${this.props.entity.id}`}
|
||||||
cudUrl={`/users/${this.props.entity.id}/edit`}
|
backUrl={`/users/${this.props.entity.id}/edit`}
|
||||||
listUrl="/users"
|
successUrl="/users"
|
||||||
deletingMsg={t('Deleting user ...')}
|
deletingMsg={t('Deleting user ...')}
|
||||||
deletedMsg={t('User deleted')}/>
|
deletedMsg={t('User deleted')}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,7 +198,7 @@ roles:
|
||||||
name: Master
|
name: Master
|
||||||
admin: true
|
admin: true
|
||||||
description: All permissions
|
description: All permissions
|
||||||
permissions: [rebuildPermissions, createJavascriptWithROAccess, manageBlacklist, manageSettings]
|
permissions: [rebuildPermissions, createJavascriptWithROAccess, manageBlacklist, manageSettings, setupAutomation]
|
||||||
rootNamespaceRole: master
|
rootNamespaceRole: master
|
||||||
|
|
||||||
namespace:
|
namespace:
|
||||||
|
@ -208,7 +208,7 @@ roles:
|
||||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||||
children:
|
children:
|
||||||
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
||||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments]
|
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
|
||||||
customForm: [view, edit, delete, share]
|
customForm: [view, edit, delete, share]
|
||||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats]
|
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats]
|
||||||
template: [view, edit, delete, share, viewFiles, manageFiles]
|
template: [view, edit, delete, share, viewFiles, manageFiles]
|
||||||
|
@ -227,7 +227,7 @@ roles:
|
||||||
master:
|
master:
|
||||||
name: Master
|
name: Master
|
||||||
description: All permissions
|
description: All permissions
|
||||||
permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments]
|
permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
|
||||||
|
|
||||||
customForm:
|
customForm:
|
||||||
master:
|
master:
|
||||||
|
|
161
models/imports.js
Normal file
161
models/imports.js
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const knex = require('../lib/knex');
|
||||||
|
const hasher = require('node-object-hash')();
|
||||||
|
const { enforce, filterObject } = require('../lib/helpers');
|
||||||
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
const shares = require('./shares');
|
||||||
|
const {ImportType, ImportStatus, RunStatus} = require('../shared/imports');
|
||||||
|
const fs = require('fs-extra-promise');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const filesDir = path.join(__dirname, '..', 'files', 'imports');
|
||||||
|
|
||||||
|
const allowedKeys = new Set(['name', 'description', 'type', 'settings']);
|
||||||
|
|
||||||
|
function hash(entity) {
|
||||||
|
return hasher.hash(filterObject(entity, allowedKeys));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getById(context, listId, id) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewImports');
|
||||||
|
|
||||||
|
const entity = await tx('imports').where({list: listId, id}).first();
|
||||||
|
entity.settings = JSON.parse(entity.settings);
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listDTAjax(context, listId, params) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewImports');
|
||||||
|
|
||||||
|
return await dtHelpers.ajaxListTx(
|
||||||
|
tx,
|
||||||
|
params,
|
||||||
|
builder => builder
|
||||||
|
.from('imports')
|
||||||
|
.where('imports.list', listId),
|
||||||
|
[ 'imports.id', 'imports.name', 'imports.description', 'imports.type', 'imports.status', 'imports.last_run' ]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _validateAndPreprocess(tx, listId, entity, isCreate) {
|
||||||
|
enforce(Number.isInteger(entity.type));
|
||||||
|
enforce(entity.type >= ImportType.MIN && entity.type <= ImportType.MAX, 'Invalid import type');
|
||||||
|
|
||||||
|
entity.settings = entity.settings || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(context, listId, entity, files) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
shares.enforceGlobalPermission(context, 'setupAutomation');
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageImports');
|
||||||
|
|
||||||
|
await _validateAndPreprocess(tx, listId, entity, true);
|
||||||
|
|
||||||
|
// FIXME - set status
|
||||||
|
|
||||||
|
if (entity.type === ImportType.CSV_FILE) {
|
||||||
|
enforce(files.csvFile, 'File must be included');
|
||||||
|
const csvFile = files.csvFile[0];
|
||||||
|
const filePath = path.join(filesDir, csvFile.filename);
|
||||||
|
await fs.moveAsync(csvFile.path, filePath, {});
|
||||||
|
|
||||||
|
entity.settings.csv = {
|
||||||
|
originalname: csvFile.originalname,
|
||||||
|
filename: csvFile.filename
|
||||||
|
};
|
||||||
|
|
||||||
|
entity.status = ImportStatus.NOT_READY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const filteredEntity = filterObject(entity, allowedKeys);
|
||||||
|
filteredEntity.list = listId;
|
||||||
|
filteredEntity.settings = JSON.stringify(filteredEntity.settings);
|
||||||
|
|
||||||
|
const ids = await tx('imports').insert(filteredEntity);
|
||||||
|
const id = ids[0];
|
||||||
|
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWithConsistencyCheck(context, listId, entity, files) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
shares.enforceGlobalPermission(context, 'setupAutomation');
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageImports');
|
||||||
|
|
||||||
|
const existing = await tx('imports').where({list: listId, id: entity.id}).first();
|
||||||
|
if (!existing) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.settings = JSON.parse(existing.settings);
|
||||||
|
const existingHash = hash(existing);
|
||||||
|
if (existingHash !== entity.originalHash) {
|
||||||
|
throw new interoperableErrors.ChangedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
enforce(entity.type === existing.type, 'Import type cannot be changed');
|
||||||
|
await _validateAndPreprocess(tx, listId, entity, false);
|
||||||
|
|
||||||
|
if (entity.type === ImportType.CSV_FILE) {
|
||||||
|
entity.settings.csv = existing.settings.csv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME - set status
|
||||||
|
// FIXME - create CSV import table
|
||||||
|
|
||||||
|
const filteredEntity = filterObject(entity, allowedKeys);
|
||||||
|
filteredEntity.list = listId;
|
||||||
|
filteredEntity.settings = JSON.stringify(filteredEntity.settings);
|
||||||
|
|
||||||
|
await tx('imports').where({list: listId, id: entity.id}).update(filteredEntity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTx(tx, context, listId, id) {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageImports');
|
||||||
|
|
||||||
|
const existing = await tx('imports').where({list: listId, id: id}).first();
|
||||||
|
if (!existing) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME - remove csv import table
|
||||||
|
|
||||||
|
await tx('import_failed').whereIn('run', function() {this.from('import_runs').select('id').where('import', id)});
|
||||||
|
await tx('import_runs').where('import', id).del();
|
||||||
|
await tx('imports').where({list: listId, id}).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(context, listId, id) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
await removeTx(tx, context, listId, id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAllByListIdTx(tx, context, listId) {
|
||||||
|
const entities = await tx('imports').where('list', listId).select(['id']);
|
||||||
|
for (const entity of entities) {
|
||||||
|
await removeTx(tx, context, listId, entity.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This is to handle circular dependency with segments.js
|
||||||
|
module.exports = {
|
||||||
|
hash,
|
||||||
|
getById,
|
||||||
|
listDTAjax,
|
||||||
|
create,
|
||||||
|
updateWithConsistencyCheck,
|
||||||
|
remove,
|
||||||
|
removeAllByListIdTx
|
||||||
|
};
|
|
@ -70,6 +70,7 @@ async function _validateAndPreprocess(tx, context, campaignId, entity) {
|
||||||
|
|
||||||
async function create(context, campaignId, entity) {
|
async function create(context, campaignId, entity) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
|
shares.enforceGlobalPermission(context, 'setupAutomation');
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'manageTriggers');
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'manageTriggers');
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, context, campaignId, entity);
|
await _validateAndPreprocess(tx, context, campaignId, entity);
|
||||||
|
@ -93,6 +94,7 @@ async function create(context, campaignId, entity) {
|
||||||
|
|
||||||
async function updateWithConsistencyCheck(context, campaignId, entity) {
|
async function updateWithConsistencyCheck(context, campaignId, entity) {
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
|
shares.enforceGlobalPermission(context, 'setupAutomation');
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'manageTriggers');
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'manageTriggers');
|
||||||
|
|
||||||
const existing = await tx('triggers').where({campaign: campaignId, id: entity.id}).first();
|
const existing = await tx('triggers').where({campaign: campaignId, id: entity.id}).first();
|
||||||
|
|
51
routes/rest/imports.js
Normal file
51
routes/rest/imports.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const passport = require('../../lib/passport');
|
||||||
|
const imports = require('../../models/imports');
|
||||||
|
|
||||||
|
const router = require('../../lib/router-async').create();
|
||||||
|
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const files = require('../../models/files');
|
||||||
|
const uploadedFilesDir = path.join(files.filesDir, 'uploaded');
|
||||||
|
|
||||||
|
const multer = require('multer')({
|
||||||
|
dest: uploadedFilesDir
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/imports-table/:listId', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await imports.listDTAjax(req.context, req.params.listId, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.getAsync('/imports/:listId/:importId', passport.loggedIn, async (req, res) => {
|
||||||
|
const entity = await imports.getById(req.context, req.params.listId, req.params.importId);
|
||||||
|
entity.hash = imports.hash(entity);
|
||||||
|
return res.json(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileFields = [
|
||||||
|
{name: 'csvFile', maxCount: 1}
|
||||||
|
];
|
||||||
|
|
||||||
|
router.postAsync('/imports/:listId', passport.loggedIn, passport.csrfProtection, multer.fields(fileFields), async (req, res) => {
|
||||||
|
const entity = JSON.parse(req.body.entity);
|
||||||
|
|
||||||
|
return res.json(await imports.create(req.context, req.params.listId, entity, req.files));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.putAsync('/imports/:listId/:importId', passport.loggedIn, passport.csrfProtection, multer.fields(fileFields), async (req, res) => {
|
||||||
|
const entity = JSON.parse(req.body.entity);
|
||||||
|
entity.id = parseInt(req.params.importId);
|
||||||
|
|
||||||
|
await imports.updateWithConsistencyCheck(req.context, req.params.listId, entity, req.files);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.deleteAsync('/imports/:listId/:importId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
await imports.remove(req.context, req.params.listId, req.params.importId);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -975,6 +975,44 @@ async function migrateTriggers(knex) {
|
||||||
await knex.schema.dropTableIfExists('trigger');
|
await knex.schema.dropTableIfExists('trigger');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateImporter(knex) {
|
||||||
|
await knex.schema.dropTableIfExists('import_failed');
|
||||||
|
await knex.schema.dropTableIfExists('importer');
|
||||||
|
|
||||||
|
await knex.schema.createTable('imports', table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('name');
|
||||||
|
table.text('description');
|
||||||
|
table.integer('list').unsigned().references('lists.id');
|
||||||
|
table.integer('type').unsigned().notNullable();
|
||||||
|
table.integer('status').unsigned().notNullable();
|
||||||
|
table.text('settings', 'longtext');
|
||||||
|
table.timestamp('last_run');
|
||||||
|
table.timestamp('created').defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTable('import_runs', table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('import').unsigned().references('imports.id');
|
||||||
|
table.integer('status').unsigned().notNullable();
|
||||||
|
table.integer('new').defaultTo(0);
|
||||||
|
table.integer('failed').defaultTo(0);
|
||||||
|
table.integer('processed').defaultTo(0);
|
||||||
|
table.text('error');
|
||||||
|
table.timestamp('created').defaultTo(knex.fn.now());
|
||||||
|
table.timestamp('finished');
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTable('import_failed', table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('run').unsigned().references('import_runs.id');
|
||||||
|
table.string('email').notNullable();
|
||||||
|
table.text('reason');
|
||||||
|
table.timestamp('created').defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
await migrateBase(knex);
|
await migrateBase(knex);
|
||||||
await addNamespaces(knex);
|
await addNamespaces(knex);
|
||||||
|
@ -997,6 +1035,8 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
await migrateAttachments(knex);
|
await migrateAttachments(knex);
|
||||||
|
|
||||||
await migrateTriggers(knex);
|
await migrateTriggers(knex);
|
||||||
|
|
||||||
|
await migrateImporter(knex);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
|
|
28
shared/imports.js
Normal file
28
shared/imports.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ImportType = {
|
||||||
|
MIN: 0,
|
||||||
|
|
||||||
|
CSV_FILE: 0,
|
||||||
|
LIST: 1,
|
||||||
|
|
||||||
|
MAX: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImportStatus = {
|
||||||
|
NOT_READY: 0,
|
||||||
|
SCHEDULED: 1,
|
||||||
|
RUNNING: 2,
|
||||||
|
FINISHED: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const RunStatus = {
|
||||||
|
RUNNING: 0,
|
||||||
|
FINISHED: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ImportType,
|
||||||
|
ImportStatus,
|
||||||
|
RunStatus
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue