diff --git a/TODO.md b/TODO.md index 6d063285..82f5c76f 100644 --- a/TODO.md +++ b/TODO.md @@ -21,3 +21,6 @@ Note that some of these may be already obsolete... - Add field to subscriptions which says till when the consent has been given - Provide a link (and merge tag) that will update the consent date to now - Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds) + +### RSS Campaigns +- Aggregated RSS campaigns \ No newline at end of file diff --git a/client/src/lib/decorator-helpers.js b/client/src/lib/decorator-helpers.js index 628cb1ee..f5b0f3ab 100644 --- a/client/src/lib/decorator-helpers.js +++ b/client/src/lib/decorator-helpers.js @@ -2,17 +2,19 @@ import React from "react"; -export function createComponentMixin(contexts, deps, decoratorFn) { +export function createComponentMixin(opts) { return { - contexts, - deps, - decoratorFn + contexts: opts.contexts || [], + deps: opts.deps || [], + delegateFuns: opts.delegateFuns || [], + decoratorFn: opts.decoratorFn }; } export function withComponentMixins(mixins, delegateFuns) { const mixinsClosure = new Set(); for (const mixin of mixins) { + console.assert(mixin); mixinsClosure.add(mixin); for (const dep of mixin.deps) { mixinsClosure.add(dep); @@ -34,6 +36,10 @@ export function withComponentMixins(mixins, delegateFuns) { mixinDelegateFuns.push(...delegateFuns); } + for (const mixin of mixinsClosure.values()) { + mixinDelegateFuns.push(...mixin.delegateFuns); + } + function TargetClassWithCtors(props) { if (!new.target) { throw new TypeError(); @@ -55,54 +61,90 @@ export function withComponentMixins(mixins, delegateFuns) { TargetClassWithCtors[attr] = TargetClass[attr]; } + function incorporateMixins(DecoratedInner) { + for (const mixin of mixinsClosure.values()) { + if (mixin.decoratorFn) { + const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors); - class ComponentMixinsInner extends React.Component { - render() { - const props = { - ...this.props, - ref: this.props._decoratorInnerInstanceRefFn - }; - delete props._decoratorInnerInstanceRefFn; + if (res.cls) { + DecoratedInner = res.cls; + } - return ( - - ); + if (res.ctor) { + ctors.push(res.ctor); + } + } } + + return DecoratedInner; } - let DecoratedInner = ComponentMixinsInner; - - for (const mixin of mixinsClosure.values()) { - const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors); - - if (res.cls) { - DecoratedInner = res.cls; - } - - if (res.ctor) { - ctors.push(res.ctor); - } - - if (res.delegateFuns) { - mixinDelegateFuns.push(...res.delegateFuns); - } - } - - class ComponentMixinsOuter extends React.Component { - constructor(props) { - super(props); - - this._decoratorInnerInstanceRefFn = node => this._decoratorInnerInstance = node - } - render() { - let innerFn = parentProps => { + if (mixinDelegateFuns.length > 0) { + class ComponentMixinsInner extends React.Component { + render() { const props = { - ...parentProps, - _decoratorInnerInstanceRefFn: this._decoratorInnerInstanceRefFn + ...this.props, + ref: this.props._decoratorInnerInstanceRefFn + }; + delete props._decoratorInnerInstanceRefFn; + + return ( + + ); + } + } + + const DecoratedInner = incorporateMixins(ComponentMixinsInner); + + class ComponentMixinsOuter extends React.Component { + constructor(props) { + super(props); + + this._decoratorInnerInstanceRefFn = node => this._decoratorInnerInstance = node + } + render() { + let innerFn = parentProps => { + const props = { + ...parentProps, + _decoratorInnerInstanceRefFn: this._decoratorInnerInstanceRefFn + }; + + return }; - return + for (const [propName, Context] of contexts.entries()) { + const existingInnerFn = innerFn; + innerFn = parentProps => ( + + { + value => existingInnerFn({ + ...parentProps, + [propName]: value + }) + } + + ); + } + + return innerFn(this.props); } + } + + for (const fun of mixinDelegateFuns) { + ComponentMixinsOuter.prototype[fun] = function (...args) { + return this._decoratorInnerInstance[fun](...args); + } + } + + return ComponentMixinsOuter; + + } else { + const DecoratedInner = incorporateMixins(TargetClassWithCtors); + + function ComponentContextProvider(props) { + let innerFn = props => { + return + }; for (const [propName, Context] of contexts.entries()) { const existingInnerFn = innerFn; @@ -118,17 +160,12 @@ export function withComponentMixins(mixins, delegateFuns) { ); } - return innerFn(this.props); + return innerFn(props); } + + return ComponentContextProvider; } - for (const fun of mixinDelegateFuns) { - ComponentMixinsOuter.prototype[fun] = function (...args) { - return this._decoratorInnerInstance[fun](...args); - } - } - - return ComponentMixinsOuter; }; } diff --git a/client/src/lib/error-handling.js b/client/src/lib/error-handling.js index 09553c1a..50700e8e 100644 --- a/client/src/lib/error-handling.js +++ b/client/src/lib/error-handling.js @@ -21,33 +21,36 @@ function handleError(that, error) { } export const ParentErrorHandlerContext = React.createContext(null); -export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => { - /* Example of use: - this.getFormValuesFromURL(....).catch(error => this.handleError(error)); +export const withErrorHandling = createComponentMixin({ + contexts: [{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], + decoratorFn: (TargetClass, InnerClass) => { + /* Example of use: + this.getFormValuesFromURL(....).catch(error => this.handleError(error)); - It's equivalent to: + It's equivalent to: - @withAsyncErrorHandler - async loadFormValues() { - await this.getFormValuesFromURL(...); - } - */ + @withAsyncErrorHandler + async loadFormValues() { + await this.getFormValuesFromURL(...); + } + */ - const originalRender = InnerClass.prototype.render; + const originalRender = InnerClass.prototype.render; - InnerClass.prototype.render = function() { - return ( - - {originalRender.apply(this)} - - ); + InnerClass.prototype.render = function () { + return ( + + {originalRender.apply(this)} + + ); + } + + InnerClass.prototype.handleError = function (error) { + handleError(this, error); + }; + + return {}; } - - InnerClass.prototype.handleError = function(error) { - handleError(this, error); - }; - - return {}; }); export function withAsyncErrorHandler(target, name, descriptor) { diff --git a/client/src/lib/form.js b/client/src/lib/form.js index 40eede72..892e5919 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -46,12 +46,15 @@ const FormSendMethod = HTTPMethod; export const FormStateOwnerContext = React.createContext(null); -const withFormStateOwner = createComponentMixin([{context: FormStateOwnerContext, propName: 'formStateOwner'}], [], (TargetClass, InnerClass) => { - InnerClass.prototype.getFormStateOwner = function() { - return this.props.formStateOwner; - }; +const withFormStateOwner = createComponentMixin({ + contexts: [{context: FormStateOwnerContext, propName: 'formStateOwner'}], + decoratorFn: (TargetClass, InnerClass) => { + InnerClass.prototype.getFormStateOwner = function () { + return this.props.formStateOwner; + }; - return {}; + return {}; + } }); export function withFormErrorHandlers(target, name, descriptor) { @@ -345,9 +348,6 @@ class InputField extends Component { } } -@withComponentMixins([ - withFormStateOwner -]) class CheckBox extends Component { static propTypes = { id: PropTypes.string.isRequired, @@ -359,19 +359,28 @@ class CheckBox extends Component { } render() { - const props = this.props; - const owner = this.getFormStateOwner(); - const id = this.props.id; - const htmlId = 'form_' + id; + return ( + + { + owner => { + const props = this.props; + const id = this.props.id; + const htmlId = 'form_' + id; - const inputClassName = owner.addFormValidationClass('form-check-input', id); + const inputClassName = owner.addFormValidationClass('form-check-input', id); - return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, -
- owner.updateFormValue(id, !owner.getFormValue(id))}/> - -
+ return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, +
+ owner.updateFormValue(id, !owner.getFormValue(id))}/> + +
+ ); + } + + } +
); + } } @@ -489,6 +498,15 @@ class RadioGroup extends Component { withFormStateOwner ]) class TextArea extends Component { + constructor() { + super(); + this.onChange = evt => { + const id = this.props.id; + const owner = this.getFormStateOwner(); + owner.updateFormValue(id, evt.target.value); + } + } + static propTypes = { id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, @@ -506,7 +524,7 @@ class TextArea extends Component { const className = owner.addFormValidationClass('form-control ' + (props.className || '') , id); return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, - + ); } } @@ -977,214 +995,443 @@ class ACEEditor extends Component { } -const withForm = createComponentMixin([], [], (TargetClass, InnerClass) => { - const proto = InnerClass.prototype; +const withForm = createComponentMixin({ + decoratorFn: (TargetClass, InnerClass) => { + const proto = InnerClass.prototype; - const cleanFormState = Immutable.Map({ - state: FormState.Loading, - isValidationShown: false, - isDisabled: false, - statusMessageText: '', - data: Immutable.Map(), - savedData: Immutable.Map(), - isServerValidationRunning: false - }); + const cleanFormState = Immutable.Map({ + state: FormState.Loading, + isValidationShown: false, + isDisabled: false, + statusMessageText: '', + data: Immutable.Map(), + savedData: Immutable.Map(), + isServerValidationRunning: false + }); - const getSaveData = (self, formStateData) => { - let data = formStateData.map(attr => attr.get('value')).toJS(); + const getSaveData = (self, formStateData) => { + let data = formStateData.map(attr => attr.get('value')).toJS(); - if (self.submitFormValuesMutator) { - const newData = self.submitFormValuesMutator(data, false); - if (newData !== undefined) { - data = newData; + if (self.submitFormValuesMutator) { + const newData = self.submitFormValuesMutator(data, false); + if (newData !== undefined) { + data = newData; + } + } + + return data; + }; + + // formValidateResolve is called by "validateForm" once client receives validation response from server that does not + // trigger another server validation + let formValidateResolve = null; + + function scheduleValidateForm(self) { + setTimeout(() => { + self.setState(previousState => ({ + formState: previousState.formState.withMutations(mutState => { + validateFormState(self, mutState); + }) + })); + }, 0); + } + + function validateFormState(self, mutState) { + const settings = self.state.formSettings; + + if (!mutState.get('isServerValidationRunning') && settings.serverValidation) { + const payload = {}; + let payloadNotEmpty = false; + + for (const attr of settings.serverValidation.extra || []) { + if (typeof attr === 'string') { + payload[attr] = mutState.getIn(['data', attr, 'value']); + } else { + const data = mutState.get('data').map(attr => attr.get('value')).toJS(); + payload[attr.key] = attr.data(data); + } + } + + for (const attr of settings.serverValidation.changed) { + const currValue = mutState.getIn(['data', attr, 'value']); + const serverValue = mutState.getIn(['data', attr, 'serverValue']); + + // This really assumes that all form values are preinitialized (i.e. not undef) + if (currValue !== serverValue) { + mutState.setIn(['data', attr, 'serverValidated'], false); + payload[attr] = currValue; + payloadNotEmpty = true; + } + } + + if (payloadNotEmpty) { + mutState.set('isServerValidationRunning', true); + + axios.post(getUrl(settings.serverValidation.url), payload) + .then(response => { + + if (self.isComponentMounted()) { + self.setState(previousState => ({ + formState: previousState.formState.withMutations(mutState => { + mutState.set('isServerValidationRunning', false); + + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + for (const attr in payload) { + mutStateData.setIn([attr, 'serverValue'], payload[attr]); + + if (payload[attr] === mutState.getIn(['data', attr, 'value'])) { + mutStateData.setIn([attr, 'serverValidated'], true); + mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true); + } + } + })); + }) + })); + + scheduleValidateForm(self); + } + }) + .catch(error => { + if (self.isComponentMounted()) { + console.log('Error in "validateFormState": ' + error); + + self.setState(previousState => ({ + formState: previousState.formState.set('isServerValidationRunning', false) + })); + + // TODO: It might be good not to give up immediatelly, but retry a couple of times + // scheduleValidateForm(self); + } + }); + } else { + if (formValidateResolve) { + const resolve = formValidateResolve; + formValidateResolve = null; + resolve(); + } + } + } + + if (self.localValidateFormValues) { + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + self.localValidateFormValues(mutStateData); + })); } } - return data; - }; + const previousComponentDidMount = proto.componentDidMount; + proto.componentDidMount = function () { + this._isComponentMounted = true; + if (previousComponentDidMount) { + previousComponentDidMount.apply(this); + } + }; - // formValidateResolve is called by "validateForm" once client receives validation response from server that does not - // trigger another server validation - let formValidateResolve = null; + const previousComponentWillUnmount = proto.componentWillUnmount; + proto.componentWillUnmount = function () { + this._isComponentMounted = false; + if (previousComponentWillUnmount) { + previousComponentDidMount.apply(this); + } + }; - function scheduleValidateForm(self) { - setTimeout(() => { - self.setState(previousState => ({ + proto.isComponentMounted = function () { + return !!this._isComponentMounted; + } + + proto.initForm = function (settings) { + const state = this.state || {}; + state.formState = cleanFormState; + state.formSettings = { + leaveConfirmation: true, + ...(settings || {}) + }; + this.state = state; + }; + + proto.resetFormState = function () { + this.setState({ + formState: cleanFormState + }); + }; + + proto.getFormValuesFromEntity = function (entity) { + const settings = this.state.formSettings; + const data = Object.assign({}, entity); + + data.originalHash = data.hash; + delete data.hash; + + if (this.getFormValuesMutator) { + this.getFormValuesMutator(data, this.getFormValues()); + } + + this.populateFormValues(data); + }; + + proto.getFormValuesFromURL = async function (url) { + const settings = this.state.formSettings; + setTimeout(() => { + this.setState(previousState => { + if (previousState.formState.get('state') === FormState.Loading) { + return { + formState: previousState.formState.set('state', FormState.LoadingWithNotice) + }; + } + }); + }, 500); + + const response = await axios.get(getUrl(url)); + + let data = response.data; + + data.originalHash = data.hash; + delete data.hash; + + if (this.getFormValuesMutator) { + const newData = this.getFormValuesMutator(data, this.getFormValues()); + + if (newData !== undefined) { + data = newData; + } + } + + this.populateFormValues(data); + }; + + proto.validateAndSendFormValuesToURL = async function (method, url) { + const settings = this.state.formSettings; + await this.waitForFormServerValidated(); + + if (this.isFormWithoutErrors()) { + if (settings.getPreSubmitUpdater) { + const preSubmitUpdater = await settings.getPreSubmitUpdater(); + + await new Promise((resolve, reject) => { + this.setState(previousState => ({ + formState: previousState.formState.withMutations(mutState => { + mutState.update('data', stateData => stateData.withMutations(preSubmitUpdater)); + }) + }), resolve); + }); + } + + let data = this.getFormValues(); + + if (this.submitFormValuesMutator) { + const newData = this.submitFormValuesMutator(data, true); + if (newData !== undefined) { + data = newData; + } + } + + const response = await axios.method(method, getUrl(url), data); + + if (settings.leaveConfirmation) { + await new Promise((resolve, reject) => { + this.setState(previousState => ({ + formState: previousState.formState.set('savedData', getSaveData(this, previousState.formState.get('data'))) + }), resolve); + }); + } + + return response.data || true; + + } else { + this.showFormValidation(); + return false; + } + }; + + + proto.populateFormValues = function (data) { + const settings = this.state.formSettings; + + this.setState(previousState => ({ formState: previousState.formState.withMutations(mutState => { - validateFormState(self, mutState); + mutState.set('state', FormState.Ready); + + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + for (const key in data) { + mutStateData.set(key, Immutable.Map({ + value: data[key] + })); + } + })); + + if (settings.leaveConfirmation) { + mutState.set('savedData', getSaveData(this, mutState.get('data'))); + } + + validateFormState(this, mutState); }) })); - }, 0); - } - - function validateFormState(self, mutState) { - const settings = self.state.formSettings; - - if (!mutState.get('isServerValidationRunning') && settings.serverValidation) { - const payload = {}; - let payloadNotEmpty = false; - - for (const attr of settings.serverValidation.extra || []) { - if (typeof attr === 'string') { - payload[attr] = mutState.getIn(['data', attr, 'value']); - } else { - const data = mutState.get('data').map(attr => attr.get('value')).toJS(); - payload[attr.key] = attr.data(data); - } - } - - for (const attr of settings.serverValidation.changed) { - const currValue = mutState.getIn(['data', attr, 'value']); - const serverValue = mutState.getIn(['data', attr, 'serverValue']); - - // This really assumes that all form values are preinitialized (i.e. not undef) - if (currValue !== serverValue) { - mutState.setIn(['data', attr, 'serverValidated'], false); - payload[attr] = currValue; - payloadNotEmpty = true; - } - } - - if (payloadNotEmpty) { - mutState.set('isServerValidationRunning', true); - - axios.post(getUrl(settings.serverValidation.url), payload) - .then(response => { - - if (self.isComponentMounted()) { - self.setState(previousState => ({ - formState: previousState.formState.withMutations(mutState => { - mutState.set('isServerValidationRunning', false); - - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - for (const attr in payload) { - mutStateData.setIn([attr, 'serverValue'], payload[attr]); - - if (payload[attr] === mutState.getIn(['data', attr, 'value'])) { - mutStateData.setIn([attr, 'serverValidated'], true); - mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true); - } - } - })); - }) - })); - - scheduleValidateForm(self); - } - }) - .catch(error => { - if (self.isComponentMounted()) { - console.log('Error in "validateFormState": ' + error); - - self.setState(previousState => ({ - formState: previousState.formState.set('isServerValidationRunning', false) - })); - - // TODO: It might be good not to give up immediatelly, but retry a couple of times - // scheduleValidateForm(self); - } - }); - } else { - if (formValidateResolve) { - const resolve = formValidateResolve; - formValidateResolve = null; - resolve(); - } - } - } - - if (self.localValidateFormValues) { - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - self.localValidateFormValues(mutStateData); - })); - } - } - - const previousComponentDidMount = proto.componentDidMount; - proto.componentDidMount = function() { - this._isComponentMounted = true; - if (previousComponentDidMount) { - previousComponentDidMount.apply(this); - } - }; - - const previousComponentWillUnmount = proto.componentWillUnmount; - proto.componentWillUnmount = function() { - this._isComponentMounted = false; - if (previousComponentWillUnmount) { - previousComponentDidMount.apply(this); - } - }; - - proto.isComponentMounted = function() { - return !!this._isComponentMounted; - } - - proto.initForm = function(settings) { - const state = this.state || {}; - state.formState = cleanFormState; - state.formSettings = { - leaveConfirmation: true, - ...(settings || {}) }; - this.state = state; - }; - proto.resetFormState = function() { - this.setState({ - formState: cleanFormState - }); - }; - - proto.getFormValuesFromEntity = function(entity) { - const settings = this.state.formSettings; - const data = Object.assign({}, entity); - - data.originalHash = data.hash; - delete data.hash; - - if (this.getFormValuesMutator) { - this.getFormValuesMutator(data, this.getFormValues()); - } - - this.populateFormValues(data); - }; - - proto.getFormValuesFromURL = async function(url) { - const settings = this.state.formSettings; - setTimeout(() => { - this.setState(previousState => { - if (previousState.formState.get('state') === FormState.Loading) { - return { - formState: previousState.formState.set('state', FormState.LoadingWithNotice) - }; - } - }); - }, 500); - - const response = await axios.get(getUrl(url)); - - let data = response.data; - - data.originalHash = data.hash; - delete data.hash; - - if (this.getFormValuesMutator) { - const newData = this.getFormValuesMutator(data, this.getFormValues()); - - if (newData !== undefined) { - data = newData; + proto.waitForFormServerValidated = async function () { + if (!this.isFormServerValidated()) { + await new Promise(resolve => { + formValidateResolve = resolve; + }); } - } + }; - this.populateFormValues(data); - }; + proto.scheduleFormRevalidate = function () { + scheduleValidateForm(this); + }; - proto.validateAndSendFormValuesToURL = async function(method, url) { - const settings = this.state.formSettings; - await this.waitForFormServerValidated(); + proto.updateForm = function (mutator) { + this.setState(previousState => { + const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {}; + + const formState = previousState.formState.withMutations(mutState => { + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + mutator(mutStateData); + + if (typeof onChangeBeforeValidationCallback === 'object') { + for (const key in onChangeBeforeValidationCallback) { + const oldValue = previousState.formState.getIn(['data', key, 'value']); + const newValue = mutStateData.getIn([key, 'value']); + onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, newValue); + } + } else { + onChangeBeforeValidationCallback(mutStateData); + } + })); + + validateFormState(this, mutState); + }); + + let newState = { + formState + }; + + + const onChangeCallback = this.state.formSettings.onChange || {}; + + if (typeof onChangeCallback === 'object') { + for (const key in onChangeCallback) { + const oldValue = previousState.formState.getIn(['data', key, 'value']); + const newValue = formState.getIn(['data', key, 'value']); + onChangeCallback[key](newState, key, oldValue, newValue); + } + } else { + onChangeCallback(newState); + } + + return newState; + }); + }; + + proto.updateFormValue = function (key, value) { + this.setState(previousState => { + const oldValue = previousState.formState.getIn(['data', key, 'value']); + + const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {}; + + const formState = previousState.formState.withMutations(mutState => { + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + mutStateData.setIn([key, 'value'], value); + + if (typeof onChangeBeforeValidationCallback === 'object') { + if (onChangeBeforeValidationCallback[key]) { + onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, value); + } + } else { + onChangeBeforeValidationCallback(mutStateData, key, oldValue, value); + } + })); + + validateFormState(this, mutState); + }); + + let newState = { + formState + }; + + + const onChangeCallback = this.state.formSettings.onChange || {}; + + if (typeof onChangeCallback === 'object') { + if (onChangeCallback[key]) { + onChangeCallback[key](newState, key, oldValue, value); + } + } else { + onChangeCallback(newState, key, oldValue, value); + } + + return newState; + }); + }; + + proto.getFormValue = function (name) { + return this.state.formState.getIn(['data', name, 'value']); + }; + + proto.getFormValues = function (name) { + if (!this.state || !this.state.formState) return undefined; + return this.state.formState.get('data').map(attr => attr.get('value')).toJS(); + }; + + proto.getFormError = function (name) { + return this.state.formState.getIn(['data', name, 'error']); + }; + + proto.isFormWithLoadingNotice = function () { + return this.state.formState.get('state') === FormState.LoadingWithNotice; + }; + + proto.isFormLoading = function () { + return this.state.formState.get('state') === FormState.Loading || this.state.formState.get('state') === FormState.LoadingWithNotice; + }; + + proto.isFormReady = function () { + return this.state.formState.get('state') === FormState.Ready; + }; + + const _isFormChanged = self => { + const currentData = getSaveData(self, self.state.formState.get('data')); + const savedData = self.state.formState.get('savedData'); + + function isDifferent(data1, data2, prefix) { + if (typeof data1 === 'object' && typeof data2 === 'object' && data1 && data2) { + const keys = new Set([...Object.keys(data1), ...Object.keys(data2)]); + for (const key of keys) { + if (isDifferent(data1[key], data2[key], `${prefix}/${key}`)) { + return true; + } + } + } else if (data1 !== data2) { + // console.log(prefix); + return true; + } + return false; + } + + const result = isDifferent(currentData, savedData, ''); + + return result; + }; + + proto.isFormChanged = function () { + const settings = this.state.formSettings; + + if (!settings.leaveConfirmation) return false; + + if (settings.getPreSubmitUpdater) { + // getPreSubmitUpdater is an async function. We cannot do anything async here. So to be on the safe side, + // we simply assume that the form has been changed. + return true; + } + + return _isFormChanged(this); + }; + + proto.isFormChangedAsync = async function () { + const settings = this.state.formSettings; + + if (!settings.leaveConfirmation) return false; - if (this.isFormWithoutErrors()) { if (settings.getPreSubmitUpdater) { const preSubmitUpdater = await settings.getPreSubmitUpdater(); @@ -1197,357 +1444,132 @@ const withForm = createComponentMixin([], [], (TargetClass, InnerClass) => { }); } - let data = this.getFormValues(); + return _isFormChanged(this); - if (this.submitFormValuesMutator) { - const newData = this.submitFormValuesMutator(data, true); - if (newData !== undefined) { - data = newData; - } - } + }; - const response = await axios.method(method, getUrl(url), data); + proto.isFormValidationShown = function () { + return this.state.formState.get('isValidationShown'); + }; - if (settings.leaveConfirmation) { - await new Promise((resolve, reject) => { - this.setState(previousState => ({ - formState: previousState.formState.set('savedData', getSaveData(this, previousState.formState.get('data'))) - }), resolve); - }); - } - - return response.data || true; - - } else { - this.showFormValidation(); - return false; - } - }; - - - proto.populateFormValues = function(data) { - const settings = this.state.formSettings; - - this.setState(previousState => ({ - formState: previousState.formState.withMutations(mutState => { - mutState.set('state', FormState.Ready); - - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - for (const key in data) { - mutStateData.set(key, Immutable.Map({ - value: data[key] - })); - } - })); - - if (settings.leaveConfirmation) { - mutState.set('savedData', getSaveData(this, mutState.get('data'))); - } - - validateFormState(this, mutState); - }) - })); - }; - - proto.waitForFormServerValidated = async function() { - if (!this.isFormServerValidated()) { - await new Promise(resolve => { formValidateResolve = resolve; }); - } - }; - - proto.scheduleFormRevalidate = function() { - scheduleValidateForm(this); - }; - - proto.updateForm = function(mutator) { - this.setState(previousState => { - const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {}; - - const formState = previousState.formState.withMutations(mutState => { - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - mutator(mutStateData); - - if (typeof onChangeBeforeValidationCallback === 'object') { - for (const key in onChangeBeforeValidationCallback) { - const oldValue = previousState.formState.getIn(['data', key, 'value']); - const newValue = mutStateData.getIn([key, 'value']); - onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, newValue); - } - } else { - onChangeBeforeValidationCallback(mutStateData); - } - })); - - validateFormState(this, mutState); - }); - - let newState = { - formState - }; - - - const onChangeCallback = this.state.formSettings.onChange || {}; - - if (typeof onChangeCallback === 'object') { - for (const key in onChangeCallback) { - const oldValue = previousState.formState.getIn(['data', key, 'value']); - const newValue = formState.getIn(['data', key, 'value']); - onChangeCallback[key](newState, key, oldValue, newValue); + proto.addFormValidationClass = function (className, name) { + if (this.isFormValidationShown()) { + const error = this.getFormError(name); + if (error) { + return className + ' is-invalid'; + } else { + return className + ' is-valid'; } } else { - onChangeCallback(newState); + return className; } + }; - return newState; - }); - }; - - proto.updateFormValue = function(key, value) { - this.setState(previousState => { - const oldValue = previousState.formState.getIn(['data', key, 'value']); - - const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {}; - - const formState = previousState.formState.withMutations(mutState => { - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - mutStateData.setIn([key, 'value'], value); - - if (typeof onChangeBeforeValidationCallback === 'object') { - if (onChangeBeforeValidationCallback[key]) { - onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, value); - } - } else { - onChangeBeforeValidationCallback(mutStateData, key, oldValue, value); - } - })); - - validateFormState(this, mutState); - }); - - let newState = { - formState - }; - - - const onChangeCallback = this.state.formSettings.onChange || {}; - - if (typeof onChangeCallback === 'object') { - if (onChangeCallback[key]) { - onChangeCallback[key](newState, key, oldValue, value); - } + proto.getFormValidationMessage = function (name) { + if (this.isFormValidationShown()) { + return this.getFormError(name); } else { - onChangeCallback(newState, key, oldValue, value); + return ''; } + }; - return newState; - }); - }; + proto.showFormValidation = function () { + this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', true)})); + }; - proto.getFormValue = function(name) { - return this.state.formState.getIn(['data', name, 'value']); - }; + proto.hideFormValidation = function () { + this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', false)})); + }; - proto.getFormValues = function(name) { - if (!this.state || !this.state.formState) return undefined; - return this.state.formState.get('data').map(attr => attr.get('value')).toJS(); - }; + proto.isFormWithoutErrors = function () { + return !this.state.formState.get('data').find(attr => attr.get('error')); + }; - proto.getFormError = function(name) { - return this.state.formState.getIn(['data', name, 'error']); - }; + proto.isFormServerValidated = function () { + return !this.state.formSettings.serverValidation || this.state.formSettings.serverValidation.changed.every(attr => this.state.formState.getIn(['data', attr, 'serverValidated'])); + }; - proto.isFormWithLoadingNotice = function() { - return this.state.formState.get('state') === FormState.LoadingWithNotice; - }; + proto.getFormStatusMessageText = function () { + return this.state.formState.get('statusMessageText'); + }; - proto.isFormLoading = function() { - return this.state.formState.get('state') === FormState.Loading || this.state.formState.get('state') === FormState.LoadingWithNotice; - }; + proto.getFormStatusMessageSeverity = function () { + return this.state.formState.get('statusMessageSeverity'); + }; - proto.isFormReady = function() { - return this.state.formState.get('state') === FormState.Ready; - }; + proto.setFormStatusMessage = function (severity, text) { + this.setState(previousState => ({ + formState: previousState.formState.withMutations(map => { + map.set('statusMessageText', text); + map.set('statusMessageSeverity', severity); + }) + })); + }; - const _isFormChanged = self => { - const currentData = getSaveData(self, self.state.formState.get('data')); - const savedData = self.state.formState.get('savedData'); + proto.clearFormStatusMessage = function () { + this.setState(previousState => ({ + formState: previousState.formState.withMutations(map => { + map.set('statusMessageText', ''); + }) + })); + }; - function isDifferent(data1, data2, prefix) { - if (typeof data1 === 'object' && typeof data2 === 'object' && data1 && data2) { - const keys = new Set([...Object.keys(data1), ...Object.keys(data2)]); - for (const key of keys) { - if (isDifferent(data1[key], data2[key], `${prefix}/${key}`)) { - return true; - } + proto.enableForm = function () { + this.setState(previousState => ({formState: previousState.formState.set('isDisabled', false)})); + }; + + proto.disableForm = function () { + this.setState(previousState => ({formState: previousState.formState.set('isDisabled', true)})); + }; + + proto.isFormDisabled = function () { + return this.state.formState.get('isDisabled'); + }; + + proto.formHandleErrors = async function (fn) { + const t = this.props.t; + try { + await fn(); + } catch (error) { + if (error instanceof interoperableErrors.ChangedError) { + this.disableForm(); + this.setFormStatusMessage('danger', + + {t('yourUpdatesCannotBeSaved')}{' '} + {t('someoneElseHasIntroducedModificationIn')} + + ); + return; } - } else if (data1 !== data2) { - // console.log(prefix); - return true; + + if (error instanceof interoperableErrors.NamespaceNotFoundError) { + this.disableForm(); + this.setFormStatusMessage('danger', + + {t('yourUpdatesCannotBeSaved')}{' '} + {t('itSeemsThatSomeoneElseHasDeletedThe')} + + ); + return; + } + + if (error instanceof interoperableErrors.NotFoundError) { + this.disableForm(); + this.setFormStatusMessage('danger', + + {t('yourUpdatesCannotBeSaved')}{' '} + {t('itSeemsThatSomeoneElseHasDeletedThe-1')} + + ); + return; + } + + throw error; } - return false; - } + }; - const result = isDifferent(currentData, savedData, ''); - - return result; - }; - - proto.isFormChanged = function() { - const settings = this.state.formSettings; - - if (!settings.leaveConfirmation) return false; - - if (settings.getPreSubmitUpdater) { - // getPreSubmitUpdater is an async function. We cannot do anything async here. So to be on the safe side, - // we simply assume that the form has been changed. - return true; - } - - return _isFormChanged(this); - }; - - proto.isFormChangedAsync = async function() { - const settings = this.state.formSettings; - - if (!settings.leaveConfirmation) return false; - - if (settings.getPreSubmitUpdater) { - const preSubmitUpdater = await settings.getPreSubmitUpdater(); - - await new Promise((resolve, reject) => { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(mutState => { - mutState.update('data', stateData => stateData.withMutations(preSubmitUpdater)); - }) - }), resolve); - }); - } - - return _isFormChanged(this); - - }; - - proto.isFormValidationShown = function() { - return this.state.formState.get('isValidationShown'); - }; - - proto.addFormValidationClass = function(className, name) { - if (this.isFormValidationShown()) { - const error = this.getFormError(name); - if (error) { - return className + ' is-invalid'; - } else { - return className + ' is-valid'; - } - } else { - return className; - } - }; - - proto.getFormValidationMessage = function(name) { - if (this.isFormValidationShown()) { - return this.getFormError(name); - } else { - return ''; - } - }; - - proto.showFormValidation = function() { - this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', true)})); - }; - - proto.hideFormValidation = function() { - this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', false)})); - }; - - proto.isFormWithoutErrors = function() { - return !this.state.formState.get('data').find(attr => attr.get('error')); - }; - - proto.isFormServerValidated = function() { - return !this.state.formSettings.serverValidation || this.state.formSettings.serverValidation.changed.every(attr => this.state.formState.getIn(['data', attr, 'serverValidated'])); - }; - - proto.getFormStatusMessageText = function() { - return this.state.formState.get('statusMessageText'); - }; - - proto.getFormStatusMessageSeverity = function() { - return this.state.formState.get('statusMessageSeverity'); - }; - - proto.setFormStatusMessage = function(severity, text) { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(map => { - map.set('statusMessageText', text); - map.set('statusMessageSeverity', severity); - }) - })); - }; - - proto.clearFormStatusMessage = function() { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(map => { - map.set('statusMessageText', ''); - }) - })); - }; - - proto.enableForm = function() { - this.setState(previousState => ({formState: previousState.formState.set('isDisabled', false)})); - }; - - proto.disableForm = function() { - this.setState(previousState => ({formState: previousState.formState.set('isDisabled', true)})); - }; - - proto.isFormDisabled = function() { - return this.state.formState.get('isDisabled'); - }; - - proto.formHandleErrors = async function(fn) { - const t = this.props.t; - try { - await fn(); - } catch (error) { - if (error instanceof interoperableErrors.ChangedError) { - this.disableForm(); - this.setFormStatusMessage('danger', - - {t('yourUpdatesCannotBeSaved')}{' '} - {t('someoneElseHasIntroducedModificationIn')} - - ); - return; - } - - if (error instanceof interoperableErrors.NamespaceNotFoundError) { - this.disableForm(); - this.setFormStatusMessage('danger', - - {t('yourUpdatesCannotBeSaved')}{' '} - {t('itSeemsThatSomeoneElseHasDeletedThe')} - - ); - return; - } - - if (error instanceof interoperableErrors.NotFoundError) { - this.disableForm(); - this.setFormStatusMessage('danger', - - {t('yourUpdatesCannotBeSaved')}{' '} - {t('itSeemsThatSomeoneElseHasDeletedThe-1')} - - ); - return; - } - - throw error; - } - }; - - return {}; + return {}; + } }); function filterData(obj, allowedKeys) { diff --git a/client/src/lib/i18n.js b/client/src/lib/i18n.js index 9e7d30bb..ba39afe7 100644 --- a/client/src/lib/i18n.js +++ b/client/src/lib/i18n.js @@ -1,8 +1,8 @@ 'use strict'; import React from 'react'; +import {I18nextProvider, withNamespaces} from 'react-i18next'; import i18n from 'i18next'; -import {withNamespaces} from "react-i18next"; import LanguageDetector from 'i18next-browser-languagedetector'; import mailtrainConfig from 'mailtrainConfig'; @@ -63,12 +63,30 @@ i18n export default i18n; -export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => { - return { - cls: withNamespaces()(TargetClass) - }; +export const TranslationContext = React.createContext(null); + +export const withTranslation = createComponentMixin({ + contexts: [{context: TranslationContext, propName: 't'}] }); +const TranslationContextProvider = withNamespaces()(props => { + return ( + + {props.children} + + ); +}); + +export function TranslationRoot(props) { + return ( + + + {props.children} + + + ); +} + export function tMark(key) { return key; } diff --git a/client/src/lib/page-common.js b/client/src/lib/page-common.js index a812ce58..488b1399 100644 --- a/client/src/lib/page-common.js +++ b/client/src/lib/page-common.js @@ -354,30 +354,34 @@ export function renderRoute(route, panelRouteCtor, loadingMessageFn, flashMessag } export const SectionContentContext = React.createContext(null); -export const withPageHelpers = createComponentMixin([{context: SectionContentContext, propName: 'sectionContent'}], [withErrorHandling], (TargetClass, InnerClass) => { - InnerClass.prototype.setFlashMessage = function(severity, text) { - return this.props.sectionContent.setFlashMessage(severity, text); - }; +export const withPageHelpers = createComponentMixin({ + contexts: [{context: SectionContentContext, propName: 'sectionContent'}], + deps: [withErrorHandling], + decoratorFn: (TargetClass, InnerClass) => { + InnerClass.prototype.setFlashMessage = function (severity, text) { + return this.props.sectionContent.setFlashMessage(severity, text); + }; - InnerClass.prototype.navigateTo = function(path) { - return this.props.sectionContent.navigateTo(path); - }; + InnerClass.prototype.navigateTo = function (path) { + return this.props.sectionContent.navigateTo(path); + }; - InnerClass.prototype.navigateBack = function() { - return this.props.sectionContent.navigateBack(); - }; + InnerClass.prototype.navigateBack = function () { + return this.props.sectionContent.navigateBack(); + }; - InnerClass.prototype.navigateToWithFlashMessage = function(path, severity, text) { - return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text); - }; + InnerClass.prototype.navigateToWithFlashMessage = function (path, severity, text) { + return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text); + }; - InnerClass.prototype.registerBeforeUnloadHandlers = function(handlers) { - return this.props.sectionContent.registerBeforeUnloadHandlers(handlers); - }; + InnerClass.prototype.registerBeforeUnloadHandlers = function (handlers) { + return this.props.sectionContent.registerBeforeUnloadHandlers(handlers); + }; - InnerClass.prototype.deregisterBeforeUnloadHandlers = function(handlers) { - return this.props.sectionContent.deregisterBeforeUnloadHandlers(handlers); - }; + InnerClass.prototype.deregisterBeforeUnloadHandlers = function (handlers) { + return this.props.sectionContent.deregisterBeforeUnloadHandlers(handlers); + }; - return {}; + return {}; + } }); diff --git a/client/src/lib/page.js b/client/src/lib/page.js index 5042b845..a6f8e271 100644 --- a/client/src/lib/page.js +++ b/client/src/lib/page.js @@ -683,21 +683,24 @@ export class NavDropdown extends Component { } -export const requiresAuthenticatedUser = createComponentMixin([], [withPageHelpers], (TargetClass, InnerClass) => { - class RequiresAuthenticatedUser extends React.Component { - constructor(props) { - super(props); - props.sectionContent.ensureAuthenticated(); +export const requiresAuthenticatedUser = createComponentMixin({ + deps: [withPageHelpers], + decoratorFn: (TargetClass, InnerClass) => { + class RequiresAuthenticatedUser extends React.Component { + constructor(props) { + super(props); + props.sectionContent.ensureAuthenticated(); + } + + render() { + return + } } - render() { - return - } + return { + cls: RequiresAuthenticatedUser + }; } - - return { - cls: RequiresAuthenticatedUser - }; }); export function getLanguageChooser(t) { diff --git a/client/src/lib/sandboxed-ckeditor-root.js b/client/src/lib/sandboxed-ckeditor-root.js index 3f319a47..6ab2f49f 100644 --- a/client/src/lib/sandboxed-ckeditor-root.js +++ b/client/src/lib/sandboxed-ckeditor-root.js @@ -4,8 +4,7 @@ import './public-path'; import React, {Component} from 'react'; import ReactDOM from 'react-dom'; -import {I18nextProvider} from 'react-i18next'; -import i18n, {withTranslation} from './i18n'; +import {TranslationRoot, withTranslation} from './i18n'; import {parentRPC, UntrustedContentRoot} from './untrusted'; import PropTypes from "prop-types"; import styles from "./sandboxed-ckeditor.scss"; @@ -126,9 +125,9 @@ export default function() { parentRPC.init(); ReactDOM.render( - + } /> - , + , document.getElementById('root') ); }; diff --git a/client/src/lib/sandboxed-codeeditor-root.js b/client/src/lib/sandboxed-codeeditor-root.js index e0803641..97326e06 100644 --- a/client/src/lib/sandboxed-codeeditor-root.js +++ b/client/src/lib/sandboxed-codeeditor-root.js @@ -4,8 +4,7 @@ import './public-path'; import React, {Component} from 'react'; import ReactDOM from 'react-dom'; -import {I18nextProvider} from 'react-i18next'; -import i18n, {withTranslation} from './i18n'; +import {TranslationRoot, withTranslation} from './i18n'; import {parentRPC, UntrustedContentRoot} from './untrusted'; import PropTypes from "prop-types"; import styles from "./sandboxed-codeeditor.scss"; @@ -211,9 +210,9 @@ export default function() { parentRPC.init(); ReactDOM.render( - + } /> - , + , document.getElementById('root') ); }; diff --git a/client/src/lib/sandboxed-grapesjs-root.js b/client/src/lib/sandboxed-grapesjs-root.js index bbb6608e..f9c2f76d 100644 --- a/client/src/lib/sandboxed-grapesjs-root.js +++ b/client/src/lib/sandboxed-grapesjs-root.js @@ -4,8 +4,7 @@ import './public-path'; import React, {Component} from 'react'; import ReactDOM from 'react-dom'; -import {I18nextProvider} from 'react-i18next'; -import i18n, {withTranslation} from './i18n'; +import {TranslationRoot, withTranslation} from './i18n'; import {parentRPC, UntrustedContentRoot} from './untrusted'; import PropTypes from "prop-types"; import {getPublicUrl, getSandboxUrl, getTrustedUrl} from "./urls"; @@ -626,9 +625,9 @@ export default function() { parentRPC.init(); ReactDOM.render( - + } /> - , + , document.getElementById('root') ); }; diff --git a/client/src/lib/sandboxed-mosaico-root.js b/client/src/lib/sandboxed-mosaico-root.js index b86403a8..4a3dfd5a 100644 --- a/client/src/lib/sandboxed-mosaico-root.js +++ b/client/src/lib/sandboxed-mosaico-root.js @@ -4,8 +4,7 @@ import './public-path'; import React, {Component} from 'react'; import ReactDOM from 'react-dom'; -import {I18nextProvider} from 'react-i18next'; -import i18n, {withTranslation} from './i18n'; +import {TranslationRoot, withTranslation} from './i18n'; import {parentRPC, UntrustedContentRoot} from './untrusted'; import PropTypes from "prop-types"; import {getPublicUrl, getSandboxUrl, getTrustedUrl} from "./urls"; @@ -149,9 +148,9 @@ export default function() { parentRPC.init(); ReactDOM.render( - + } /> - , + , document.getElementById('root') ); }; diff --git a/client/src/namespaces/CUD.js b/client/src/namespaces/CUD.js index c90d27dd..9cc0abea 100644 --- a/client/src/namespaces/CUD.js +++ b/client/src/namespaces/CUD.js @@ -82,9 +82,11 @@ export default class CUD extends Component { this.removeNsIdSubtree(data); } - this.setState({ - treeData: data - }); + if (this.isComponentMounted()) { + this.setState({ + treeData: data + }); + } } } @@ -191,7 +193,7 @@ export default class CUD extends Component { render() { const t = this.props.t; const isEdit = !!this.props.entity; - const canDelete = isEdit && !this.isEditGlobal() && this.props.entity.permissions.includes('delete'); + const canDelete = isEdit && !this.isEditGlobal() && mailtrainConfig.user.namespace !== this.props.entity.id && this.props.entity.permissions.includes('delete'); return (
diff --git a/client/src/namespaces/List.js b/client/src/namespaces/List.js index 8185cd7f..56b952b0 100644 --- a/client/src/namespaces/List.js +++ b/client/src/namespaces/List.js @@ -10,6 +10,7 @@ import {checkPermissions} from "../lib/permissions"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {getGlobalNamespaceId} from "../../../shared/namespaces"; import {withComponentMixins} from "../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -64,7 +65,8 @@ export default class List extends Component { }); } - if (Number.parseInt(node.key) !== getGlobalNamespaceId()) { + const namespaceId = Number.parseInt(node.key); + if (namespaceId !== getGlobalNamespaceId() && mailtrainConfig.user.namespace !== namespaceId) { tableAddDeleteButton(actions, this, node.data.permissions, `rest/namespaces/${node.key}`, node.data.unsanitizedTitle, t('deletingNamespace'), t('namespaceDeleted')); } diff --git a/client/src/root.js b/client/src/root.js index 0e0e15ce..92bfba68 100644 --- a/client/src/root.js +++ b/client/src/root.js @@ -4,8 +4,7 @@ import './lib/public-path'; import React, {Component} from 'react'; import ReactDOM from 'react-dom'; -import {I18nextProvider} from 'react-i18next'; -import i18n, {withTranslation} from './lib/i18n'; +import {TranslationRoot, withTranslation} from './lib/i18n'; import account from './account/root'; import login from './login/root'; import blacklist from './blacklist/root'; @@ -139,7 +138,7 @@ class Root extends Component { } export default function() { - ReactDOM.render(,document.getElementById('root')); + ReactDOM.render(,document.getElementById('root')); }; diff --git a/server/lib/dependency-helpers.js b/server/lib/dependency-helpers.js index 9992ff3a..9aaba342 100644 --- a/server/lib/dependency-helpers.js +++ b/server/lib/dependency-helpers.js @@ -37,17 +37,12 @@ async function ensureNoDependencies(tx, context, id, depSpecs) { name: row.name, link: entityType.clientLink(row.id) }); - } else if (await shares.checkEntityPermissionTx(tx, context, depSpec.entityTypeId, row.id, 'view')) { + } else if (!depSpec.viewPermission && await shares.checkEntityPermissionTx(tx, context, depSpec.entityTypeId, row.id, 'view')) { deps.push({ entityTypeId: depSpec.entityTypeId, name: row.name, link: entityType.clientLink(row.id) }); - } else { - deps.push({ - entityTypeId: depSpec.entityTypeId, - id: row.id - }); } } }