From 0bfb30817bfc8142a0dbf73268bcb3643b5b4924 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Mon, 14 Aug 2017 22:53:29 +0200 Subject: [PATCH] work in progress on segments some cleanup of models - handling dependencies in delete --- app.js | 4 + client/src/lib/tree.js | 8 +- client/src/lists/CUD.js | 2 +- client/src/lists/fields/CUD.js | 43 +- client/src/lists/forms/CUD.js | 2 +- client/src/lists/root.js | 39 +- client/src/lists/segments/CUD.js | 433 +++------------- client/src/lists/segments/List.js | 1 - client/src/lists/subscriptions/CUD.js | 486 ------------------ client/src/namespaces/CUD.js | 2 +- client/src/reports/CUD.js | 2 +- client/src/reports/templates/CUD.js | 2 +- client/src/users/CUD.js | 2 +- models/fields.js | 56 +- models/forms.js | 11 +- models/lists.js | 22 +- models/namespaces.js | 4 +- models/report-templates.js | 13 +- models/reports.js | 29 +- models/segments.js | 88 +++- models/shares.js | 23 +- models/users.js | 4 +- routes/rest/lists.js | 20 +- routes/rest/segments.js | 36 ++ routes/rest/subscriptions.js | 14 + ...> 20170731072050_upgrade_custom_fields.js} | 5 + ...04_create_foreign_keys_in_custom_fields.js | 8 - .../20170814174051_upgrade_segments.js | 175 +++++++ ...0643_remove_cascading_delete_in_reports.js | 9 + 29 files changed, 553 insertions(+), 990 deletions(-) delete mode 100644 client/src/lists/subscriptions/CUD.js create mode 100644 routes/rest/segments.js create mode 100644 routes/rest/subscriptions.js rename setup/knex/migrations/{20170731072050_change_field_group_template_to_settings_and_simplify_types.js => 20170731072050_upgrade_custom_fields.js} (88%) delete mode 100644 setup/knex/migrations/20170813143004_create_foreign_keys_in_custom_fields.js create mode 100644 setup/knex/migrations/20170814174051_upgrade_segments.js create mode 100644 setup/knex/migrations/20170814180643_remove_cascading_delete_in_reports.js diff --git a/app.js b/app.js index 244854a7..e1191b34 100644 --- a/app.js +++ b/app.js @@ -54,6 +54,8 @@ const listsRest = require('./routes/rest/lists'); const formsRest = require('./routes/rest/forms'); const fieldsRest = require('./routes/rest/fields'); const sharesRest = require('./routes/rest/shares'); +const segmentsRest = require('./routes/rest/segments'); +const subscriptionsRest = require('./routes/rest/subscriptions'); const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration'); const usersLegacyIntegration = require('./routes/users-legacy-integration'); @@ -277,6 +279,8 @@ app.use('/rest', listsRest); app.use('/rest', formsRest); app.use('/rest', fieldsRest); app.use('/rest', sharesRest); +app.use('/rest', segmentsRest); +app.use('/rest', subscriptionsRest); if (config.reports && config.reports.enabled === true) { app.use('/rest', reportTemplatesRest); diff --git a/client/src/lib/tree.js b/client/src/lib/tree.js index a0fd819c..a26a18c4 100644 --- a/client/src/lib/tree.js +++ b/client/src/lib/tree.js @@ -91,9 +91,11 @@ class TreeTable extends Component { const data = []; for (const unsafeEntry of unsafeData) { const entry = Object.assign({}, unsafeEntry); - entry.title = ReactDOMServer.renderToStaticMarkup(
{entry.title}
) - entry.description = ReactDOMServer.renderToStaticMarkup(
{entry.description}
) - entry.children = this.sanitizeTreeData(entry.children); + entry.title = ReactDOMServer.renderToStaticMarkup(
{entry.title}
); + entry.description = ReactDOMServer.renderToStaticMarkup(
{entry.description}
); + if (entry.children) { + entry.children = this.sanitizeTreeData(entry.children); + } data.push(entry); } return data; diff --git a/client/src/lists/CUD.js b/client/src/lists/CUD.js index 1abd00ae..f2f32a26 100644 --- a/client/src/lists/CUD.js +++ b/client/src/lists/CUD.js @@ -81,7 +81,7 @@ export default class CUD extends Component { } this.disableForm(); - this.setFormStatusMessage('info', t('Saving list ...')); + this.setFormStatusMessage('info', t('Saving ...')); const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { if (data.form === 'default') { diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js index 834b80b1..fe4599d7 100644 --- a/client/src/lists/fields/CUD.js +++ b/client/src/lists/fields/CUD.js @@ -11,7 +11,6 @@ import { import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; import {DeleteModalDialog} from "../../lib/delete"; import { getFieldTypes } from './field-types'; -import axios from '../../lib/axios'; import interoperableErrors from '../../../../shared/interoperable-errors'; import validators from '../../../../shared/validators'; import slugify from 'slugify'; @@ -45,6 +44,7 @@ export default class CUD extends Component { static propTypes = { action: PropTypes.string.isRequired, list: PropTypes.object, + fields: PropTypes.array, entity: PropTypes.object } @@ -58,27 +58,6 @@ export default class CUD extends Component { } } - @withAsyncErrorHandler - async loadOrderOptions() { - const t = this.props.t; - - const flds = await axios.get(`/rest/fields/${this.props.list.id}`); - - const getOrderOptions = fld => { - return [ - {key: 'none', label: t('Not visible')}, - ...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})), - {key: 'end', label: t('End of list')} - ]; - }; - - this.setState({ - orderListOptions: getOrderOptions('order_list'), - orderSubscribeOptions: getOrderOptions('order_subscribe'), - orderManageOptions: getOrderOptions('order_manage') - }); - } - componentDidMount() { if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity, data => { @@ -139,8 +118,6 @@ export default class CUD extends Component { orderManageOptions: [] }); } - - this.loadOrderOptions(); } localValidateFormValues(state) { @@ -250,7 +227,7 @@ export default class CUD extends Component { try { this.disableForm(); - this.setFormStatusMessage('info', t('Saving field ...')); + this.setFormStatusMessage('info', t('Saving ...')); const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { if (data.default_value.trim() === '') { @@ -320,6 +297,16 @@ export default class CUD extends Component { const t = this.props.t; const isEdit = !!this.props.entity; + + const getOrderOptions = fld => { + return [ + {key: 'none', label: t('Not visible')}, + ...this.props.fields.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})), + {key: 'end', label: t('End of list')} + ]; + }; + + const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label})); const type = this.getFormValue('type'); @@ -469,9 +456,9 @@ export default class CUD extends Component { {type !== 'option' &&
- - - + + +
} diff --git a/client/src/lists/forms/CUD.js b/client/src/lists/forms/CUD.js index c4999d63..f094364c 100644 --- a/client/src/lists/forms/CUD.js +++ b/client/src/lists/forms/CUD.js @@ -323,7 +323,7 @@ export default class CUD extends Component { } this.disableForm(); - this.setFormStatusMessage('info', t('Saving forms ...')); + this.setFormStatusMessage('info', t('Saving ...')); const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { delete data.selectedTemplate; diff --git a/client/src/lists/root.js b/client/src/lists/root.js index af9d526e..dc1aff77 100644 --- a/client/src/lists/root.js +++ b/client/src/lists/root.js @@ -14,6 +14,7 @@ import FieldsList from './fields/List'; import FieldsCUD from './fields/CUD'; import SubscriptionsList from './subscriptions/List'; import SegmentsList from './segments/List'; +import SegmentsCUD from './segments/CUD'; import Share from '../shares/Share'; @@ -58,20 +59,24 @@ const getStructure = t => { ':fieldId([0-9]+)': { title: resolved => t('Field "{{name}}"', {name: resolved.field.name}), resolve: { - field: params => `/rest/fields/${params.listId}/${params.fieldId}` + field: params => `/rest/fields/${params.listId}/${params.fieldId}`, + fields: params => `/rest/fields/${params.listId}` }, link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`, navs: { ':action(edit|delete)': { title: t('Edit'), link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`, - render: props => + render: props => } } }, create: { - title: t('Create Field'), - render: props => + title: t('Create'), + resolve: { + fields: params => `/rest/fields/${params.listId}` + }, + render: props => } } }, @@ -79,7 +84,31 @@ const getStructure = t => { title: t('Segments'), link: params => `/lists/${params.listId}/segments`, visible: resolved => resolved.list.permissions.includes('manageSegments'), - render: props => + render: props => , + children: { + ':segmentId([0-9]+)': { + title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}), + resolve: { + segment: params => `/rest/segments/${params.listId}/${params.segmentId}`, + fields: params => `/rest/fields/${params.listId}` + }, + link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`, + navs: { + ':action(edit|delete)': { + title: t('Edit'), + link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`, + render: props => + } + } + }, + create: { + title: t('Create'), + resolve: { + fields: params => `/rest/fields/${params.listId}` + }, + render: props => + } + } }, share: { title: t('Share'), diff --git a/client/src/lists/segments/CUD.js b/client/src/lists/segments/CUD.js index 834b80b1..0d0cc035 100644 --- a/client/src/lists/segments/CUD.js +++ b/client/src/lists/segments/CUD.js @@ -5,17 +5,12 @@ import PropTypes from 'prop-types'; import { translate, Trans } from 'react-i18next'; import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page'; import { - withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button, - Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField + withForm, Form, FormSendMethod, InputField, ButtonRow, Button } from '../../lib/form'; import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; import {DeleteModalDialog} from "../../lib/delete"; -import { getFieldTypes } from './field-types'; -import axios from '../../lib/axios'; import interoperableErrors from '../../../../shared/interoperable-errors'; -import validators from '../../../../shared/validators'; -import slugify from 'slugify'; -import { parseDate, parseBirthday } from '../../../../shared/fields'; +import {TreeTable} from "../../lib/tree"; @translate() @withForm @@ -28,119 +23,28 @@ export default class CUD extends Component { this.state = {}; - this.fieldTypes = getFieldTypes(props.t); - - this.initForm({ - serverValidation: { - url: `/rest/fields-validate/${this.props.list.id}`, - changed: ['key'], - extra: ['id'] - }, - onChange: { - name: ::this.onChangeName - } - }); + this.initForm(); } static propTypes = { action: PropTypes.string.isRequired, list: PropTypes.object, + fields: PropTypes.array, entity: PropTypes.object } - onChangeName(state, attr, oldValue, newValue) { - const oldComputedKey = ('MERGE_' + slugify(oldValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, ''); - const oldKey = state.formState.getIn(['data', 'key', 'value']); - - if (oldKey === '' || oldKey === oldComputedKey) { - const newKey = ('MERGE_' + slugify(newValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, ''); - state.formState = state.formState.setIn(['data', 'key', 'value'], newKey); - } - } - - @withAsyncErrorHandler - async loadOrderOptions() { - const t = this.props.t; - - const flds = await axios.get(`/rest/fields/${this.props.list.id}`); - - const getOrderOptions = fld => { - return [ - {key: 'none', label: t('Not visible')}, - ...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})), - {key: 'end', label: t('End of list')} - ]; - }; - - this.setState({ - orderListOptions: getOrderOptions('order_list'), - orderSubscribeOptions: getOrderOptions('order_subscribe'), - orderManageOptions: getOrderOptions('order_manage') - }); - } - componentDidMount() { if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity, data => { - data.settings = data.settings || {}; - - if (data.default_value === null) { - data.default_value = ''; - } - - if (data.type !== 'option') { - data.group = null; - } - - data.enumOptions = ''; - data.dateFormat = 'eur'; - data.renderTemplate = ''; - - switch (data.type) { - case 'checkbox': - case 'radio-grouped': - case 'dropdown-grouped': - case 'json': - data.renderTemplate = data.settings.renderTemplate; - break; - - case 'radio-enum': - case 'dropdown-enum': - data.enumOptions = this.renderEnumOptions(data.settings.enumOptions); - data.renderTemplate = data.settings.renderTemplate; - break; - - case 'date': - case 'birthday': - data.dateFormat = data.settings.dateFormat; - break; - } - - data.orderListBefore = data.orderListBefore.toString(); - data.orderSubscribeBefore = data.orderSubscribeBefore.toString(); - data.orderManageBefore = data.orderManageBefore.toString(); + // FIXME populate all others from settings }); } else { this.populateFormValues({ name: '', - type: 'text', - key: '', - default_value: '', - group: null, - renderTemplate: '', - enumOptions: '', - dateFormat: 'eur', - orderListBefore: 'end', // possible values are / 'end' / 'none' - orderSubscribeBefore: 'end', - orderManageBefore: 'end', - orderListOptions: [], - orderSubscribeOptions: [], - orderManageOptions: [] + settingsJSON: '' }); } - - this.loadOrderOptions(); } localValidateFormValues(state) { @@ -152,151 +56,31 @@ export default class CUD extends Component { state.setIn(['name', 'error'], null); } - const keyServerValidation = state.getIn(['key', 'serverValidation']); - if (!validators.mergeTagValid(state.getIn(['key', 'value']))) { - state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.')); - } else if (!keyServerValidation) { - state.setIn(['key', 'error'], t('Validation is in progress...')); - } else if (keyServerValidation.exists) { - state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.')); - } else { - state.setIn(['key', 'error'], null); - } - - const type = state.getIn(['type', 'value']); - - const group = state.getIn(['group', 'value']); - if (type === 'option' && !group) { - state.setIn(['group', 'error'], t('Group has to be selected')); - } else { - state.setIn(['group', 'error'], null); - } - - const defaultValue = state.getIn(['default_value', 'value']); - if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) { - state.setIn(['default_value', 'error'], t('Default value is not integer number')); - } else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) { - state.setIn(['default_value', 'error'], t('Default value is not a properly formatted date')); - } else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) { - state.setIn(['default_value', 'error'], t('Default value is not a properly formatted birthday date')); - } else { - state.setIn(['default_value', 'error'], null); - } - - if (type === 'radio-enum' || type === 'dropdown-enum') { - const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value'])); - if (enumOptions.errors) { - state.setIn(['enumOptions', 'error'],
{enumOptions.errors.map((err, idx) =>
{err}
)}
); - } else { - state.setIn(['enumOptions', 'error'], null); - - if (defaultValue !== '' && !(defaultValue in enumOptions.options)) { - state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options')); - } - } - } else { - state.setIn(['enumOptions', 'error'], null); - } } - parseEnumOptions(text) { - const t = this.props.t; - const errors = []; - const options = {}; - - const lines = text.split('\n'); - for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { - const line = lines[lineIdx].trim(); - - if (line != '') { - const matches = line.match(/^([^|]*)[|](.*)$/); - if (matches) { - const key = matches[1].trim(); - const label = matches[2].trim(); - options[key] = label; - } else { - errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1})); - } - } - } - - if (errors.length) { - return { - errors - }; - } else { - return { - options - }; - } - } - - renderEnumOptions(options) { - return Object.keys(options).map(key => `${key}|${options[key]}`).join('\n'); - } - - async submitHandler() { const t = this.props.t; let sendMethod, url; if (this.props.entity) { sendMethod = FormSendMethod.PUT; - url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}` + url = `/rest/segments/${this.props.list.id}/${this.props.entity.id}` } else { sendMethod = FormSendMethod.POST; - url = `/rest/fields/${this.props.list.id}` + url = `/rest/segments/${this.props.list.id}` } try { this.disableForm(); - this.setFormStatusMessage('info', t('Saving field ...')); + this.setFormStatusMessage('info', t('Saving ...')); const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { - if (data.default_value.trim() === '') { - data.default_value = null; - } - - if (data.type !== 'option') { - data.group = null; - } - - data.settings = {}; - switch (data.type) { - case 'checkbox': - case 'radio-grouped': - case 'dropdown-grouped': - case 'json': - data.settings.renderTemplate = data.renderTemplate; - break; - - case 'radio-enum': - case 'dropdown-enum': - data.settings.enumOptions = this.parseEnumOptions(data.enumOptions).options; - data.settings.renderTemplate = data.renderTemplate; - break; - - case 'date': - case 'birthday': - data.settings.dateFormat = data.dateFormat; - break; - } - - delete data.renderTemplate; - delete data.enumOptions; - delete data.dateFormat; - - if (data.type === 'option') { - data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none'; - } else { - data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore; - data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore; - data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore; - } + // FIXME - make sure settings is correct and delete all others }); if (submitSuccessful) { - this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved')); + this.enableForm(); + this.setFormStatusMessage('success', t('Segment saved')); } else { this.enableForm(); this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); @@ -320,123 +104,50 @@ export default class CUD extends Component { const t = this.props.t; const isEdit = !!this.props.entity; - const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label})); + const sampleTreeData = [ + { + key: 'a', + title: 'A', + expanded: true, + children: [ + { + key: 'aa', + title: 'AA', + expanded: true, + children: [ + { + key: 'aaa', + title: 'AAA', + expanded: true + }, + { + key: 'aab', + title: 'AAB', + expanded: true + } + ] + }, + { + key: 'ab', + title: 'AB', + expanded: true, + children: [ + { + key: 'aba', + title: 'ABA', + expanded: true + }, + { + key: 'abb', + title: 'ABB', + expanded: true + } + ] + }, + ] + } + ]; - const type = this.getFormValue('type'); - - let fieldSettings = null; - switch (type) { - case 'text': - case 'website': - case 'longtext': - case 'gpg': - case 'number': - fieldSettings = -
- -
; - break; - - case 'checkbox': - case 'radio-grouped': - case 'dropdown-grouped': - fieldSettings = -
- You can control the appearance of the merge tag with this template. The template - uses handlebars syntax and you can find all values from {'{{values}}'} array, for - example {'{{#each values}} {{this}} {{/each}}'}. If template is not defined then - multiple values are joined with commas.} - /> -
; - break; - - case 'radio-enum': - case 'dropdown-enum': - fieldSettings = -
-
Specify the options to select from in the following format:key|label. For example:
-
au|Australia
at|Austria
} - /> - Default key (e.g. au used when the field is empty.')}/> - You can control the appearance of the merge tag with this template. The template - uses handlebars syntax and you can find all values from {'{{values}}'} array. - Each entry in the array is an object with attributes key and label. - For example {'{{#each values}} {{this.value}} {{/each}}'}. If template is not defined then - multiple values are joined with commas.} - /> -
; - break; - - case 'date': - fieldSettings = -
- - Default value used when the field is empty.}/> -
; - break; - - case 'birthday': - fieldSettings = -
- - Default value used when the field is empty.}/> -
; - break; - - case 'json': - fieldSettings =
- Default key (e.g. au used when the field is empty.')}/> - You can use this template to render JSON values (if the JSON is an array then the array is - exposed as values, otherwise you can access the JSON keys directly).} - /> -
; - break; - - case 'option': - const fieldsGroupedColumns = [ - { data: 4, title: "#" }, - { data: 1, title: t('Name') }, - { data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false }, - { data: 3, title: t('Merge Tag') } - ]; - - fieldSettings = -
- - -
; - break; - } return ( @@ -445,40 +156,26 @@ export default class CUD extends Component { + deleteUrl={`/rest/segments/${this.props.list.id}/${this.props.entity.id}`} + cudUrl={`/lists/segments/${this.props.list.id}/${this.props.entity.id}/edit`} + listUrl={`/lists/segments/${this.props.list.id}`} + deletingMsg={t('Deleting segment ...')} + deletedMsg={t('Segment deleted')}/> } - {isEdit ? t('Edit Field') : t('Create Field')} + {isEdit ? t('Edit Segment') : t('Create Segment')}
- {isEdit ? - {(this.fieldTypes[this.getFormValue('type')] || {}).label} - : - - } - - - - {fieldSettings} - - {type !== 'option' && -
- - - -
- } -