Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REFACTOR: InsertModeSelector & dndTypes to TypeScript #3430

Open
wants to merge 6 commits into
base: 8.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Selector} from 'testcafe';
import {ReactSelector} from 'testcafe-react-selectors';
import {beforeEach, subSection, checkPropTypes} from './../../utils.js';
import {Page} from './../../pageModel';
import {beforeEach, subSection, checkPropTypes} from '../../utils';
import {Page} from '../../pageModel';

/* global fixture:true */

Expand Down
2 changes: 1 addition & 1 deletion Tests/IntegrationTests/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const adminUser = Role(adminUrl, async t => {

export async function checkPropTypes() {
const {error} = await t.getBrowserConsoleMessages();
// Quick fix hack to get rid of the react life cycle warnings
// Quick fix hack to get rid of the React life cycle warnings
if (error[0] && error[0].search('Warning: Unsafe legacy lifecycles') >= 0) {
delete error[0];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const dndTypes = {
NODE: 'neos-tree-node',
MULTISELECT: 'neos-multiselect-value'
};
} as const;

export default dndTypes;
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@ import ButtonGroup from '@neos-project/react-ui-components/src/ButtonGroup/';
import Button from '@neos-project/react-ui-components/src/Button/';
import Icon from '@neos-project/react-ui-components/src/Icon/';

import {neos} from '@neos-project/neos-ui-decorators';
import {neos, NeosifiedProps} from '@neos-project/neos-ui-decorators';
import I18n from '@neos-project/neos-ui-i18n';

import style from './style.module.css';

type InsertMode = 'after' | 'before' | 'into'

type InsertModeSelectorOwnProps = {
mode?: InsertMode
enableAlongsideModes: boolean
enableIntoMode: boolean
onSelect: (mode: InsertMode) => string
}

const neosifier = neos((globalRegistry) => ({
i18nRegistry: globalRegistry.get('i18n')
}));

type InjectedNeosProps = NeosifiedProps<typeof neosifier>

type InsertModeSelectorProps = InsertModeSelectorOwnProps & InjectedNeosProps

const MODE_AFTER = 'after';
const MODE_BEFORE = 'before';
const MODE_INTO = 'into';
Expand All @@ -20,10 +37,10 @@ const MODE_INTO = 'into';
//
// If the `into` mode is allowed, it should always be preferred.
//
// Otherwise `after` should be preferred, since `before` is a rather exceptional
// Otherwise, `after` should be preferred since `before` is a rather exceptional
// choice.
//
const calculatePreferredInitialMode = props => {
const calculatePreferredInitialMode = (props: InsertModeSelectorProps) => {
const {enableAlongsideModes, enableIntoMode} = props;

if (enableIntoMode) {
Expand All @@ -37,22 +54,17 @@ const calculatePreferredInitialMode = props => {
return null;
};

@neos(globalRegistry => ({
i18nRegistry: globalRegistry.get('i18n')
}))
export default class InsertModeSelector extends PureComponent {
class InsertModeSelector extends PureComponent<InsertModeSelectorProps> {
static propTypes = {
mode: PropTypes.string,
mode: PropTypes.oneOf([MODE_AFTER, MODE_BEFORE, MODE_INTO]),
enableAlongsideModes: PropTypes.bool.isRequired,
enableIntoMode: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,

// injected neos props
i18nRegistry: PropTypes.object.isRequired
};

options = [];

selectPreferredInitialModeIfModeIsEmpty(props) {
selectPreferredInitialModeIfModeIsEmpty(props: InsertModeSelectorProps) {
const {mode, onSelect} = props;
let reconsiderMode = !mode;

Expand All @@ -77,7 +89,7 @@ export default class InsertModeSelector extends PureComponent {
this.selectPreferredInitialModeIfModeIsEmpty(this.props);
}

UNSAFE_componentWillReceiveProps(props) {
UNSAFE_componentWillReceiveProps(props: InsertModeSelectorProps) {
this.selectPreferredInitialModeIfModeIsEmpty(props);
}

Expand Down Expand Up @@ -130,9 +142,11 @@ export default class InsertModeSelector extends PureComponent {
);
}

handleSelect = mode => {
handleSelect = (mode: InsertMode) => {
const {onSelect} = this.props;

onSelect(mode);
}
}

export default neosifier(InsertModeSelector);
1 change: 1 addition & 0 deletions packages/neos-ui-decorators/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"typescript": "^4.6.4"
},
"peerDependencies": {
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.5.10",
"react": "^16.12.0"
},
Expand Down
92 changes: 92 additions & 0 deletions packages/neos-ui-decorators/src/decorator-type-helpers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Type definitions extracted from react-redux 7.1
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/59c89cc048de1e795eb3c88108fcb982ee4674ae/types/react-redux/index.d.ts
// bundled via rollup https://github.com/Swatinem/rollup-plugin-dts/tree/master to extract only the { ConnectedProps, InferableComponentEnhancerWithProps }

import {JSXElementConstructor, ComponentClass, ClassAttributes, NamedExoticComponent} from 'react';
import {NonReactStatics} from 'hoist-non-react-statics';

type DistributiveOmit<T, K extends keyof T> = T extends
unknown ? Omit<T, K> : never;

/**
* A property P will be present if:
* - it is present in DecorationTargetProps
*
* Its value will be dependent on the following conditions
* - if property P is present in InjectedProps and its definition extends the definition
* in DecorationTargetProps, then its definition will be that of DecorationTargetProps[P]
* - if property P is not present in InjectedProps then its definition will be that of
* DecorationTargetProps[P]
* - if property P is present in InjectedProps but does not extend the
* DecorationTargetProps[P] definition, its definition will be that of InjectedProps[P]
*/
type Matching<InjectedProps, DecorationTargetProps> = {
[P in keyof DecorationTargetProps]: P extends keyof InjectedProps
? InjectedProps[P] extends DecorationTargetProps[P]
? DecorationTargetProps[P]
: InjectedProps[P]
: DecorationTargetProps[P];
};

/**
* a property P will be present if :
* - it is present in both DecorationTargetProps and InjectedProps
* - InjectedProps[P] can satisfy DecorationTargetProps[P]
* ie: decorated component can accept more types than decorator is injecting
*
* For decoration, inject props or ownProps are all optionally
* required by the decorated (right hand side) component.
* But any property required by the decorated component must be satisfied by the injected property.
*/
type Shared<
InjectedProps,
DecorationTargetProps
> = {
[P in Extract<keyof InjectedProps, keyof DecorationTargetProps>]?: InjectedProps[P] extends DecorationTargetProps[P] ? DecorationTargetProps[P] : never;
};

// Infers prop type from component C
type GetProps<C> = C extends JSXElementConstructor<infer P>
? C extends ComponentClass<P> ? ClassAttributes<InstanceType<C>> & P : P
: never;

// Applies LibraryManagedAttributes (proper handling of defaultProps
// and propTypes).
type GetLibraryManagedProps<C> = JSX.LibraryManagedAttributes<C, GetProps<C>>;

// Defines WrappedComponent and derives non-react statics.
type ConnectedComponent<
C extends JSXElementConstructor<any>,
P
> = NamedExoticComponent<P> & NonReactStatics<C> & {
WrappedComponent: C;
};

// Injects props and removes them from the prop requirements.
// Will not pass through the injected props if they are passed in during
// render. Also adds new prop requirements from TNeedsProps.
// Uses distributive omit to preserve discriminated unions part of original prop type
type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> =
<C extends JSXElementConstructor<Matching<TInjectedProps, GetProps<C>>>>(
component: C
) => ConnectedComponent<C, DistributiveOmit<GetLibraryManagedProps<C>, keyof Shared<TInjectedProps, GetLibraryManagedProps<C>>> & TNeedsProps>;

// Injects props and removes them from the prop requirements.
// Will not pass through the injected props if they are passed in during
// render.
type InferableComponentEnhancer<TInjectedProps> =
InferableComponentEnhancerWithProps<TInjectedProps, {}>;

/**
* Infers the type of props that a connector will inject into a component.
*/
type ConnectedProps<TConnector> =
TConnector extends InferableComponentEnhancerWithProps<infer TInjectedProps, any>
? unknown extends TInjectedProps
? TConnector extends InferableComponentEnhancer<infer TInjectedProps>
? TInjectedProps
: never
: TInjectedProps
: never;

export {ConnectedProps as HighOrderComponentProps, InferableComponentEnhancerWithProps};
8 changes: 1 addition & 7 deletions packages/neos-ui-decorators/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
import neos, {NeosContext, NeosInjectedProps} from './neos';

export {
neos,
NeosContext,
NeosInjectedProps
};
export {neos, NeosContext, NeosifiedProps} from './neos';
JamesAlias marked this conversation as resolved.
Show resolved Hide resolved
48 changes: 48 additions & 0 deletions packages/neos-ui-decorators/src/neos.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
InferableComponentEnhancerWithProps,
HighOrderComponentProps
} from './decorator-type-helpers';
import {GlobalRegistry} from '@neos-project/neos-ts-interfaces';

export interface NeosContextInterface {
globalRegistry: GlobalRegistry;
configuration: {};
routes: {};
}

/**
* Infers the type of props that a neosifier will inject into a component.
*
* @example
* const neosifier = neos((globalRegistry: GlobalRegistry) => ({
* i18nRegistry: globalRegistry.get('i18n')
* }));
* type NeosProps = NeosifiedProps<typeof neosifier>;
*
* const MyPlainComponent = (props: NeosProps & OwnProps) => "huhu";
*
* export const MyComponent = neosifier(MyPlainComponent);
*
*/
export type NeosifiedProps<TNeosifier> = HighOrderComponentProps<TNeosifier>;

export const NeosContext: React.Context<NeosContextInterface | null>;

type MapRegistryToPropsParam<TStateProps> = (
globalRegistry: GlobalRegistry
) => TStateProps;

interface Neos {
<TStateProps = {}, TOwnProps = {}>(
mapRegistryToProps: MapRegistryToPropsParam<TStateProps>
): InferableComponentEnhancerWithProps<
TStateProps & { neos: NeosContextInterface },
TOwnProps
>;
}

/**
* Creates an higher order component to easily spread global configuration
* {@link NeosifiedProps}
*/
export const neos: Neos;
40 changes: 40 additions & 0 deletions packages/neos-ui-decorators/src/neos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import {defaultMemoize} from 'reselect';

// We need to memoize configuration and global registry; otherwise a new object is created at every render; leading to
// LOADS of unnecessary re-draws.
const buildConfigurationAndGlobalRegistry = defaultMemoize((configuration, globalRegistry, routes) => ({configuration, globalRegistry, routes}));

export const NeosContext = React.createContext(null);

export const neos = (mapRegistriesToProps) => (WrappedComponent) => {
const Decorator = class NeosDecorator extends React.PureComponent {
static Original = WrappedComponent;

static contextType = NeosContext;

static displayName = `Neos(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;

render() {
return (
<NeosContext.Consumer>
{context => {
if (!context) {
console.error('Context missing!', this.props);
return null;
}
const registriesToPropsMap = mapRegistriesToProps ? mapRegistriesToProps(context.globalRegistry) : {};
return (
<WrappedComponent
neos={buildConfigurationAndGlobalRegistry(context.configuration, context.globalRegistry, context.routes)}
{...this.props}
{...registriesToPropsMap}
/>
);
}}
</NeosContext.Consumer>
);
}
};
return Decorator;
};
52 changes: 0 additions & 52 deletions packages/neos-ui-decorators/src/neos.tsx

This file was deleted.

12 changes: 5 additions & 7 deletions packages/neos-ui-i18n/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React from 'react';
import {neos} from '@neos-project/neos-ui-decorators';
import {GlobalRegistry} from '@neos-project/neos-ts-interfaces';
import {NeosInjectedProps} from '@neos-project/neos-ui-decorators/src/neos';
import {neos, NeosifiedProps} from '@neos-project/neos-ui-decorators';

const regsToProps = (globalRegistry: GlobalRegistry) => ({
const neosifier = neos((globalRegistry) => ({
i18nRegistry: globalRegistry.get('i18n')
});
type InjectedProps = NeosInjectedProps<typeof regsToProps>;
}));
type InjectedProps = NeosifiedProps<typeof neosifier>;

interface I18nProps {
// Fallback key which gets rendered once the i18n service doesn't return a translation.
Expand Down Expand Up @@ -36,4 +34,4 @@ class I18n extends React.PureComponent<I18nProps & InjectedProps> {
}
}

export default neos<I18nProps, InjectedProps>(regsToProps)(I18n);
export default neosifier(I18n);