Field setup wizard for new list - addresses 1st line of #510

Bugfixes to address #511
This commit is contained in:
Tomas Bures 2018-12-31 09:45:59 +00:00
parent de55870561
commit b26f5008da
10 changed files with 144 additions and 59 deletions

View file

@ -107,8 +107,6 @@ export default class StatisticsOpened extends Component {
{ data: 5, title: t('opensCount') } { data: 5, title: t('opensCount') }
]; ];
console.log(this.state.statisticsOpened);
const renderNavPill = (key, label) => ( const renderNavPill = (key, label) => (
<li role="presentation" className={agg === key ? 'active' : ''}> <li role="presentation" className={agg === key ? 'active' : ''}>
<Link to={`/campaigns/${entity.id}/statistics/opened/${key}`}>{label}</Link> <Link to={`/campaigns/${entity.id}/statistics/opened/${key}`}>{label}</Link>
@ -223,8 +221,6 @@ export default class StatisticsOpened extends Component {
); );
} }
console.log(mailtrainConfig);
return ( return (
<div> <div>
<Title>{t('detailedStatistics')}</Title> <Title>{t('detailedStatistics')}</Title>

View file

@ -1,3 +1,5 @@
@import "../../static/scss/variables";
.form { // This is here to give the styles below higher priority than Bootstrap has .form { // This is here to give the styles below higher priority than Bootstrap has
:global .DayPicker { :global .DayPicker {
border-left: 1px solid lightgray; border-left: 1px solid lightgray;
@ -148,7 +150,8 @@
} }
.iconDisabled { .iconDisabled {
color: #bf3e11; color: $link-color;
text-decoration: $link-decoration;
} }
.dependenciesList { .dependenciesList {

View file

@ -30,7 +30,7 @@ import {
NamespaceSelect, NamespaceSelect,
validateNamespace validateNamespace
} from '../lib/namespace'; } from '../lib/namespace';
import {UnsubscriptionMode} from '../../../shared/lists'; import {UnsubscriptionMode, FieldWizard} from '../../../shared/lists';
import styles import styles
from "../lib/styles.scss"; from "../lib/styles.scss";
import mailtrainConfig import mailtrainConfig
@ -78,7 +78,8 @@ export default class CUD extends Component {
homepage: '', homepage: '',
unsubscription_mode: UnsubscriptionMode.ONE_STEP, unsubscription_mode: UnsubscriptionMode.ONE_STEP,
namespace: mailtrainConfig.user.namespace, namespace: mailtrainConfig.user.namespace,
to_name: '[MERGE_FIRST_NAME] [MERGE_LAST_NAME]', to_name: '',
fieldWizard: FieldWizard.FIRST_LAST_NAME,
send_configuration: null, send_configuration: null,
listunsubscribe_disabled: false listunsubscribe_disabled: false
}); });
@ -129,6 +130,10 @@ export default class CUD extends Component {
data.default_form = null; data.default_form = null;
} }
delete data.form; delete data.form;
if (data.fieldWizard === FieldWizard.FIRST_LAST_NAME || data.fieldWizard === FieldWizard.NAME) {
data.to_name = null;
}
}); });
if (submitSuccessful) { if (submitSuccessful) {
@ -193,6 +198,32 @@ export default class CUD extends Component {
{ data: 6, title: t('namespace') } { data: 6, title: t('namespace') }
]; ];
let toNameFields;
if (isEdit) {
toNameFields = <InputField id="to_name" label={t('recipientsNameTemplate')} help={t('specifyUsingMergeTagsOfThisListHowTo')}/>;
} else {
const fieldWizardOptions = [
{key: FieldWizard.NONE, label: t('Empty / Custom (no fields)')},
{key: FieldWizard.NAME, label: t('Name (one field)')},
{key: FieldWizard.FIRST_LAST_NAME, label: t('First name and Last name (two fields)')},
];
const fieldWizardValue = this.getFormValue('fieldWizard');
const fieldWizardSelector = <Dropdown id="fieldWizard" label={t('Representation of subscriber\'s name')} options={fieldWizardOptions} help={t('Select how the name of a subscriber will be represented. The fields in list will be created accordingly. You can always adjust the choice later by editing the list fields. If you select "Empty / Custom", provide a template below in "Recipients name template" that will be used as subscriber\'s name as it will appear in the emails\' "To" field.')}/>
if (fieldWizardValue === FieldWizard.NONE) {
toNameFields = (
<>
{fieldWizardSelector}
<InputField id="to_name" label={t('recipientsNameTemplate')} help={t('specifyUsingMergeTagsOfThisListHowTo')}/>
</>
);
} else {
toNameFields = fieldWizardSelector;
}
}
return ( return (
<div> <div>
{canDelete && {canDelete &&
@ -221,7 +252,7 @@ export default class CUD extends Component {
<InputField id="contact_email" label={t('contactEmail')} help={t('contactEmailUsedInSubscriptionFormsAnd')}/> <InputField id="contact_email" label={t('contactEmail')} help={t('contactEmailUsedInSubscriptionFormsAnd')}/>
<InputField id="homepage" label={t('homepage')} help={t('homepageUrlUsedInSubscriptionFormsAnd')}/> <InputField id="homepage" label={t('homepage')} help={t('homepageUrlUsedInSubscriptionFormsAnd')}/>
<InputField id="to_name" label={t('recipientsNameTemplate')} help={t('specifyUsingMergeTagsOfThisListHowTo')}/> {toNameFields}
<TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('sendConfigurationThatWillBeUsedFor')}/> <TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('sendConfigurationThatWillBeUsedFor')}/>
<NamespaceSelect/> <NamespaceSelect/>

View file

@ -71,6 +71,10 @@ export default class CUD extends Component {
}; };
this.initForm(); this.initForm();
this.onRuleSettingsPaneUpdatedHandler = ::this.onRuleSettingsPaneUpdated;
this.onRuleSettingsPaneCloseHandler = ::this.onRuleSettingsPaneClose;
this.onRuleSettingsPaneDeleteHandler = ::this.onRuleSettingsPaneDelete;
} }
static propTypes = { static propTypes = {
@ -334,7 +338,6 @@ export default class CUD extends Component {
} }
} }
return ( return (
<div> <div>
@ -411,7 +414,7 @@ export default class CUD extends Component {
<div className={styles.rightPane}> <div className={styles.rightPane}>
<div className={styles.rightPaneInner}> <div className={styles.rightPaneInner}>
{selectedRule && {selectedRule &&
<RuleSettingsPane rule={selectedRule} fields={this.props.fields} onChange={::this.onRuleSettingsPaneUpdated} onClose={::this.onRuleSettingsPaneClose} onDelete={::this.onRuleSettingsPaneDelete} forceShowValidation={this.isFormValidationShown()}/>} <RuleSettingsPane rule={selectedRule} fields={this.props.fields} onChange={this.onRuleSettingsPaneUpdatedHandler} onClose={this.onRuleSettingsPaneCloseHandler} onDelete={this.onRuleSettingsPaneDeleteHandler} forceShowValidation={this.isFormValidationShown()}/>}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import React, {Component} from "react"; import React, {PureComponent} from "react";
import PropTypes import PropTypes
from "prop-types"; from "prop-types";
import {withTranslation} from '../../lib/i18n'; import {withTranslation} from '../../lib/i18n';
@ -31,7 +31,7 @@ import {withComponentMixins} from "../../lib/decorator-helpers";
withPageHelpers, withPageHelpers,
requiresAuthenticatedUser requiresAuthenticatedUser
]) ])
export default class CUD extends Component { export default class RuleSettingsPane extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -136,7 +136,7 @@ export default class CUD extends Component {
if (type) { if (type) {
const settings = ruleHelpers.primitiveRuleTypes[colType][type]; const settings = ruleHelpers.primitiveRuleTypes[colType][type];
if (!settings) { if (!settings) {
// The existing rule type does not fit the newly changed column. This resets the rule type chooser to "-- Select ---" // The existing rule type does not fit the newly changed column. This resets the rule type chooser to "--- Select ---"
mutState.setIn(['type', 'value'], ''); mutState.setIn(['type', 'value'], '');
} }
} }
@ -212,7 +212,7 @@ export default class CUD extends Component {
const ruleType = this.getFormValue('type'); const ruleType = this.getFormValue('type');
if (ruleType) { if (ruleType) {
ruleSettings = ruleHelpers.primitiveRuleTypes[colType][ruleType].form; ruleSettings = ruleHelpers.primitiveRuleTypes[colType][ruleType].getForm();
} }
} }
} }
@ -226,8 +226,6 @@ export default class CUD extends Component {
</div>; </div>;
} }
return ( return (
<div className={styles.ruleOptions}> <div className={styles.ruleOptions}>
<h3>{t('ruleOptions')}</h3> <h3>{t('ruleOptions')}</h3>

View file

@ -216,7 +216,7 @@ export function getRuleHelpers(t, fields) {
const stringValueSettings = allowEmpty => ({ const stringValueSettings = allowEmpty => ({
form: <InputField id="value" label={t('value')} />, getForm: () => <InputField id="value" label={t('value')} />,
getFormData: rule => ({ getFormData: rule => ({
value: rule.value value: rule.value
}), }),
@ -233,7 +233,7 @@ export function getRuleHelpers(t, fields) {
}); });
const numberValueSettings = { const numberValueSettings = {
form: <InputField id="value" label={t('value')} />, getForm: () => <InputField id="value" label={t('value')} />,
getFormData: rule => ({ getFormData: rule => ({
value: rule.value.toString() value: rule.value.toString()
}), }),
@ -253,7 +253,7 @@ export function getRuleHelpers(t, fields) {
}; };
const birthdayValueSettings = { const birthdayValueSettings = {
form: <DatePicker id="birthday" label={t('date')} birthday />, getForm: () => <DatePicker id="birthday" label={t('date')} birthday />,
getFormData: rule => ({ getFormData: rule => ({
birthday: formatBirthday(DateFormat.INTL, rule.value) birthday: formatBirthday(DateFormat.INTL, rule.value)
}), }),
@ -274,7 +274,7 @@ export function getRuleHelpers(t, fields) {
}; };
const dateValueSettings = { const dateValueSettings = {
form: <DatePicker id="date" label={t('date')} />, getForm: () => <DatePicker id="date" label={t('date')} />,
getFormData: rule => ({ getFormData: rule => ({
date: formatDate(DateFormat.INTL, rule.value) date: formatDate(DateFormat.INTL, rule.value)
}), }),
@ -295,7 +295,7 @@ export function getRuleHelpers(t, fields) {
}; };
const dateRelativeValueSettings = { const dateRelativeValueSettings = {
form: getForm: () =>
<div> <div>
<InputField id="daysValue" label={t('numberOfDays')}/> <InputField id="daysValue" label={t('numberOfDays')}/>
<Dropdown id="direction" label={t('beforeAfter')} options={[ <Dropdown id="direction" label={t('beforeAfter')} options={[
@ -324,7 +324,7 @@ export function getRuleHelpers(t, fields) {
}; };
const optionValueSettings = { const optionValueSettings = {
form: null, getForm: () => null,
getFormData: rule => ({}), getFormData: rule => ({}),
assignRuleSettings: (rule, getter) => {}, assignRuleSettings: (rule, getter) => {},
validate: state => {} validate: state => {}

View file

@ -532,41 +532,45 @@ async function _sortIn(tx, listId, entityId, orderListBefore, orderSubscribeBefo
} }
} }
async function createTx(tx, context, listId, entity) {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
await _validateAndPreprocess(tx, listId, entity, true);
const fieldType = fieldTypes[entity.type];
let columnName;
if (!fieldType.grouped) {
columnName = ('custom_' + slugify(entity.name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
}
const filteredEntity = filterObject(entity, allowedKeysCreate);
filteredEntity.list = listId;
filteredEntity.column = columnName;
const ids = await tx('custom_fields').insert(filteredEntity);
const id = ids[0];
await _sortIn(tx, listId, id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
if (columnName) {
await knex.schema.table('subscription__' + listId, table => {
fieldType.addColumn(table, columnName);
if (fieldType.indexed) {
table.index(columnName);
}
});
// Altough this is a reference to an import, it is represented as signed int(11). This is because we use negative values for constant from SubscriptionSource
await knex.schema.raw('ALTER TABLE `subscription__' + listId + '` ADD `source_' + columnName +'` int(11) DEFAULT NULL');
}
return id;
}
async function create(context, listId, entity) { async function create(context, listId, entity) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields'); return await createTx(tx, context, listId, entity);
await _validateAndPreprocess(tx, listId, entity, true);
const fieldType = fieldTypes[entity.type];
let columnName;
if (!fieldType.grouped) {
columnName = ('custom_' + slugify(entity.name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
}
const filteredEntity = filterObject(entity, allowedKeysCreate);
filteredEntity.list = listId;
filteredEntity.column = columnName;
const ids = await tx('custom_fields').insert(filteredEntity);
const id = ids[0];
await _sortIn(tx, listId, id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
if (columnName) {
await knex.schema.table('subscription__' + listId, table => {
fieldType.addColumn(table, columnName);
if (fieldType.indexed) {
table.index(columnName);
}
});
// Altough this is a reference to an import, it is represented as signed int(11). This is because we use negative values for constant from SubscriptionSource
await knex.schema.raw('ALTER TABLE `subscription__' + listId + '` ADD `source_' + columnName +'` int(11) DEFAULT NULL');
}
return id;
}); });
} }
@ -833,6 +837,7 @@ module.exports.listByOrderListTx = listByOrderListTx;
module.exports.listDTAjax = listDTAjax; module.exports.listDTAjax = listDTAjax;
module.exports.listGroupedDTAjax = listGroupedDTAjax; module.exports.listGroupedDTAjax = listGroupedDTAjax;
module.exports.create = create; module.exports.create = create;
module.exports.createTx = createTx;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck; module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove; module.exports.remove = remove;
module.exports.removeAllByListIdTx = removeAllByListIdTx; module.exports.removeAllByListIdTx = removeAllByListIdTx;

View file

@ -14,7 +14,7 @@ const imports = require('./imports');
const entitySettings = require('../lib/entity-settings'); const entitySettings = require('../lib/entity-settings');
const dependencyHelpers = require('../lib/dependency-helpers'); const dependencyHelpers = require('../lib/dependency-helpers');
const UnsubscriptionMode = require('../../shared/lists').UnsubscriptionMode; const {UnsubscriptionMode, FieldWizard} = require('../../shared/lists');
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace', 'to_name', 'listunsubscribe_disabled', 'send_configuration']); const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace', 'to_name', 'listunsubscribe_disabled', 'send_configuration']);
@ -112,6 +112,47 @@ async function create(context, entity) {
await _validateAndPreprocess(tx, entity); await _validateAndPreprocess(tx, entity);
const fieldsToAdd = [];
const fieldWizard = entity.fieldWizard;
if (fieldWizard === FieldWizard.FIRST_LAST_NAME) {
if (entity.to_name === null) {
entity.to_name = '[MERGE_FIRST_NAME] [MERGE_LAST_NAME]';
}
fieldsToAdd.push({
name: 'First name',
key: 'MERGE_FIRST_NAME',
default_value: '',
type: 'text',
group: null,
settings: {}
});
fieldsToAdd.push({
name: 'Last name',
key: 'MERGE_LAST_NAME',
default_value: '',
type: 'text',
group: null,
settings: {}
});
} else if (fieldWizard === FieldWizard.NAME) {
if (entity.to_name === null) {
entity.to_name = '[MERGE_NAME]';
}
fieldsToAdd.push({
name: 'Name',
key: 'MERGE_NAME',
default_value: '',
type: 'text',
group: null,
settings: {}
});
}
const filteredEntity = filterObject(entity, allowedKeys); const filteredEntity = filterObject(entity, allowedKeys);
filteredEntity.cid = shortid.generate(); filteredEntity.cid = shortid.generate();
@ -148,6 +189,10 @@ async function create(context, entity) {
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: id }); await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: id });
for (const fld of fieldsToAdd) {
await fields.createTx(tx, context, id, fld);
}
return id; return id;
}); });
} }

View file

@ -44,9 +44,6 @@ router.post('/login', passport.csrfProtection, passport.restLogin);
router.post('/logout', passport.csrfProtection, passport.restLogout); router.post('/logout', passport.csrfProtection, passport.restLogout);
router.postAsync('/password-reset-send', passport.csrfProtection, async (req, res) => { router.postAsync('/password-reset-send', passport.csrfProtection, async (req, res) => {
// FIXME
console.log(req.locale);
console.log(req.cookies);
await users.sendPasswordReset(req.locale, req.body.usernameOrEmail); await users.sendPasswordReset(req.locale, req.body.usernameOrEmail);
return res.json(); return res.json();
}); });

View file

@ -32,6 +32,12 @@ const SubscriptionSource = {
ERASED: -6 ERASED: -6
}; };
const FieldWizard = {
NONE: 'none',
NAME: 'full_name',
FIRST_LAST_NAME: 'first_last_name'
}
function getFieldColumn(field) { function getFieldColumn(field) {
return field.column || 'grouped_' + field.id; return field.column || 'grouped_' + field.id;
} }
@ -40,5 +46,6 @@ module.exports = {
UnsubscriptionMode, UnsubscriptionMode,
SubscriptionStatus, SubscriptionStatus,
SubscriptionSource, SubscriptionSource,
FieldWizard,
getFieldColumn getFieldColumn
}; };