UI for basic import and preparation phase of CSV.

This commit is contained in:
Tomas Bures 2018-08-26 11:46:12 +02:00
parent 877e0a857d
commit 739b9452de
24 changed files with 907 additions and 138 deletions

View file

@ -112,7 +112,8 @@ class Fieldset extends Component {
id: PropTypes.string,
label: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
flat: PropTypes.bool
flat: PropTypes.bool,
className: PropTypes.string
}
static contextTypes = {
@ -125,7 +126,10 @@ class Fieldset extends Component {
const id = this.props.id;
const htmlId = 'form_' + id;
const className = id ? owner.addFormValidationClass('', id) : null;
let className = id ? owner.addFormValidationClass('', id) : null;
if (this.props.className) {
className = (className || '') + ' ' + this.props.className;
}
let helpBlock = null;
if (this.props.help) {
@ -154,7 +158,17 @@ class Fieldset extends Component {
}
function wrapInput(id, htmlId, owner, format, rightContainerClass, label, help, input) {
const className = id ? owner.addFormValidationClass('form-group', id) : 'form-group';
// wrapInput may be used also outside forms to make a kind of fake read-only forms
let className;
if (owner) {
if (id) {
className = owner.addFormValidationClass('form-group', id);
} else {
className = 'form-group';
}
} else {
className = 'row ' + styles.staticFormGroup;
}
let colLeft = '';
let colRight = '';
@ -580,7 +594,6 @@ class Dropdown extends Component {
label: PropTypes.string.isRequired,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
options: PropTypes.array,
optGroups: PropTypes.array,
className: PropTypes.string,
format: PropTypes.string
}
@ -595,16 +608,20 @@ class Dropdown extends Component {
const owner = this.context.formStateOwner;
const id = this.props.id;
const htmlId = 'form_' + id;
let options = [];
const options = [];
if (this.props.options) {
options = props.options.map(option => <option key={option.key} value={option.key}>{option.label}</option>);
} else if (this.props.optGroups) {
options = props.optGroups.map(optGroup =>
<optgroup key={optGroup.key} label={optGroup.label}>
{optGroup.options.map(option => <option key={option.key} value={option.key}>{option.label}</option>)}
</optgroup>
);
for (const optOrGrp of props.options) {
if (optOrGrp.options) {
options.push(
<optgroup key={optOrGrp.key} label={optOrGrp.label}>
{optOrGrp.options.map(opt => <option key={opt.key} value={opt.key}>{opt.label}</option>)}
</optgroup>
)
} else {
options.push(<option key={optOrGrp.key} value={optOrGrp.key}>{optOrGrp.label}</option>)
}
}
}
let className = 'form-control';
@ -629,7 +646,7 @@ class AlignedRow extends Component {
}
static contextTypes = {
formStateOwner: PropTypes.object.isRequired
formStateOwner: PropTypes.object // AlignedRow may be used also outside forms to make a kind of fake read-only forms
}
static defaultProps = {

View file

@ -27,6 +27,10 @@
}
}
.staticFormGroup {
margin-bottom: 15px;
}
.dayPickerWrapper {
text-align: right;
}

View file

@ -347,6 +347,11 @@ class Table extends Component {
this.fetchAndNotifySelectionData();
}
componentWillUnmount() {
clearInterval(this.refreshIntervalId);
clearTimeout(this.refreshTimeoutId);
}
async notifySelection(eventCallback, newSelectionMap) {
if (eventCallback) {
const selPairs = Array.from(newSelectionMap).sort((l, r) => l[0] - r[0]);

View file

@ -95,7 +95,7 @@ export default class List extends Component {
if (perms.includes('viewImports')) {
actions.push({
label: <Icon icon="arrow-down" title={t('Imports')}/>,
label: <Icon icon="sort" title={t('Imports')}/>,
link: `/lists/${data[0]}/imports`
});
}

View file

@ -427,7 +427,7 @@ export default class CUD extends Component {
{ selectedTemplate &&
<Fieldset label={t('Templates')}>
<Dropdown id="selectedTemplate" label={t('Edit')} optGroups={templateOptGroups} help={this.templateSettings[selectedTemplate].help}/>
<Dropdown id="selectedTemplate" label={t('Edit')} options={templateOptGroups} help={this.templateSettings[selectedTemplate].help}/>
<ACEEditor id={selectedTemplate} height="500px" mode={this.templateSettings[selectedTemplate].mode}/>
</Fieldset>
}

View file

@ -10,10 +10,10 @@ import {
withPageHelpers
} from '../../lib/page';
import {
AlignedRow,
Button,
ButtonRow,
Dropdown,
Fieldset,
Form,
FormSendMethod,
InputField,
@ -21,11 +21,33 @@ import {
TextArea,
withForm
} from '../../lib/form';
import {withErrorHandling} from '../../lib/error-handling';
import {
withAsyncErrorHandler,
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';
import {
ImportType,
inProgress,
prepInProgress,
runInProgress
} from '../../../../shared/imports';
import axios from "../../lib/axios";
import {getUrl} from "../../lib/urls";
import styles from "../styles.scss";
function truncate(str, len, ending = '...') {
str = str.trim();
if (str.length > len) {
return str.substring(0, len - ending.length) + ending;
} else {
return str;
}
}
@translate()
@withForm
@ -47,26 +69,54 @@ export default class CUD extends Component {
// {key: ImportType.LIST, label: importTypeLabels[ImportType.LIST]}
];
this.refreshTimeoutHandler = ::this.refreshEntity;
this.refreshTimeoutId = 0;
this.initForm();
}
static propTypes = {
action: PropTypes.string.isRequired,
list: PropTypes.object,
fieldsGrouped: PropTypes.array,
entity: PropTypes.object
}
initFromEntity(entity) {
this.getFormValuesFromEntity(entity, data => {
data.settings = data.settings || {};
const mapping = data.mapping || {};
if (data.type === ImportType.CSV_FILE) {
data.csvFileName = data.settings.csv.originalname;
data.csvDelimiter = data.settings.csv.delimiter;
}
for (const field of this.props.fieldsGrouped) {
if (field.column) {
const colMapping = mapping[field.column] || {};
data['mapping_' + field.column + '_column'] = colMapping.column || '';
} else {
for (const option of field.settings.options) {
const col = field.groupedOptions[option.key].column;
const colMapping = mapping[col] || {};
data['mapping_' + col + '_column'] = colMapping.column || '';
}
}
}
const emailMapping = mapping.email || {};
data.mapping_email_column = emailMapping.column || '';
});
if (inProgress(entity.status)) {
this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 1000);
}
}
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;
data.csvDelimiter = data.settings.csv.delimiter;
}
});
this.initFromEntity(this.props.entity);
} else {
this.populateFormValues({
name: '',
@ -78,10 +128,21 @@ export default class CUD extends Component {
}
}
componentWillUnmount() {
clearTimeout(this.refreshTimeoutId);
}
@withAsyncErrorHandler
async refreshEntity() {
const resp = await axios.get(getUrl(`rest/imports/${this.props.list.id}/${this.props.entity.id}`));
this.initFromEntity(resp.data);
}
localValidateFormValues(state) {
const t = this.props.t;
const isEdit = !!this.props.entity;
const type = Number.parseInt(state.getIn(['type', 'value']));
const status = this.getFormValue('status');
for (const key of state.keys()) {
state.setIn([key, 'error'], null);
@ -100,12 +161,20 @@ export default class CUD extends Component {
state.setIn(['csvDelimiter', 'error'], t('CSV delimiter must not be empty'));
}
}
if (isEdit) {
if (!state.getIn(['mapping_email_column', 'value'])) {
state.setIn(['mapping_email_column', 'error'], t('Email mapping has to be provided'));
}
}
}
async submitHandler() {
const t = this.props.t;
const isEdit = !!this.props.entity;
const type = Number.parseInt(this.getFormValue('type'));
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
@ -119,7 +188,7 @@ export default class CUD extends Component {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.type = Number.parseInt(data.type);
data.settings = {};
@ -128,18 +197,52 @@ export default class CUD extends Component {
data.settings.csv = {};
formData.append('csvFile', this.csvFile.files[0]);
data.settings.csv.delimiter = data.csvDelimiter.trim();
delete data.csvFile;
delete data.csvDelimiter;
}
if (isEdit) {
const mapping = {};
for (const field of this.props.fieldsGrouped) {
if (field.column) {
mapping[field.column] = {
column: data['mapping_' + field.column + '_column']
};
delete data['mapping_' + field.column + '_column'];
} else {
for (const option of field.settings.options) {
const col = field.groupedOptions[option.key].column;
mapping[col] = {
column: data['mapping_' + col + '_column']
};
delete data['mapping_' + col + '_column'];
}
}
}
mapping.email = {
column: data.mapping_email_column
};
data.mapping = mapping;
}
delete data.csvFile;
delete data.csvDelimiter;
delete data.sampleRow;
formData.append('entity', JSON.stringify(data));
return formData;
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/imports`, 'success', t('Import saved'));
if (submitResponse) {
if (!isEdit && type === ImportType.CSV_FILE) {
this.navigateTo(`/lists/${this.props.list.id}/imports/${submitResponse}/edit`);
} else {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/imports/${this.props.entity.id}/status`, 'success', t('Import saved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
@ -158,17 +261,19 @@ export default class CUD extends Component {
const isEdit = !!this.props.entity;
const type = Number.parseInt(this.getFormValue('type'));
const status = this.getFormValue('status');
const settings = this.getFormValue('settings');
let settings = null;
let settingsEdit = null;
if (type === ImportType.CSV_FILE) {
if (isEdit) {
settings =
settingsEdit =
<div>
<StaticField id="csvFileName" className={styles.formDisabled} label={t('File')}>{this.getFormValue('csvFileName')}</StaticField>
<StaticField id="csvDelimiter" className={styles.formDisabled} label={t('Delimiter')}>{this.getFormValue('csvDelimiter')}</StaticField>
</div>;
} else {
settings =
settingsEdit =
<div>
<StaticField withValidation id="csvFileName" label={t('File')}><input ref={node => this.csvFile = node} type="file" onChange={::this.onFileSelected}/></StaticField>
<InputField id="csvDelimiter" label={t('Delimiter')}/>
@ -176,6 +281,57 @@ export default class CUD extends Component {
}
}
let mappingEdit;
if (isEdit) {
if (prepInProgress(status)) {
mappingEdit = <div>{t('Preparation in progress. Please wait till it is done or visit this page later.')}</div>;
} else if (runInProgress(status)) {
mappingEdit = <div>{t('Run in progress. Please wait till it is done or visit this page later.')}</div>;
} else {
const sampleRow = this.getFormValue('sampleRow');
const sourceOpts = [];
sourceOpts.push({key: '', label: t(' Select ')});
if (type === ImportType.CSV_FILE) {
for (const csvCol of settings.csv.columns) {
let help = '';
if (sampleRow) {
help = ' (' + t('e.g.:', {keySeparator: '>', nsSeparator: '|'}) + ' ' + truncate(sampleRow[csvCol.column], 50) + ')';
}
sourceOpts.push({key: csvCol.column, label: csvCol.name + help});
}
}
const mappingRows = [
<Dropdown key="email" id="mapping_email_column" label={t('Email')} options={sourceOpts}/>
];
for (const field of this.props.fieldsGrouped) {
if (field.column) {
mappingRows.push(
<Dropdown key={field.column} id={'mapping_' + field.column + '_column'} label={field.name} options={sourceOpts}/>
);
} else {
for (const option of field.settings.options) {
const col = field.groupedOptions[option.key].column;
mappingRows.push(
<Dropdown key={col} id={'mapping_' + col + '_column'} label={field.groupedOptions[option.key].name} options={sourceOpts}/>
);
}
}
}
mappingEdit = mappingRows;
}
}
let saveButtonLabel;
if (!isEdit && type === ImportType.CSV_FILE) {
saveButtonLabel = t('Save and edit mapping');
} else {
saveButtonLabel = t('Save');
}
return (
<div>
{isEdit &&
@ -201,10 +357,17 @@ export default class CUD extends Component {
<Dropdown id="type" label={t('Type')} options={this.importTypeOptions}/>
}
{settings}
{settingsEdit}
{mappingEdit &&
<Fieldset label={t('Mapping')} className={styles.mapping}>
{mappingEdit}
</Fieldset>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<Button type="submit" className="btn-primary" icon="ok" label={saveButtonLabel}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/imports/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>

View file

@ -1,15 +1,22 @@
'use strict';
import React, { Component } from 'react';
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 {translate} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
Toolbar,
withPageHelpers
} 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";
import {inProgress} from '../../../../shared/imports';
@translate()
@withPageHelpers
@ -39,12 +46,19 @@ export default class List extends Component {
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: 3, title: t('Source'), render: data => this.importTypeLabels[data], sortable: false, searchable: false },
{ data: 4, title: t('Status'), render: data => this.importStatusLabels[data], sortable: false, searchable: false },
{ data: 5, title: t('Last run'), render: data => moment(data).fromNow() },
{
actions: data => {
const actions = [];
const status = data[4];
let refreshTimeout;
if (inProgress(status)) {
refreshTimeout = 1000;
}
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports')) {
actions.push({
@ -53,7 +67,12 @@ export default class List extends Component {
});
}
return actions;
actions.push({
label: <Icon icon="eye-open" title={t('Detailed status')}/>,
link: `/lists/${this.props.list.id}/imports/${data[0]}/status`
});
return { refreshTimeout, actions };
}
}
];

View file

@ -0,0 +1,93 @@
'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 {AlignedRow} from '../../lib/form';
import {
withAsyncErrorHandler,
withErrorHandling
} from '../../lib/error-handling';
import {getImportTypes} from './helpers';
import axios from "../../lib/axios";
import {getUrl} from "../../lib/urls";
import moment from "moment";
import {runStatusInProgress} from "../../../../shared/imports";
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class Status extends Component {
constructor(props) {
super(props);
this.state = {
entity: props.entity
};
const {importTypeLabels, importStatusLabels, runStatusLabels} = getImportTypes(props.t);
this.importTypeLabels = importTypeLabels;
this.importStatusLabels = importStatusLabels;
this.runStatusLabels = runStatusLabels;
this.refreshTimeoutHandler = ::this.periodicRefreshTask;
this.refreshTimeoutId = 0;
}
static propTypes = {
entity: PropTypes.object,
imprt: PropTypes.object,
list: PropTypes.object
}
@withAsyncErrorHandler
async refreshEntity() {
const resp = await axios.get(getUrl(`rest/import-runs/${this.props.list.id}/${this.props.imprt.id}/${this.props.entity.id}`));
this.setState({
entity: resp.data
});
}
async periodicRefreshTask() {
if (runStatusInProgress(this.state.entity.status)) {
await this.refreshEntity();
this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 2000);
}
}
componentDidMount() {
this.periodicRefreshTask();
}
componentWillUnmount() {
clearTimeout(this.refreshTimeoutId);
}
render() {
const t = this.props.t;
const entity = this.state.entity;
const imprt = this.props.imprt;
return (
<div>
<Title>{t('Import Run Status')}</Title>
<AlignedRow label={t('Import name')}>{imprt.name}</AlignedRow>
<AlignedRow label={t('Import type')}>{this.importTypeLabels[imprt.type]}</AlignedRow>
<AlignedRow label={t('Run started')}>{moment(entity.created).fromNow()}</AlignedRow>
{entity.finished && <AlignedRow label={t('Run finished')}>{moment(entity.finished).fromNow()}</AlignedRow>}
<AlignedRow label={t('Run status')}>{this.runStatusLabels[entity.status]}</AlignedRow>
<AlignedRow label={t('Processed entries')}>{entity.processed}</AlignedRow>
<AlignedRow label={t('New entries')}>{entity.new}</AlignedRow>
<AlignedRow label={t('Failed entries')}>{entity.failed}</AlignedRow>
{entity.error && <AlignedRow label={t('Error')}><pre>{entity.error}</pre></AlignedRow>}
</div>
);
}
}

View file

@ -0,0 +1,165 @@
'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 {
AlignedRow,
ButtonRow,
Fieldset
} from '../../lib/form';
import {
withAsyncErrorHandler,
withErrorHandling
} from '../../lib/error-handling';
import {getImportTypes} from './helpers';
import {
prepFinishedAndNotInProgress,
runInProgress,
RunStatus,
runStatusInProgress
} from '../../../../shared/imports';
import {Table} from "../../lib/table";
import {
Button,
Icon
} from "../../lib/bootstrap-components";
import axios from "../../lib/axios";
import {getUrl} from "../../lib/urls";
import moment from "moment";
import interoperableErrors from '../../../../shared/interoperable-errors';
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class Status extends Component {
constructor(props) {
super(props);
this.state = {
entity: props.entity
};
const {importTypeLabels, importStatusLabels, runStatusLabels} = getImportTypes(props.t);
this.importTypeLabels = importTypeLabels;
this.importStatusLabels = importStatusLabels;
this.runStatusLabels = runStatusLabels;
this.refreshTimeoutHandler = ::this.periodicRefreshTask;
this.refreshTimeoutId = 0;
}
static propTypes = {
entity: PropTypes.object,
list: PropTypes.object
}
@withAsyncErrorHandler
async refreshEntity() {
const resp = await axios.get(getUrl(`rest/imports/${this.props.list.id}/${this.props.entity.id}`));
this.setState({
entity: resp.data
});
}
async periodicRefreshTask() {
// The periodic task runs all the time, so that we don't have to worry about starting/stopping it as a reaction to the buttons.
await this.refreshEntity();
this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 2000);
}
componentDidMount() {
this.periodicRefreshTask();
}
componentWillUnmount() {
clearTimeout(this.refreshTimeoutId);
}
async startRunAsync() {
try {
await axios.post(getUrl(`rest/import-start/${this.props.list.id}/${this.props.entity.id}`));
} catch (err) {
if (err instanceof interoperableErrors.InvalidStateError) {
// Just mask the fact that it's not possible to start anything and refresh instead.
} else {
throw err;
}
}
await this.refreshEntity();
}
async stopRunAsync() {
try {
await axios.post(getUrl(`rest/import-stop/${this.props.list.id}/${this.props.entity.id}`));
} catch (err) {
if (err instanceof interoperableErrors.InvalidStateError) {
// Just mask the fact that it's not possible to stop anything and refresh instead.
} else {
throw err;
}
}
await this.refreshEntity();
}
render() {
const t = this.props.t;
const entity = this.state.entity;
const columns = [
{ data: 1, title: t('Started'), render: data => moment(data).fromNow() },
{ data: 2, title: t('Finished'), render: data => data ? moment(data).fromNow() : '' },
{ data: 3, title: t('Status'), render: data => this.runStatusLabels[data], sortable: false, searchable: false },
{ data: 4, title: t('Processed') },
{ data: 5, title: t('New') },
{ data: 6, title: t('Failed') },
{
actions: data => {
const actions = [];
const status = data[3];
let refreshTimeout;
if (runStatusInProgress(status)) {
refreshTimeout = 1000;
}
actions.push({
label: <Icon icon="eye-open" title={t('Run status')}/>,
link: `/lists/${this.props.list.id}/imports/${this.props.entity.id}/status/${data[0]}`
});
return { refreshTimeout, actions };
}
}
];
return (
<div>
<Title>{t('Import Status')}</Title>
<AlignedRow label={t('Name')}>{entity.name}</AlignedRow>
<AlignedRow label={t('Type')}>{this.importTypeLabels[entity.type]}</AlignedRow>
<AlignedRow label={t('Status')}>{this.importStatusLabels[entity.status]}</AlignedRow>
{entity.error && <AlignedRow label={t('Error')}><pre>{entity.error}</pre></AlignedRow>}
<ButtonRow label={t('Actions')}>
{prepFinishedAndNotInProgress(entity.status) && <Button className="btn-primary" icon="play" label={t('Start')} onClickAsync={::this.startRunAsync}/>}
{runInProgress(entity.status) && <Button className="btn-primary" icon="stop" label={t('Stop')} onClickAsync={::this.stopRunAsync}/>}
</ButtonRow>
<hr/>
<h3>{t('Import Runs')}</h3>
<Table withHeader dataUrl={`rest/import-runs-table/${this.props.list.id}/${this.props.entity.id}`} columns={columns} />
</div>
);
}
}

View file

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import {ImportType, ImportStatus} from '../../../../shared/imports';
import {ImportType, ImportStatus, RunStatus} from '../../../../shared/imports';
export function getImportTypes(t) {
@ -13,17 +13,27 @@ export function getImportTypes(t) {
const importStatusLabels = {
[ImportStatus.PREP_SCHEDULED]: t('Created'),
[ImportStatus.PREP_RUNNING]: t('Preparing'),
[ImportStatus.PREP_STOPPING]: t('Stopping'),
[ImportStatus.PREP_FINISHED]: t('Ready'),
[ImportStatus.PREP_FAILED]: t('Preparation failed'),
[ImportStatus.RUN_SCHEDULED]: t('Scheduled'),
[ImportStatus.RUN_RUNNING]: t('Running'),
[ImportStatus.RUN_STOPPING]: t('Stopping'),
[ImportStatus.RUN_FINISHED]: t('Finished'),
[ImportStatus.RUN_FAILED]: t('Failed')
};
const runStatusLabels = {
[RunStatus.SCHEDULED]: t('Starting'),
[RunStatus.RUNNING]: t('Running'),
[RunStatus.STOPPING]: t('Stopping'),
[RunStatus.FINISHED]: t('Finished')
};
return {
importStatusLabels,
importTypeLabels
importTypeLabels,
runStatusLabels
};
}

View file

@ -14,6 +14,8 @@ import SegmentsList from './segments/List';
import SegmentsCUD from './segments/CUD';
import ImportsList from './imports/List';
import ImportsCUD from './imports/CUD';
import ImportsStatus from './imports/Status';
import ImportRunsStatus from './imports/RunStatus';
import Share from '../shares/Share';
import TriggersList from './TriggersList';
@ -140,18 +142,36 @@ function getMenus(t) {
resolve: {
import: params => `rest/imports/${params.listId}/${params.importId}`,
},
link: params => `/lists/${params.listId}/imports/${params.importId}/edit`,
link: params => `/lists/${params.listId}/imports/${params.importId}/status`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
resolve: {
fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
},
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} />
panelRender: props => <ImportsCUD action={props.match.params.action} entity={props.resolved.import} list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped}/>
},
'status': {
title: t('Status'),
link: params => `/lists/${params.listId}/imports/${params.importId}/status`,
panelRender: props => <ImportsStatus entity={props.resolved.import} list={props.resolved.list} />,
children: {
':importRunId([0-9]+)': {
title: resolved => t('Run'),
resolve: {
importRun: params => `rest/import-runs/${params.listId}/${params.importId}/${params.importRunId}`,
},
link: params => `/lists/${params.listId}/imports/${params.importId}/status/${params.importRunId}`,
panelRender: props => <ImportRunsStatus entity={props.resolved.importRun} imprt={props.resolved.import} list={props.resolved.list} />
}
}
}
}
},
create: {
title: t('Create'),
panelRender: props => <ImportsCUD action="create" list={props.resolved.list} imports={props.resolved.imports} />
panelRender: props => <ImportsCUD action="create" list={props.resolved.list} />
}
}
},

View file

@ -0,0 +1,3 @@
.mapping {
margin-top: 30px;
}

View file

@ -15,6 +15,7 @@ import {
import {Icon, Button} from "../../lib/bootstrap-components";
import axios from '../../lib/axios';
import {getFieldTypes, getSubscriptionStatusLabels} from './helpers';
import {getUrl} from "../../lib/urls";
@translate()
@withForm