Add column selection to data table

[MAILPOET-1809]
This commit is contained in:
Pavel Dohnal
2019-07-03 14:05:59 +02:00
committed by M. Shull
parent 5556c2f360
commit 72fa226cee
4 changed files with 220 additions and 347 deletions

View File

@@ -158,269 +158,6 @@ jQuery(document).ready(() => {
? data
: new Handlebars.SafeString(Handlebars.Utils.escapeExpression(data))));
// start array index from 1
Handlebars.registerHelper('calculate_index', (rawIndex) => {
const index = parseInt(rawIndex, 10);
// display filler data (e.g., ellipsis) if we've reached the maximum number of rows and
// subscribers count is greater than the maximum number of rows we're displaying
if (index === maxRowsToShow && subscribers.subscribersCount > (maxRowsToShow + 1)) {
fillerPosition = index;
return filler;
}
if (index === (subscribers.subscribers.length - 1)) {
// if we're on the last line, show the total count of subscribers data
return subscribers.subscribersCount.toLocaleString();
}
return index + 1;
});
// reduce subscribers object if the total length is greater than the
// maximum number of defined rows
if (subscribers.subscribersCount > (maxRowsToShow + 1)) {
subscribers.subscribers.splice(
maxRowsToShow, subscribers.subscribersCount - (maxRowsToShow + 1),
fillerArray
);
}
// filter subscribers' data to detect dates, emails, etc.
function filterSubscribers() {
const subscribersClone = jQuery.extend(true, {}, subscribers);
let preventNextStep = false;
jQuery(
'[data-id="notice_invalidEmail"], [data-id="notice_invalidDate"]'
)
.remove();
const displayedColumns = jQuery.map(
jQuery('.mailpoet_subscribers_column_data_match'), (element, elementIndex) => {
const columnId = jQuery(element).data('column-id');
const validationRule = jQuery(element).data('validation-rule');
jQuery(element).val(columnId).trigger('change');
return {
id: columnId,
index: elementIndex,
validationRule,
element,
};
}
);
// iterate through the object of mailpoet columns
jQuery.map(window.mailpoetColumns, (column) => {
let firstRowData;
let validationRule;
let testedFormat;
let allowedDateFormats;
// check if the column id matches the selected id of one of the
// subscriber's data columns
const matchedColumn = _.find(
displayedColumns,
data => data.id === column.id
);
// EMAIL filter: if the first value in the column doesn't have a valid
// email, hide the next button
if (column.id === 'email') {
if (!window.mailpoet_email_regex.test(
subscribersClone.subscribers[0][matchedColumn.index]
)
) {
preventNextStep = true;
if (!jQuery('[data-id="notice_invalidEmail"]').length) {
MailPoet.Notice.error(MailPoet.I18n.t('columnContainsInvalidElement'), {
static: true,
scroll: true,
hideClose: true,
id: 'invalidEmail',
});
}
} else {
MailPoet.Notice.hide('invalidEmail');
}
}
// DATE filter: if column type is date, check if we can recognize it
if (column.type === 'date' && matchedColumn) {
allowedDateFormats = [
Moment.ISO_8601,
'YYYY/MM/DD',
'MM/DD/YYYY',
'DD/MM/YYYY',
'YYYY/MM/DD',
'YYYY/DD/MM',
'MM/YYYY',
'YYYY/MM',
'YYYY',
];
firstRowData = subscribersClone.subscribers[0][matchedColumn.index];
validationRule = false;
// check if date exists
if (firstRowData.trim() === '') {
subscribersClone.subscribers[0][matchedColumn.index] = `<span class="mailpoet_data_match mailpoet_import_error" title="${MailPoet.I18n.t('noDateFieldMatch')}">${MailPoet.I18n.t('emptyFirstRowDate')}</span> `;
preventNextStep = true;
} else {
Object.keys(allowedDateFormats).forEach((format) => {
testedFormat = allowedDateFormats[format];
if (Moment(firstRowData, testedFormat, true).isValid()) {
validationRule = (typeof (testedFormat) === 'function')
? 'datetime'
: testedFormat;
// set validation on the column element
jQuery(matchedColumn.element).data('validation-rule', validationRule);
return;
}
if (validationRule === 'datetime') {
validationRule = Moment.ISO_8601;
}
});
}
jQuery.map(subscribersClone.subscribers, (dataSubscribers, index) => {
const data = dataSubscribers;
const rowData = data[matchedColumn.index];
const date = Moment(rowData, testedFormat, true);
if (index === fillerPosition || rowData.trim() === '') return;
// validate date
if (date.isValid()) {
data[matchedColumn.index] = new Handlebars.SafeString(
`${Handlebars.Utils.escapeExpression(data[matchedColumn.index])}<span class="mailpoet_data_match" title="${MailPoet.I18n.t('verifyDateMatch')}">${MailPoet.Date.format(date)}</span> `
);
} else {
data[matchedColumn.index] = new Handlebars.SafeString(
`${Handlebars.Utils.escapeExpression(data[matchedColumn.index])}<span class="mailpoet_data_match mailpoet_import_error" title="${MailPoet.I18n.t('noDateFieldMatch')}">${new Handlebars.SafeString(MailPoet.I18n.t('dateMatchError'))}</span> `
);
preventNextStep = true;
}
});
if (preventNextStep && !jQuery('.mailpoet_invalidDate').length) {
MailPoet.Notice.error(MailPoet.I18n.t('columnContainsInvalidDate'), {
static: true,
scroll: true,
hideClose: true,
id: 'invalidDate',
});
}
}
});
// refresh table with susbcribers' data
jQuery('#subscribers_data > table > tbody')
.html(subscribersDataTemplatePartial(subscribersClone));
if (preventNextStep) {
toggleNextStepButton('off');
} else if (!jQuery('.mailpoet_notice.error:visible').length
&& segmentSelectElement.val()) {
toggleNextStepButton('on');
}
}
// render template
jQuery('#subscribers_data > table').html(subscribersDataTemplate(subscribers));
// filter displayed data
jQuery('select.mailpoet_subscribers_column_data_match')
.select2({
data: window.mailpoetColumnsSelect2,
width: '15em',
templateResult(item) {
return item.name;
},
templateSelection(item) {
return item.name;
},
})
.on('select2:selecting', (selectEvent) => {
const selectElement = selectEvent.currentTarget;
const selectedOptionId = selectEvent.params.args.data.id;
// CREATE CUSTOM FIELD
if (selectedOptionId === 'create') {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
MailPoet.Modal.popup({
title: MailPoet.I18n.t('addNewField'),
template: jQuery('#form_template_field_form').html(),
});
jQuery('#form_field_new').parsley().on('form:submit', () => {
// get data
const data = jQuery('#form_field_new').mailpoetSerializeObject();
// save custom field
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'customFields',
action: 'save',
data,
}).done((response) => {
const newColumnData = {
id: response.data.id,
name: response.data.name,
type: response.data.type,
params: response.data.params,
custom: true,
};
// if this is the first custom column, create an "optgroup"
if (window.mailpoetColumnsSelect2.length === 2) {
window.mailpoetColumnsSelect2.push({
name: MailPoet.I18n.t('userColumns'),
children: [],
});
}
window.mailpoetColumnsSelect2[2].children.push(newColumnData);
window.mailpoetColumns.push(newColumnData);
jQuery('select.mailpoet_subscribers_column_data_match')
.each(() => {
jQuery(selectElement)
.html('')
.select2('destroy')
.select2({
data: window.mailpoetColumnsSelect2,
width: '15em',
templateResult(item) {
return item.name;
},
templateSelection(item) {
return item.name;
},
});
});
jQuery(selectElement).data('column-id', newColumnData.id);
jQuery(selectElement).data('validation-rule', false);
filterSubscribers();
// close popup
MailPoet.Modal.close();
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(error => error.message),
{ positionAfter: '#field_name' }
);
}
});
return false;
});
} else {
// CHANGE COLUMN
// check for duplicate values in all select options
jQuery('select.mailpoet_subscribers_column_data_match')
.each(() => {
const element = selectElement;
const elementId = jQuery(element).val();
// if another column has the same value and it's not an 'ignore',
// prompt user
if (elementId === selectedOptionId
&& elementId !== 'ignore') {
if (confirm(`${MailPoet.I18n.t('selectedValueAlreadyMatched')} ${MailPoet.I18n.t('confirmCorrespondingColumn')}`)) { // eslint-disable-line
jQuery(element).data('column-id', 'ignore');
} else {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
}
}
});
}
})
.on('select2:select', (selectEvent) => {
const selectElement = selectEvent.currentTarget;
const selectedOptionId = selectEvent.params.data.id;
jQuery(selectElement).data('column-id', selectedOptionId);
filterSubscribers();
});
nextStepButton.off().on('click', (event) => {

View File

@@ -4,6 +4,7 @@ import { withRouter } from 'react-router-dom';
import PreviousNextStepButtons from './previous_next_step_buttons.jsx';
import Warnings from './step_data_manipulation/warnings.jsx';
import MatchTable from './step_data_manipulation/match_table.jsx';
import SelectSegment from './step_data_manipulation/select_segment.jsx';
function getPreviousStepLink(importData, subscribersLimitForValidation) {
if (importData === undefined) {
@@ -46,6 +47,7 @@ function StepDataManipulation({
subscribers={stepMethodSelectionData.subscribers}
header={stepMethodSelectionData.header}
/>
<SelectSegment />
</div>
<PreviousNextStepButtons
canGoNext={false}

View File

@@ -0,0 +1,120 @@
import jQuery from 'jquery';
import MailPoet from 'mailpoet';
export default (selectColumnType) => {
jQuery('select.mailpoet_subscribers_column_data_match')
.select2({
data: window.mailpoetColumnsSelect2,
width: '15em',
templateResult(item) {
return item.name;
},
templateSelection(item) {
return item.name;
},
})
.on('select2:selecting', (selectEvent) => {
const selectElement = selectEvent.currentTarget;
const selectedOptionId = selectEvent.params.args.data.id;
// CREATE CUSTOM FIELD
if (selectedOptionId === 'create') {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
MailPoet.Modal.popup({
title: MailPoet.I18n.t('addNewField'),
template: jQuery('#form_template_field_form').html(),
});
jQuery('#form_field_new').parsley().on('form:submit', () => {
// get data
const data = jQuery('#form_field_new').mailpoetSerializeObject();
// save custom field
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'customFields',
action: 'save',
data,
}).done((response) => {
const newColumnData = {
id: response.data.id,
name: response.data.name,
type: response.data.type,
params: response.data.params,
custom: true,
};
// if this is the first custom column, create an "optgroup"
if (window.mailpoetColumnsSelect2.length === 2) {
window.mailpoetColumnsSelect2.push({
name: MailPoet.I18n.t('userColumns'),
children: [],
});
}
window.mailpoetColumnsSelect2[2].children.push(newColumnData);
window.mailpoetColumns.push(newColumnData);
jQuery('select.mailpoet_subscribers_column_data_match')
.each(() => {
jQuery(selectElement)
.html('')
.select2('destroy')
.select2({
data: window.mailpoetColumnsSelect2,
width: '15em',
templateResult(item) {
return item.name;
},
templateSelection(item) {
return item.name;
},
});
});
jQuery(selectElement).data('column-id', newColumnData.id);
jQuery(selectElement).data('validation-rule', false);
const columnIndex = jQuery(selectElement).data('column-index');
selectColumnType(newColumnData.id, columnIndex);
// close popup
MailPoet.Modal.close();
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(error => error.message),
{ positionAfter: '#field_name' }
);
}
});
return false;
});
} else {
// CHANGE COLUMN
// check for duplicate values in all select options
jQuery('select.mailpoet_subscribers_column_data_match')
.each(() => {
const element = selectElement;
const elementId = jQuery(element).val();
// if another column has the same value and it's not an 'ignore',
// prompt user
if (elementId === selectedOptionId
&& elementId !== 'ignore') {
if (confirm(`${MailPoet.I18n.t('selectedValueAlreadyMatched')} ${MailPoet.I18n.t('confirmCorrespondingColumn')}`)) { // eslint-disable-line
jQuery(element).data('column-id', 'ignore');
} else {
selectEvent.preventDefault();
jQuery(selectElement).select2('close');
}
}
});
}
})
.on('select2:select', (selectEvent) => {
const selectElement = selectEvent.currentTarget;
const selectedOptionId = selectEvent.params.data.id;
jQuery(selectElement).data('column-id', selectedOptionId);
const columnIndex = jQuery(selectElement).data('column-index');
selectColumnType(selectedOptionId, columnIndex);
});
jQuery.map(
jQuery('.mailpoet_subscribers_column_data_match'), (element) => {
const columnId = jQuery(element).data('column-id');
jQuery(element).val(columnId).trigger('change');
}
);
};

View File

@@ -1,106 +1,120 @@
import React from 'react';
import React, { useLayoutEffect } from 'react';
import PropTypes from 'prop-types';
import MailPoet from 'mailpoet';
import _ from 'underscore';
import generateColumnSelection from './generate_column_selection.jsx';
import matchColumns from './match_columns.jsx';
const MAX_SUBSCRIBERS_SHOWN = 10;
function ColumnDataMatch({ columnTypes }) {
return (
<tr>
<th>{MailPoet.I18n.t('matchData')}</th>
{/* eslint-disable-next-line react/no-array-index-key */}
{columnTypes.map((columnType, i) => <th key={columnType.column_id + i}>{columnType.column_id}</th>)}
</tr>
);
}
ColumnDataMatch.propTypes = {
columnTypes: PropTypes.arrayOf(PropTypes.shape({ column_id: PropTypes.string })).isRequired,
};
function Header({ header }) {
return (
<tr className="mailpoet_header">
<td />
{header.map(headerName => <td key={headerName}>{headerName}</td>)}
</tr>
);
}
Header.propTypes = {
header: PropTypes.arrayOf(PropTypes.string).isRequired,
};
function Subscriber({ subscriber, index }) {
return (
<>
<td>{index}</td>
{/* eslint-disable-next-line react/no-array-index-key */}
{subscriber.map((field, i) => <td key={field + index + i}>{field}</td>)}
</>
);
}
Subscriber.propTypes = {
subscriber: PropTypes.arrayOf(PropTypes.string).isRequired,
index: PropTypes.node.isRequired,
};
function Subscribers({ subscribers, subscribersCount }) {
const filler = '. . .';
const fillerArray = Array(subscribers[0].length).fill(filler);
return (
<>
{
subscribers
.slice(0, MAX_SUBSCRIBERS_SHOWN)
.map((subscriber, i) => (
<tr key={subscriber[0]}>
<Subscriber subscriber={subscriber} index={i + 1} />
</tr>
))
}
{
subscribersCount > MAX_SUBSCRIBERS_SHOWN + 1
? <tr key="filler"><Subscriber subscriber={fillerArray} index={filler} /></tr>
: null
}
{
subscribersCount > MAX_SUBSCRIBERS_SHOWN
? (
<tr key={subscribers[subscribersCount - 1][0]}>
<Subscriber subscriber={subscribers[subscribersCount - 1]} index={subscribersCount} />
</tr>
)
: null
}
</>
);
}
Subscribers.propTypes = {
subscribersCount: PropTypes.number.isRequired,
subscribers: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
};
function MatchTable({
subscribersCount,
subscribers,
header,
}) {
const matchedColumnTypes = matchColumns(subscribers, header);
let selectedColumns = [];
useLayoutEffect(() => {
generateColumnSelection((selectedOptionId, columnIndex) => {
selectedColumns[columnIndex] = selectedOptionId;
});
});
function ColumnDataMatch() {
const matchedColumnTypes = matchColumns(subscribers, header);
selectedColumns = _.pluck(matchedColumnTypes, 'column_id');
return (
<tr>
<th>{MailPoet.I18n.t('matchData')}</th>
{
matchedColumnTypes.map((columnType, i) => {
return (
// eslint-disable-next-line react/no-array-index-key
<th key={columnType.column_id + i}>
<select
className="mailpoet_subscribers_column_data_match"
data-column-id={columnType.column_id}
data-validation-rule="false"
data-column-index={i}
id={`column_${i}`}
/>
</th>
);
})
}
</tr>
);
}
function Header() {
return (
<tr className="mailpoet_header">
<td />
{header.map(headerName => <td key={headerName}>{headerName}</td>)}
</tr>
);
}
function Subscriber({ subscriber, index }) {
return (
<>
<td>{index}</td>
{/* eslint-disable-next-line react/no-array-index-key */}
{subscriber.map((field, i) => <td key={field + index + i}>{field}</td>)}
</>
);
}
Subscriber.propTypes = {
subscriber: PropTypes.arrayOf(PropTypes.string).isRequired,
index: PropTypes.node.isRequired,
};
function Subscribers() {
const filler = '. . .';
const fillerArray = Array(subscribers[0].length).fill(filler);
return (
<>
{
subscribers
.slice(0, MAX_SUBSCRIBERS_SHOWN)
.map((subscriber, i) => (
<tr key={subscriber[0]}>
<Subscriber subscriber={subscriber} index={i + 1} />
</tr>
))
}
{
subscribersCount > MAX_SUBSCRIBERS_SHOWN + 1
? <tr key="filler"><Subscriber subscriber={fillerArray} index={filler} /></tr>
: null
}
{
subscribersCount > MAX_SUBSCRIBERS_SHOWN
? (
<tr key={subscribers[subscribersCount - 1][0]}>
<Subscriber
subscriber={subscribers[subscribersCount - 1]}
index={subscribersCount}
/>
</tr>
)
: null
}
</>
);
}
return (
<div className="subscribers_data">
<table className="mailpoet_subscribers widefat fixed">
<thead>
<ColumnDataMatch columnTypes={matchedColumnTypes} />
<ColumnDataMatch />
</thead>
<tbody>
<Header header={header} />
<Subscribers subscribers={subscribers} subscribersCount={subscribersCount} />
<Header />
<Subscribers />
</tbody>
</table>
</div>