Source: cmfConnect.js

/**
 * This module connect your component in the CMF environment.
 * @module react-cmf/lib/cmfConnect
 * @example
import { cmfConnect } from '@talend/react-cmf';

function MyComponent(props) {
	const onClick = (event) => {
		props.dispatchActionCreator('myaction', event, { props: props });
	};
	return <button onClick={onClick}>Edit {props.foo.name}</button>;
}

function mapStateToProps(state) {
	return {
		foo: state.cmf.collection.get('foo', { name: 'world' }),
	};
}

export default cmfConnect({
	mapStateToProps,
});
 */
import PropTypes from 'prop-types';
import { useState, useContext, useEffect, forwardRef } from 'react';
import hoistStatics from 'hoist-non-react-statics';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect, useStore } from 'react-redux';
import { randomUUID } from '@talend/utils';
import actions from './actions';
import actionCreator from './actionCreator';
import component from './component';
import CONST from './constant';
import expression from './expression';
import onEvent from './onEvent';
import { initState, getStateAccessors, getStateProps } from './componentState';
import { mapStateToViewProps } from './settings';
import omit from './omit';
import { RegistryContext } from './RegistryProvider';

export function getComponentName(WrappedComponent) {
	return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

export function getComponentId(componentId, props) {
	if (typeof componentId === 'function') {
		return componentId(props) || 'default';
	} else if (typeof componentId === 'string') {
		return componentId;
	} else if (props.componentId) {
		return props.componentId;
	}
	return 'default';
}

export function getStateToProps({
	defaultProps,
	componentId,
	ownProps,
	state,
	mapStateToProps,
	WrappedComponent,
}) {
	const props = { ...defaultProps };

	const cmfProps = getStateProps(
		state,
		getComponentName(WrappedComponent),
		getComponentId(componentId, ownProps),
	);

	Object.assign(props, cmfProps);

	const viewProps = mapStateToViewProps(
		state,
		ownProps,
		getComponentName(WrappedComponent),
		getComponentId(componentId, ownProps),
	);

	Object.assign(props, viewProps);

	let userProps = {};
	if (mapStateToProps) {
		userProps = mapStateToProps(state, { ...ownProps, ...props }, cmfProps);
	}
	Object.assign(props, userProps);
	Object.assign(props, expression.mapStateToProps(state, { ...ownProps, ...props }));
	return props;
}

export function getDispatchToProps({
	defaultState,
	dispatch,
	componentId,
	mapDispatchToProps,
	ownProps,
	WrappedComponent,
}) {
	const cmfProps = getStateAccessors(
		dispatch,
		getComponentName(WrappedComponent),
		getComponentId(componentId, ownProps),
		defaultState,
	);
	cmfProps.dispatch = dispatch;
	cmfProps.getComponent = component.get;
	cmfProps.dispatchActionCreator = (actionId, event, data, context) => {
		dispatch(actionCreator.get(context, actionId)(event, data, context));
	};

	let userProps = {};
	if (mapDispatchToProps) {
		if (process.env.NODE_ENV === 'development') {
			// eslint-disable-next-line no-console
			console.warn(`DEPRECATION WARNING: mapDispatchToProps will be removed from cmfConnect.
			Please use the injectedProps dispatchActionCreator or dispatch`);
		}
		userProps = mapDispatchToProps(dispatch, ownProps, cmfProps);
	}

	return { ...cmfProps, ...userProps };
}

/**
 * Internal: you should not have to use this
 * return the merged props which cleanup expression props
 * call mergeProps if exists after the cleanup
 * @param {object} options { mergeProps, stateProps, dispatchProps, ownProps }
 */
export function getMergeProps({ mergeProps, stateProps, dispatchProps, ownProps }) {
	if (mergeProps) {
		return mergeProps(
			expression.mergeProps(stateProps),
			expression.mergeProps(dispatchProps),
			expression.mergeProps(ownProps),
		);
	}
	return {
		...expression.mergeProps(ownProps),
		...expression.mergeProps(dispatchProps),
		...expression.mergeProps(stateProps),
	};
}

/**
 * this function wrap your component to inject CMF props
 * @example
 * The following props are injected:
 * - props.state
 * - props.setState
 * - props.initState (you should never have to call it your self)
 * - dispatch(action)
 * - dispatchActionCreator(id, event, data, [context])
 *
 * support for the following props
 * - initialState (called by props.initState)
 * - didMountActionCreator (id or array of id)
 * - willUnMountActionCreator (id or array of id)
 * - componentId (or will use uuid)
 * - keepComponentState (boolean, overrides the keepComponentState defined in container)
 * - didMountActionCreator (string called as action creator in didMount)
 * - view (string to inject the settings as props with ref support)
 * - whateverExpression (will inject `whatever` props and will remove it)
 * @example
 * options has the following shape:
{
	componentId,  // string or function(props) to compute the id in the store
	defaultState,  // the default state when the component is mount
	keepComponent,  // boolean, when the component is unmount, to keep it's state in redux store
	mapStateToProps,  // function(state, ownProps) that should return the props (same as redux)
	mapDispatchToProps,  // same as redux connect arg, you should use dispatchActionCreator instead
	mergeProps,  // same as redux connect
}
 * @param {object} options Option objects to configure the redux connect
 * @return {ReactComponent}
 */
export default function cmfConnect({
	componentId,
	defaultState,
	defaultProps,
	keepComponentState,
	mapStateToProps,
	mapDispatchToProps,
	mergeProps,
	omitCMFProps = true,
	withComponentRegistry = false,
	withDispatch = false,
	withDispatchActionCreator = false,
	withComponentId = false,
	...rest
} = {}) {
	const propsToOmit = [];
	if (omitCMFProps) {
		if (!defaultState) {
			propsToOmit.push(...CONST.INJECTED_STATE_PROPS);
		}
		if (!withComponentRegistry) {
			propsToOmit.push('getComponent');
		}
		if (!withComponentId) {
			propsToOmit.push('componentId');
		}
		if (!withDispatch) {
			propsToOmit.push('dispatch');
		}
		if (!withDispatchActionCreator) {
			propsToOmit.push('dispatchActionCreator');
		}
	}
	let displayNameWarning = true;

	return function wrapWithCMF(WrappedComponent) {
		if (!WrappedComponent.displayName && displayNameWarning) {
			displayNameWarning = false;
			// eslint-disable-next-line no-console
			console.warn(
				`${WrappedComponent.name} has no displayName. Please read https://jira.talendforge.org/browse/TUI-302`,
			);
		}
		function getState(state, id = 'default') {
			return state.cmf.components.getIn([getComponentName(WrappedComponent), id], defaultState);
		}
		function getSetStateAction(state, id, type) {
			return {
				type: type || `${getComponentName(WrappedComponent)}.setState`,
				cmf: {
					componentState: actions.components.mergeState(
						getComponentName(WrappedComponent),
						id,
						state,
					),
				},
			};
		}

		function CMFContainer(props, ref) {
			const [instanceId] = useState(randomUUID());
			const registry = useContext(RegistryContext);
			const store = useStore();

			function dispatchActionCreator(actionCreatorId, event, data, extraContext) {
				const extendedContext = { registry, store, ...extraContext };
				props.dispatchActionCreator(actionCreatorId, event, data, extendedContext);
			}

			useEffect(() => {
				initState(props);
				if (props.saga) {
					dispatchActionCreator(
						'cmf.saga.start',
						{ type: 'DID_MOUNT', componentId: instanceId },
						{
							...props, // DEPRECATED
							componentId: getComponentId(componentId, props),
						},
					);
				}
				if (props.didMountActionCreator) {
					dispatchActionCreator(props.didMountActionCreator, null, props);
				}
				return () => {
					if (props.willUnmountActionCreator) {
						dispatchActionCreator(props.willUnmountActionCreator, null, props);
					}
					// if the props.keepComponentState is present we have to stick to it
					if (
						props.keepComponentState === false ||
						(props.keepComponentState === undefined && !keepComponentState)
					) {
						props.deleteState(props.initialState);
					}
					if (props.saga) {
						dispatchActionCreator(
							'cmf.saga.stop',
							{ type: 'WILL_UNMOUNT', componentId: instanceId },
							props,
						);
					}
				};
				// eslint-disable-next-line react-hooks/exhaustive-deps
			}, []);
			function getOnEventProps() {
				return Object.keys(props).reduce(
					(acc, key) => {
						// TODO check how to replace the this
						onEvent.addOnEventSupport(onEvent.DISPATCH, { props }, acc, key);
						onEvent.addOnEventSupport(onEvent.ACTION_CREATOR, { props }, acc, key);
						onEvent.addOnEventSupport(onEvent.SETSTATE, { props }, acc, key);
						return acc;
					},
					{ toOmit: [], dispatchActionCreator },
				);
			}

			if (props.renderIf === false) {
				return null;
			}
			const { toOmit, spreadCMFState, ...handlers } = getOnEventProps();

			// remove all internal props already used by the container
			delete handlers.dispatchActionCreator;
			toOmit.push(...CONST.CMF_PROPS, ...propsToOmit);
			if (props.omitRouterProps) {
				toOmit.push('omitRouterProps', ...CONST.INJECTED_ROUTER_PROPS);
			}
			let spreadedState = {};
			if ((spreadCMFState || props.spreadCMFState) && props.state) {
				spreadedState = props.state.toJS();
			}

			const newProps = {
				...omit(props, toOmit),
				...handlers,
				...spreadedState,
			};
			if (newProps.dispatchActionCreator && toOmit.indexOf('dispatchActionCreator') === -1) {
				// override to inject CMFContainer context
				newProps.dispatchActionCreator = dispatchActionCreator;
			}
			if (!newProps.state && defaultState && toOmit.indexOf('state') === -1) {
				newProps.state = defaultState;
			}
			if (rest.forwardRef) {
				return <WrappedComponent {...newProps} ref={ref} />;
			}
			return <WrappedComponent {...newProps} />;
		}
		let CMFWithRef = hoistStatics(CMFContainer, WrappedComponent);
		CMFContainer.displayName = `CMF(${getComponentName(WrappedComponent)})`;

		CMFContainer.propTypes = {
			...cmfConnect.propTypes,
		};
		CMFContainer.WrappedComponent = WrappedComponent;
		CMFContainer.getState = getState;
		// eslint-disable-next-line @typescript-eslint/default-param-last
		CMFContainer.setStateAction = function setStateAction(state, id = 'default', type) {
			if (typeof state !== 'function') {
				return getSetStateAction(state, id, type);
			}
			return (_, getReduxState) =>
				getSetStateAction(state(getState(getReduxState(), id)), id, type);
		};
		if (rest.forwardRef) {
			CMFWithRef = forwardRef(CMFWithRef);
			CMFWithRef.displayName = `ForwardRef(${CMFContainer.displayName})`;
			CMFWithRef.WrappedComponent = CMFContainer.WrappedComponent;
			CMFWithRef.getState = CMFContainer.getState;
			CMFWithRef.setStateAction = CMFContainer.setStateAction;
		}

		const Connected = connect(
			(state, ownProps) =>
				getStateToProps({
					componentId,
					defaultProps,
					defaultState,
					ownProps,
					state,
					mapStateToProps,
					WrappedComponent,
				}),
			(dispatch, ownProps) =>
				getDispatchToProps({
					defaultState,
					dispatch,
					componentId,
					mapDispatchToProps,
					ownProps,
					WrappedComponent,
				}),
			(stateProps, dispatchProps, ownProps) =>
				getMergeProps({
					mergeProps,
					stateProps,
					dispatchProps,
					ownProps,
				}),
			{ ...rest },
		)(CMFWithRef);
		Connected.CMFContainer = CMFContainer;
		return Connected;
	};
}

cmfConnect.INJECTED_PROPS = CONST.INJECTED_PROPS;
cmfConnect.INJECTED_STATE_PROPS = CONST.INJECTED_STATE_PROPS;
cmfConnect.INJECTED_ROUTER_PROPS = CONST.INJECTED_ROUTER_PROPS;
cmfConnect.ALL_INJECTED_PROPS = CONST.INJECTED_PROPS.concat(['getComponent', 'componentId']);
cmfConnect.omit = omit;
cmfConnect.omitAllProps = props => cmfConnect.omit(props, cmfConnect.ALL_INJECTED_PROPS);

cmfConnect.propTypes = {
	state: ImmutablePropTypes.map,
	initialState: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.object]),
	getComponent: PropTypes.func,
	setState: PropTypes.func,
	initState: PropTypes.func,
	dispatchActionCreator: PropTypes.func,
	dispatch: PropTypes.func,
};