diff --git a/app-builder.js b/app-builder.js index 35156252..e0b26a64 100644 --- a/app-builder.js +++ b/app-builder.js @@ -40,6 +40,7 @@ const listsRest = require('./routes/rest/lists'); const formsRest = require('./routes/rest/forms'); const fieldsRest = require('./routes/rest/fields'); const importsRest = require('./routes/rest/imports'); +const importRunsRest = require('./routes/rest/import-runs'); const sharesRest = require('./routes/rest/shares'); const segmentsRest = require('./routes/rest/segments'); const subscriptionsRest = require('./routes/rest/subscriptions'); @@ -283,6 +284,7 @@ function createApp(trusted) { app.use('/rest', formsRest); app.use('/rest', fieldsRest); app.use('/rest', importsRest); + app.use('/rest', importRunsRest); app.use('/rest', sharesRest); app.use('/rest', segmentsRest); app.use('/rest', subscriptionsRest); diff --git a/client/src/lib/form.js b/client/src/lib/form.js index 8e3670cb..d49ffa4b 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -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 => ); - } else if (this.props.optGroups) { - options = props.optGroups.map(optGroup => - - {optGroup.options.map(option => )} - - ); + for (const optOrGrp of props.options) { + if (optOrGrp.options) { + options.push( + + {optOrGrp.options.map(opt => )} + + ) + } else { + options.push() + } + } } 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 = { diff --git a/client/src/lib/styles.scss b/client/src/lib/styles.scss index 68e8ba55..3232f1f7 100644 --- a/client/src/lib/styles.scss +++ b/client/src/lib/styles.scss @@ -27,6 +27,10 @@ } } +.staticFormGroup { + margin-bottom: 15px; +} + .dayPickerWrapper { text-align: right; } diff --git a/client/src/lib/table.js b/client/src/lib/table.js index ff6815a6..6eb3c554 100644 --- a/client/src/lib/table.js +++ b/client/src/lib/table.js @@ -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]); diff --git a/client/src/lists/List.js b/client/src/lists/List.js index 03d98bf1..715a65e9 100644 --- a/client/src/lists/List.js +++ b/client/src/lists/List.js @@ -95,7 +95,7 @@ export default class List extends Component { if (perms.includes('viewImports')) { actions.push({ - label: , + label: , link: `/lists/${data[0]}/imports` }); } diff --git a/client/src/lists/forms/CUD.js b/client/src/lists/forms/CUD.js index a6ad263f..ff056b51 100644 --- a/client/src/lists/forms/CUD.js +++ b/client/src/lists/forms/CUD.js @@ -427,7 +427,7 @@ export default class CUD extends Component { { selectedTemplate &&
- +
} diff --git a/client/src/lists/imports/CUD.js b/client/src/lists/imports/CUD.js index 0f949d89..b3897f05 100644 --- a/client/src/lists/imports/CUD.js +++ b/client/src/lists/imports/CUD.js @@ -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 =
{this.getFormValue('csvFileName')} {this.getFormValue('csvDelimiter')}
; } else { - settings = + settingsEdit =
this.csvFile = node} type="file" onChange={::this.onFileSelected}/> @@ -176,6 +281,57 @@ export default class CUD extends Component { } } + let mappingEdit; + if (isEdit) { + if (prepInProgress(status)) { + mappingEdit =
{t('Preparation in progress. Please wait till it is done or visit this page later.')}
; + } else if (runInProgress(status)) { + mappingEdit =
{t('Run in progress. Please wait till it is done or visit this page later.')}
; + } 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 = [ + + ]; + + for (const field of this.props.fieldsGrouped) { + if (field.column) { + mappingRows.push( + + ); + } else { + for (const option of field.settings.options) { + const col = field.groupedOptions[option.key].column; + mappingRows.push( + + ); + } + } + } + + mappingEdit = mappingRows; + } + } + + let saveButtonLabel; + if (!isEdit && type === ImportType.CSV_FILE) { + saveButtonLabel = t('Save and edit mapping'); + } else { + saveButtonLabel = t('Save'); + } + return (
{isEdit && @@ -201,10 +357,17 @@ export default class CUD extends Component { } - {settings} + {settingsEdit} + + {mappingEdit && +
+ {mappingEdit} +
+ } + -