From 82251d1cb97260f4d9209298f67cdf1304145367 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Wed, 17 Jun 2020 17:24:38 +0200 Subject: [PATCH] Some improvements imported from IVIS (https://github.com/smartarch/ivis-core/tree/devel) Builtin Zone-MTA upgraded Bug fix - URLs in campaign would not work if they contained non-ASCII character --- client/src/lib/decorator-helpers.js | 20 ++- client/src/lib/files.js | 23 ++-- client/src/lib/form.js | 206 ++++++++++++++++++++++++---- client/src/lib/page.js | 7 +- server/models/links.js | 2 +- zone-mta/package.json | 2 +- 6 files changed, 217 insertions(+), 43 deletions(-) diff --git a/client/src/lib/decorator-helpers.js b/client/src/lib/decorator-helpers.js index f5b0f3ab..f17d1569 100644 --- a/client/src/lib/decorator-helpers.js +++ b/client/src/lib/decorator-helpers.js @@ -11,7 +11,7 @@ export function createComponentMixin(opts) { }; } -export function withComponentMixins(mixins, delegateFuns) { +export function withComponentMixins(mixins, delegateFuns, delegateStaticFuns) { const mixinsClosure = new Set(); for (const mixin of mixins) { console.assert(mixin); @@ -53,6 +53,7 @@ export function withComponentMixins(mixins, delegateFuns) { return self; } + TargetClassWithCtors.displayName = TargetClass.name; TargetClassWithCtors.prototype = TargetClass.prototype; @@ -61,6 +62,20 @@ export function withComponentMixins(mixins, delegateFuns) { TargetClassWithCtors[attr] = TargetClass[attr]; } + function addStaticMethodsToClass(clazz) { + if (delegateStaticFuns) { + for (const staticFuncName of delegateStaticFuns) { + if (!clazz[staticFuncName]) { + Object.defineProperty( + clazz, + staticFuncName, + Object.getOwnPropertyDescriptor(TargetClass, staticFuncName) + ); + } + } + } + } + function incorporateMixins(DecoratedInner) { for (const mixin of mixinsClosure.values()) { if (mixin.decoratorFn) { @@ -102,6 +117,7 @@ export function withComponentMixins(mixins, delegateFuns) { this._decoratorInnerInstanceRefFn = node => this._decoratorInnerInstance = node } + render() { let innerFn = parentProps => { const props = { @@ -136,6 +152,7 @@ export function withComponentMixins(mixins, delegateFuns) { } } + addStaticMethodsToClass(ComponentMixinsOuter); return ComponentMixinsOuter; } else { @@ -163,6 +180,7 @@ export function withComponentMixins(mixins, delegateFuns) { return innerFn(props); } + addStaticMethodsToClass(ComponentContextProvider); return ComponentContextProvider; } diff --git a/client/src/lib/files.js b/client/src/lib/files.js index 9792e55c..3b64b760 100644 --- a/client/src/lib/files.js +++ b/client/src/lib/files.js @@ -45,7 +45,7 @@ export default class Files extends Component { usePublicDownloadUrls: true } - getFilesUploadedMessage(response){ + getFilesUploadedMessage(response) { const t = this.props.t; const details = []; if (response.data.added) { @@ -61,7 +61,7 @@ export default class Files extends Component { return t('countFileUploaded', {count: response.data.uploaded}) + detailsMessage; } - onDrop(files){ + onDrop(files) { const t = this.props.t; if (files.length > 0) { this.setFlashMessage('info', t('uploadingCountFile', {count: files.length})); @@ -70,23 +70,22 @@ export default class Files extends Component { data.append('files[]', file) } axios.post(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}`), data) - .then(res => { - this.filesTable.refresh(); - const message = this.getFilesUploadedMessage(res); - this.setFlashMessage('info', message); - }) - .catch(res => this.setFlashMessage('danger', t('fileUploadFailed') + ' ' + res.message)); - } - else{ + .then(res => { + this.filesTable.refresh(); + const message = this.getFilesUploadedMessage(res); + this.setFlashMessage('info', message); + }) + .catch(res => this.setFlashMessage('danger', t('fileUploadFailed') + ' ' + res.message)); + } else { this.setFlashMessage('info', t('noFilesToUpload')); } } - deleteFile(fileId, fileName){ + deleteFile(fileId, fileName) { this.setState({fileToDeleteId: fileId, fileToDeleteName: fileName}) } - async hideDeleteFile(){ + async hideDeleteFile() { this.setState({fileToDeleteId: null, fileToDeleteName: null}) } diff --git a/client/src/lib/form.js b/client/src/lib/form.js index 9e5f1bda..de68464d 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -248,7 +248,7 @@ function wrapInput(id, htmlId, owner, format, rightContainerClass, label, help, if (format === 'inline') { return ( -
+
{labelBlock}{input} {helpBlock} {validationBlock} @@ -256,7 +256,7 @@ function wrapInput(id, htmlId, owner, format, rightContainerClass, label, help, ); } else { return ( -
+
{labelBlock}
{input} @@ -304,6 +304,7 @@ class StaticField extends Component { } @withComponentMixins([ + withTranslation, withFormStateOwner ]) class InputField extends Component { @@ -313,18 +314,38 @@ class InputField extends Component { placeholder: PropTypes.string, type: PropTypes.string, help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - format: PropTypes.string + format: PropTypes.string, + // TODO FOR MAILTRAIN added dropdown with hints under input + withHints: PropTypes.array, + disabled: PropTypes.bool } static defaultProps = { type: 'text' } + constructor() { + super(); + this.state = {showHints: false}; + this.textInput = React.createRef(); + } + + onFocus() { + this.setState({showHints: true}); + } + + onBlur() { + this.setState({showHints: false}); + } + render() { const props = this.props; + const t = props.t; const owner = this.getFormStateOwner(); - const id = this.props.id; + const id = props.id; const htmlId = 'form_' + id; + const enableHints = !!(props.withHints && !props.disabled); + let type = 'text'; if (props.type === 'password') { @@ -342,9 +363,78 @@ class InputField extends Component { const value = owner.getFormValue(id); if (value === null || value === undefined) console.log(`Warning: InputField ${id} is ${value}`); - return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, - owner.updateFormValue(id, evt.target.value)}/> + let hintsFuns = {}; + if (enableHints) { + hintsFuns['onFocus'] = ::this.onFocus; + hintsFuns['onBlur'] = ::this.onBlur; + } + + let inputContent = ( + owner.updateFormValue(id, evt.target.value)} + disabled={props.disabled} + {...hintsFuns} + /> ); + + if (enableHints) { + inputContent = ( +
+ {inputContent} +
evt.preventDefault()}> +
+
+ ); + + let hintsDropdown = null; + if (this.state.showHints) { + const hints = []; + for (const hint of props.withHints) { + hints.push( +
  • { + this.textInput.current.blur(); + owner.updateFormValue(id, hint); + }} + onMouseDown={evt => evt.preventDefault()} + > + {hint} +
  • + ) + } + + hintsDropdown = ( +
    + {hints} +
    + ) + } + + inputContent = ( +
    + {inputContent} + {hintsDropdown} +
    + ); + } + + return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, inputContent); } } @@ -371,7 +461,11 @@ class CheckBox extends Component { return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
    - owner.updateFormValue(id, !owner.getFormValue(id))}/> + owner.updateFormValue(id, !owner.getFormValue(id))}/>
    ); @@ -427,7 +521,11 @@ class CheckBoxGroup extends Component { let number = options.push(
    - this.onChange(option.key)}/> + this.onChange(option.key)}/>
    ); @@ -475,7 +573,12 @@ class RadioGroup extends Component { let number = options.push(
    - owner.updateFormValue(id, option.key)}/> + owner.updateFormValue(id, option.key)}/>
    ); @@ -521,10 +624,15 @@ class TextArea extends Component { const owner = this.getFormStateOwner(); const id = props.id; const htmlId = 'form_' + id; - const className = owner.addFormValidationClass('form-control ' + (props.className || '') , id); + const className = owner.addFormValidationClass('form-control ' + (props.className || ''), id); return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, - + ); } } @@ -577,12 +685,13 @@ class ColorPicker extends Component {
    -
    +
    {this.state.opened &&
    - +
    }
    @@ -611,7 +720,8 @@ class DatePicker extends Component { birthday: PropTypes.bool, dateFormat: PropTypes.string, formatDate: PropTypes.func, - parseDate: PropTypes.func + parseDate: PropTypes.func, + disabled: PropTypes.bool } static defaultProps = { @@ -647,7 +757,7 @@ class DatePicker extends Component { const htmlId = 'form_' + id; const t = props.t; - function BirthdayPickerCaption({ date, localeUtils, onChange }) { + function BirthdayPickerCaption({date, localeUtils, onChange}) { const months = localeUtils.getMonths(); return (
    @@ -695,11 +805,17 @@ class DatePicker extends Component { return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, <> -
    - owner.updateFormValue(id, evt.target.value)}/> +
    + owner.updateFormValue(id, evt.target.value)} + disabled={props.disabled}/> + {!props.disabled &&
    -
    + }
    {this.state.opened &&
    @@ -755,10 +871,15 @@ class Dropdown extends Component { } } - const className = owner.addFormValidationClass('form-control ' + (props.className || '') , id); + const className = owner.addFormValidationClass('form-control ' + (props.className || ''), id); return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, - owner.updateFormValue(id, evt.target.value)} + disabled={props.disabled}> {options} ); @@ -832,10 +953,15 @@ class TreeTableSelect extends Component { const id = this.props.id; const htmlId = 'form_' + id; - const className = owner.addFormValidationClass('' , id); + const className = owner.addFormValidationClass('', id); return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, - + ); } } @@ -927,20 +1053,36 @@ class TableSelect extends Component { const t = props.t; if (props.dropdown) { - const className = owner.addFormValidationClass('form-control' , id); + const className = owner.addFormValidationClass('form-control', id); return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
    - + {!props.disabled &&
    }
    -
    - this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selectionKeyIndex={props.selectionKeyIndex} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/> +
    +
    this.table = node} + data={props.data} + dataUrl={props.dataUrl} + columns={props.columns} + selectMode={props.selectMode} + selectionAsArray={this.props.selectionAsArray} + withHeader={props.withHeader} + selectionKeyIndex={props.selectionKeyIndex} + selection={owner.getFormValue(id)} + onSelectionDataAsync={::this.onSelectionDataAsync} + onSelectionChangedAsync={::this.onSelectionChangedAsync}/> ); @@ -948,7 +1090,17 @@ class TableSelect extends Component { return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
    -
    this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} pageLength={props.pageLength} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selectionKeyIndex={props.selectionKeyIndex} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/> +
    this.table = node} + data={props.data} + dataUrl={props.dataUrl} + columns={props.columns} + pageLength={props.pageLength} + selectMode={props.selectMode} + selectionAsArray={this.props.selectionAsArray} + withHeader={props.withHeader} + selectionKeyIndex={props.selectionKeyIndex} + selection={owner.getFormValue(id)} + onSelectionChangedAsync={::this.onSelectionChangedAsync}/> ); diff --git a/client/src/lib/page.js b/client/src/lib/page.js index b3538523..4f030d0c 100644 --- a/client/src/lib/page.js +++ b/client/src/lib/page.js @@ -376,6 +376,11 @@ export class SectionContent extends Component { }; this.historyUnlisten = props.history.listen((location, action) => { + // I don't think it is ever needed on replace action, or at least it will be better than not showing the msg, + // and without it this won't work because first it goes to '/' -> '/workspaces' so replacing immediately + if (action === "REPLACE") return; + if (location.state && location.state.preserveFlashMessage) return; + // noinspection JSIgnoredPromiseFromCall this.closeFlashMessage(); }); @@ -434,7 +439,7 @@ export class SectionContent extends Component { } navigateToWithFlashMessage(path, severity, text) { - this.props.history.push(path); + this.props.history.push(path, {preserveFlashMessage: true}); this.setFlashMessage(severity, text); } diff --git a/server/models/links.js b/server/models/links.js index 30922b00..b0e217a8 100644 --- a/server/models/links.js +++ b/server/models/links.js @@ -173,7 +173,7 @@ async function updateLinks(source, tagLanguage, mergeTags, campaign, list, subsc const urls = new Map(); // url -> {id, cid} (as returned by add) for (const url of urlsToBeReplaced) { // url might include variables, need to rewrite those just as we do with message content - const expanedUrl = tools.formatCampaignTemplate(url, tagLanguage, mergeTags, false, campaign, list, subscription); + const expanedUrl = encodeURI(tools.formatCampaignTemplate(url, tagLanguage, mergeTags, false, campaign, list, subscription)); const link = await addOrGet(campaign.id, expanedUrl); urls.set(url, link); } diff --git a/zone-mta/package.json b/zone-mta/package.json index e77ef395..f4a35b02 100644 --- a/zone-mta/package.json +++ b/zone-mta/package.json @@ -14,7 +14,7 @@ "node": ">=10.0.0" }, "dependencies": { - "zone-mta": "^1.16.6", + "zone-mta": "^2.2.1", "zonemta-delivery-counters": "^1.0.1", "zonemta-limiter": "^1.0.0", "zonemta-loop-breaker": "^1.0.2"