First part of the UI for file import (upload of csv file to the server)

This commit is contained in:
Tomas Bures 2018-08-05 10:17:05 +05:30
parent 965f30cea7
commit 6648028270
31 changed files with 672 additions and 51 deletions

View file

@ -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')}/>
}

View file

@ -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')}/>
}

View file

@ -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: <Icon icon="edit" title={t('Edit')}/>,
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
@ -66,7 +67,7 @@ export default class List extends Component {
return (
<div>
{this.props.campaign.permissions.includes('manageTriggers') &&
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers') &&
<Toolbar>
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
</Toolbar>

View file

@ -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,
<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 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);

View file

@ -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}

View file

@ -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')}/>
}

View file

@ -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: <Icon icon="arrow-down" title={t('Imports')}/>,
link: `/lists/${data[0]}/imports`
});
}
if (triggersCount > 0) {
actions.push({
label: <Icon icon="flash" title={t('Triggers')}/>,

View file

@ -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: <Icon icon="edit" title={t('Edit')}/>,
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`

View file

@ -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')}/>
}

View file

@ -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')}/>
}

View 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>
);
}
}

View 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>
);
}
}

View 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
};
}

View file

@ -12,6 +12,8 @@ import SubscriptionsList from './subscriptions/List';
import SubscriptionsCUD from './subscriptions/CUD';
import SegmentsList from './segments/List';
import SegmentsCUD from './segments/CUD';
import ImportsList from './imports/List';
import ImportsCUD from './imports/CUD';
import Share from '../shares/Share';
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: {
title: t('Triggers'),
link: params => `/lists/${params.listId}/triggers`,

View file

@ -333,8 +333,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/segments/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
listUrl={`/lists/${this.props.list.id}/segments`}
backUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
successUrl={`/lists/${this.props.list.id}/segments`}
deletingMsg={t('Deleting segment ...')}
deletedMsg={t('Segment deleted')}/>
}

View file

@ -184,8 +184,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/namespaces/${this.props.entity.id}`}
cudUrl={`/namespaces/${this.props.entity.id}/edit`}
listUrl="/namespaces"
backUrl={`/namespaces/${this.props.entity.id}/edit`}
successUrl="/namespaces"
deletingMsg={t('Deleting namespace ...')}
deletedMsg={t('Namespace deleted')}
onErrorAsync={::this.onDeleteError}/>

View file

@ -220,8 +220,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/reports/${this.props.entity.id}`}
cudUrl={`/reports/${this.props.entity.id}/edit`}
listUrl="/reports"
backUrl={`/reports/${this.props.entity.id}/edit`}
successUrl="/reports"
deletingMsg={t('Deleting report ...')}
deletedMsg={t('Report deleted')}/>
}

View file

@ -299,8 +299,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/reports/templates/${this.props.entity.id}`}
cudUrl={`/reports/templates/${this.props.entity.id}/edit`}
listUrl="/reports/templates"
backUrl={`/reports/templates/${this.props.entity.id}/edit`}
successUrl="/reports/templates"
deletingMsg={t('Deleting report template ...')}
deletedMsg={t('Report template deleted')}/>
}

View file

@ -84,9 +84,9 @@ class Root extends Component {
<DropdownMenuItem label={t('Administration')}>
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</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="/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>
</DropdownMenuItem>
</ul>

View file

@ -177,8 +177,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/send-configurations/${this.props.entity.id}`}
cudUrl={`/send-configurations/${this.props.entity.id}/edit`}
listUrl="/send-configurations"
backUrl={`/send-configurations/${this.props.entity.id}/edit`}
successUrl="/send-configurations"
deletingMsg={t('Deleting send configuration ...')}
deletedMsg={t('Send configuration deleted')}/>
}

View file

@ -216,8 +216,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/templates/${this.props.entity.id}`}
cudUrl={`/templates/${this.props.entity.id}/edit`}
listUrl="/templates"
backUrl={`/templates/${this.props.entity.id}/edit`}
successUrl="/templates"
deletingMsg={t('Deleting template ...')}
deletedMsg={t('Template deleted')}/>
}

View file

@ -97,7 +97,6 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
};
},
beforeSave: data => {
console.log(data);
data[prefix + 'data'] = {
mosaicoTemplate: data[prefix + 'mosaicoTemplate'],
metadata: data[prefix + 'mosaicoData'].metadata,

View file

@ -171,8 +171,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/mosaico-templates/${this.props.entity.id}`}
cudUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
listUrl="/templates/mosaico"
backUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
successUrl="/templates/mosaico"
deletingMsg={t('Deleting Mosaico template ...')}
deletedMsg={t('Mosaico template deleted')}/>
}

View file

@ -197,8 +197,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/users/${this.props.entity.id}`}
cudUrl={`/users/${this.props.entity.id}/edit`}
listUrl="/users"
backUrl={`/users/${this.props.entity.id}/edit`}
successUrl="/users"
deletingMsg={t('Deleting user ...')}
deletedMsg={t('User deleted')}/>
}