Remove button removed from the namespace that contains the current user.

Optimizations in how mixins are composed. The refresh should now be up to 2x faster for deeper hierarchies.
This commit is contained in:
Tomas Bures 2019-07-27 17:47:25 +02:00
parent 6ae9143c22
commit a46c8fa9c3
15 changed files with 764 additions and 680 deletions

View file

@ -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

View file

@ -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 (
<TargetClassWithCtors {...props}/>
);
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 (
<TargetClassWithCtors {...props}/>
);
}
}
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 <DecoratedInner {...props}/>
};
return <DecoratedInner {...props}/>
for (const [propName, Context] of contexts.entries()) {
const existingInnerFn = innerFn;
innerFn = parentProps => (
<Context.Consumer>
{
value => existingInnerFn({
...parentProps,
[propName]: value
})
}
</Context.Consumer>
);
}
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 <DecoratedInner {...props}/>
};
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;
};
}

View file

@ -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 (
<ParentErrorHandlerContext.Provider value={this}>
{originalRender.apply(this)}
</ParentErrorHandlerContext.Provider>
);
InnerClass.prototype.render = function () {
return (
<ParentErrorHandlerContext.Provider value={this}>
{originalRender.apply(this)}
</ParentErrorHandlerContext.Provider>
);
}
InnerClass.prototype.handleError = function (error) {
handleError(this, error);
};
return {};
}
InnerClass.prototype.handleError = function(error) {
handleError(this, error);
};
return {};
});
export function withAsyncErrorHandler(target, name, descriptor) {

File diff suppressed because it is too large Load diff

View file

@ -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 (
<TranslationContext.Provider value={props.t}>
{props.children}
</TranslationContext.Provider>
);
});
export function TranslationRoot(props) {
return (
<I18nextProvider i18n={ i18n }>
<TranslationContextProvider>
{props.children}
</TranslationContextProvider>
</I18nextProvider>
);
}
export function tMark(key) {
return key;
}

View file

@ -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 {};
}
});

View file

@ -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 <TargetClass {...this.props}/>
}
}
render() {
return <TargetClass {...this.props}/>
}
return {
cls: RequiresAuthenticatedUser
};
}
return {
cls: RequiresAuthenticatedUser
};
});
export function getLanguageChooser(t) {

View file

@ -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(
<I18nextProvider i18n={ i18n }>
<TranslationRoot>
<UntrustedContentRoot render={props => <CKEditorSandbox {...props} />} />
</I18nextProvider>,
</TranslationRoot>,
document.getElementById('root')
);
};

View file

@ -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(
<I18nextProvider i18n={ i18n }>
<TranslationRoot>
<UntrustedContentRoot render={props => <CodeEditorSandbox {...props} />} />
</I18nextProvider>,
</TranslationRoot>,
document.getElementById('root')
);
};

View file

@ -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(
<I18nextProvider i18n={ i18n }>
<TranslationRoot>
<UntrustedContentRoot render={props => <GrapesJSSandbox {...props} />} />
</I18nextProvider>,
</TranslationRoot>,
document.getElementById('root')
);
};

View file

@ -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(
<I18nextProvider i18n={ i18n }>
<TranslationRoot>
<UntrustedContentRoot render={props => <MosaicoSandbox {...props} />} />
</I18nextProvider>,
</TranslationRoot>,
document.getElementById('root')
);
};

View file

@ -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 (
<div>

View file

@ -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'));
}

View file

@ -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(<I18nextProvider i18n={ i18n }><Root/></I18nextProvider>,document.getElementById('root'));
ReactDOM.render(<TranslationRoot><Root/></TranslationRoot>,document.getElementById('root'));
};

View file

@ -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
});
}
}
}