Add redux to CTB

Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
HichamELBSI 2021-01-19 17:54:43 +01:00
parent e8e93be070
commit 58a7665c63
14 changed files with 274 additions and 186 deletions

View File

@ -19,8 +19,8 @@
"type": "string"
},
"categories": {
"via": "addresses",
"collection": "category",
"via": "addresses",
"dominant": true
},
"cover": {

View File

@ -16,8 +16,8 @@
"type": "string"
},
"addresses": {
"collection": "address",
"via": "categories"
"via": "categories",
"collection": "address"
}
}
}

View File

@ -1,8 +1,9 @@
{
"collectionName": "components_blog_test_comos",
"info": {
"name": "test como",
"icon": "ad"
"name": "test comp",
"icon": "ad",
"description": ""
},
"options": {},
"attributes": {

View File

@ -97,6 +97,7 @@ Object.keys(plugins).forEach(current => {
}, {});
// Inject plugins reducers
console.log(plugin.reducers);
const pluginReducers = plugin.reducers || {};
Object.keys(pluginReducers).forEach(reducerName => {

View File

@ -10,6 +10,7 @@ import checkStore from './checkStore';
export function injectReducerFactory(store, isValid) {
return function injectReducer(key, reducer) {
// console.log(key, reducer);
if (!isValid) checkStore(store);
invariant(

View File

@ -109,18 +109,18 @@ const formatLayoutWithMetas = (contentTypeConfiguration, ctUid, models) => {
metadatas: get(contentTypeConfiguration, ['metadatas', attribute.name, 'edit'], {}),
};
if (fieldSchema.type === 'relation') {
const queryInfos = ctUid
? generateRelationQueryInfosForComponents(
contentTypeConfiguration,
attribute.name,
ctUid,
models
)
: generateRelationQueryInfos(contentTypeConfiguration, attribute.name, models);
// if (fieldSchema.type === 'relation') {
// const queryInfos = ctUid
// ? generateRelationQueryInfosForComponents(
// contentTypeConfiguration,
// attribute.name,
// ctUid,
// models
// )
// : generateRelationQueryInfos(contentTypeConfiguration, attribute.name, models);
set(data, 'queryInfos', queryInfos);
}
// set(data, 'queryInfos', queryInfos);
// }
return data;
});

View File

@ -0,0 +1,11 @@
export const ADD_COMPONENTS_TO_DYNAMIC_ZONE = 'ContentTypeBuilder/FormModal/ADD_COMPONENTS_TO_DYNAMIC_ZONE';
export const ON_CHANGE = 'ContentTypeBuilder/FormModal/ON_CHANGE';
export const ON_CHANGE_ALLOWED_TYPE = 'ContentTypeBuilder/FormModal/ON_CHANGE_ALLOWED_TYPE';
export const RESET_PROPS = 'ContentTypeBuilder/FormModal/RESET_PROPS';
export const RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO = 'ContentTypeBuilder/FormModal/RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO';
export const RESET_PROPS_AND_SAVE_CURRENT_DATA = 'ContentTypeBuilder/FormModal/RESET_PROPS_AND_SAVE_CURRENT_DATA';
export const RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ = 'ContentTypeBuilder/FormModal/RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ';
export const SET_DATA_TO_EDIT = 'ContentTypeBuilder/FormModal/SET_DATA_TO_EDIT';
export const SET_ATTRIBUTE_DATA_SCHEMA = 'ContentTypeBuilder/FormModal/SET_ATTRIBUTE_DATA_SCHEMA';
export const SET_DYNAMIC_ZONE_DATA_SCHEMA = 'ContentTypeBuilder/FormModal/SET_DYNAMIC_ZONE_DATA_SCHEMA';
export const SET_ERRORS = 'ContentTypeBuilder/FormModal/SET_ERRORS';

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import {
HeaderModal,
HeaderModalTitle,
@ -17,6 +17,7 @@ import { Inputs } from '@buffetjs/custom';
import { useHistory, useLocation } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { get, has, isEmpty, set, toLower, toString, upperFirst } from 'lodash';
import { useSelector, useDispatch } from 'react-redux';
import pluginId from '../../pluginId';
import useDataManager from '../../hooks/useDataManager';
import AttributeOption from '../../components/AttributeOption';
@ -41,9 +42,21 @@ import {
import forms from './forms';
import { createComponentUid, createUid } from './utils/createUid';
import { NAVLINKS, INITIAL_STATE_DATA } from './utils/staticData';
import init from './init';
import reducer, { initialState } from './reducer';
import CustomButton from './CustomButton';
import makeSelectFormModal from './selectors';
import {
SET_DATA_TO_EDIT,
SET_DYNAMIC_ZONE_DATA_SCHEMA,
SET_ATTRIBUTE_DATA_SCHEMA,
ADD_COMPONENTS_TO_DYNAMIC_ZONE,
ON_CHANGE_ALLOWED_TYPE,
SET_ERRORS,
ON_CHANGE,
RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ,
RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO,
RESET_PROPS_AND_SAVE_CURRENT_DATA,
RESET_PROPS,
} from './constants';
/* eslint-disable indent */
/* eslint-disable react/no-array-index-key */
@ -51,7 +64,9 @@ import CustomButton from './CustomButton';
const FormModal = () => {
const [state, setState] = useState(INITIAL_STATE_DATA);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const formModalSelector = useMemo(makeSelectFormModal, []);
const dispatch = useDispatch();
const reducerState = useSelector(state => formModalSelector(state), []);
const { push } = useHistory();
const { search } = useLocation();
const { emitEvent, formatMessage } = useGlobalContext();
@ -83,7 +98,7 @@ const FormModal = () => {
initialData,
isCreatingComponentWhileAddingAField,
modifiedData,
} = reducerState.toJS();
} = reducerState;
useEffect(() => {
if (!isEmpty(search)) {
@ -206,7 +221,7 @@ const FormModal = () => {
// Edit category
if (modalType === 'editCategory' && actionType === 'edit') {
dispatch({
type: 'SET_DATA_TO_EDIT',
type: SET_DATA_TO_EDIT,
data: {
name: query.get('categoryName'),
},
@ -220,7 +235,7 @@ const FormModal = () => {
actionType === 'create'
) {
dispatch({
type: 'SET_DATA_TO_EDIT',
type: SET_DATA_TO_EDIT,
data: {
draftAndPublish: true,
},
@ -243,7 +258,7 @@ const FormModal = () => {
);
dispatch({
type: 'SET_DATA_TO_EDIT',
type: SET_DATA_TO_EDIT,
data: {
name,
collectionName,
@ -258,7 +273,7 @@ const FormModal = () => {
const data = get(allDataSchema, pathToSchema, {});
dispatch({
type: 'SET_DATA_TO_EDIT',
type: SET_DATA_TO_EDIT,
data: {
name: data.schema.name,
category: data.category,
@ -290,7 +305,7 @@ const FormModal = () => {
};
dispatch({
type: 'SET_DYNAMIC_ZONE_DATA_SCHEMA',
type: SET_DYNAMIC_ZONE_DATA_SCHEMA,
attributeToEdit,
});
}
@ -325,7 +340,7 @@ const FormModal = () => {
}
dispatch({
type: 'SET_ATTRIBUTE_DATA_SCHEMA',
type: SET_ATTRIBUTE_DATA_SCHEMA,
attributeType,
nameToSetForRelation: get(collectionTypesForRelation, ['0', 'title'], 'error'),
targetUid: get(collectionTypesForRelation, ['0', 'uid'], 'error'),
@ -520,7 +535,7 @@ const FormModal = () => {
target: { name, components, shouldAddComponents },
}) => {
dispatch({
type: 'ADD_COMPONENTS_TO_DYNAMIC_ZONE',
type: ADD_COMPONENTS_TO_DYNAMIC_ZONE,
name,
components,
shouldAddComponents,
@ -529,7 +544,7 @@ const FormModal = () => {
const handleChangeMediaAllowedTypes = ({ target: { name, value } }) => {
dispatch({
type: 'ON_CHANGE_ALLOWED_TYPE',
type: ON_CHANGE_ALLOWED_TYPE,
name,
value,
});
@ -604,29 +619,29 @@ const FormModal = () => {
delete clonedErrors[name];
dispatch({
type: 'SET_ERRORS',
type: SET_ERRORS,
errors: clonedErrors,
});
dispatch({
type: 'ON_CHANGE',
type: ON_CHANGE,
keys: name.split('.'),
value: val,
...rest,
});
},
[formErrors, state.actionType, toggleConfirmModal]
[dispatch, formErrors, state.actionType, toggleConfirmModal]
);
const handleConfirmDisableDraftAndPublish = useCallback(() => {
dispatch({
type: 'ON_CHANGE',
type: ON_CHANGE,
keys: ['draftAndPublish'],
value: false,
});
toggleConfirmModal();
}, [toggleConfirmModal]);
}, [dispatch, toggleConfirmModal]);
const handleSubmit = async (e, shouldContinue = isCreating) => {
e.preventDefault();
@ -771,7 +786,7 @@ const FormModal = () => {
if (isDynamicZoneAttribute) {
// Step 1 of adding a component to a DZ, the user has the option to create a component
dispatch({
type: 'RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ',
type: RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ,
});
push({ search: isCreating ? nextSearch : '' });
@ -810,7 +825,7 @@ const FormModal = () => {
// The first step is either needed to create a component or just to navigate
// To the modal for adding a "common field"
dispatch({
type: 'RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO',
type: RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO,
});
// We don't want all the props to be reset
@ -878,7 +893,7 @@ const FormModal = () => {
// Here we clear the reducer state but we also keep the created component
// If we were to create the component before
dispatch({
type: 'RESET_PROPS_AND_SAVE_CURRENT_DATA',
type: RESET_PROPS_AND_SAVE_CURRENT_DATA,
});
// Terminate because we don't want the reducer to be entirely reset
@ -909,7 +924,7 @@ const FormModal = () => {
// Add the field to the schema
addAttribute(modifiedData, state.forTarget, state.targetUid, false);
dispatch({ type: 'RESET_PROPS' });
dispatch({ type: RESET_PROPS });
// Open modal attribute for adding attr to component
@ -996,13 +1011,13 @@ const FormModal = () => {
}
dispatch({
type: 'RESET_PROPS',
type: RESET_PROPS,
});
} catch (err) {
const errors = getYupInnerErrors(err);
dispatch({
type: 'SET_ERRORS',
type: SET_ERRORS,
errors,
});
}
@ -1014,7 +1029,7 @@ const FormModal = () => {
const onClosed = () => {
setState(INITIAL_STATE_DATA);
dispatch({
type: 'RESET_PROPS',
type: RESET_PROPS,
});
};
const onOpened = () => {

View File

@ -1,5 +0,0 @@
function init(initialState) {
return initialState;
}
export default init;

View File

@ -4,148 +4,169 @@ import { snakeCase } from 'lodash';
import makeUnique from '../../utils/makeUnique';
import { createComponentUid } from './utils/createUid';
import { shouldPluralizeName, shouldPluralizeTargetAttribute } from './utils/relations';
import {
ADD_COMPONENTS_TO_DYNAMIC_ZONE,
ON_CHANGE,
ON_CHANGE_ALLOWED_TYPE,
RESET_PROPS,
RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO,
RESET_PROPS_AND_SAVE_CURRENT_DATA,
RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ,
SET_DATA_TO_EDIT,
SET_ATTRIBUTE_DATA_SCHEMA,
SET_DYNAMIC_ZONE_DATA_SCHEMA,
SET_ERRORS,
} from './constants';
const initialState = fromJS({
const initialState = {
formErrors: {},
modifiedData: {},
initialData: {},
componentToCreate: {},
isCreatingComponentWhileAddingAField: false,
});
};
const formModalReducer = (jsonState = initialState, action) => {
const state = fromJS(jsonState);
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_COMPONENTS_TO_DYNAMIC_ZONE': {
case ADD_COMPONENTS_TO_DYNAMIC_ZONE: {
const { name, components, shouldAddComponents } = action;
return state.updateIn(['modifiedData', name], list => {
let updatedList = list;
return state
.updateIn(['modifiedData', name], list => {
let updatedList = list;
if (shouldAddComponents) {
updatedList = list.concat(components);
} else {
updatedList = list.filter(comp => {
return components.indexOf(comp) === -1;
});
}
return List(makeUnique(updatedList.toJS()));
});
}
case 'ON_CHANGE':
return state.update('modifiedData', obj => {
const {
selectedContentTypeFriendlyName,
keys,
value,
oneThatIsCreatingARelationWithAnother,
} = action;
const hasDefaultValue = Boolean(obj.getIn(['default']));
// There is no need to remove the default key if the default value isn't defined
if (hasDefaultValue && keys.length === 1 && keys.includes('type')) {
const previousType = obj.getIn(['type']);
if (previousType && ['date', 'datetime', 'time'].includes(previousType)) {
return obj.updateIn(keys, () => value).remove('default');
}
}
if (keys.length === 1 && keys.includes('nature')) {
return obj
.update('nature', () => value)
.update('dominant', () => {
if (value === 'manyToMany') {
return true;
}
return null;
})
.update('name', oldValue => {
return pluralize(snakeCase(oldValue), shouldPluralizeName(value));
})
.update('targetAttribute', oldValue => {
if (['oneWay', 'manyWay'].includes(value)) {
return '-';
}
return pluralize(
oldValue === '-' ? snakeCase(oneThatIsCreatingARelationWithAnother) : oldValue,
shouldPluralizeTargetAttribute(value)
);
})
.update('targetColumnName', oldValue => {
if (['oneWay', 'manyWay'].includes(value)) {
return null;
}
return oldValue;
if (shouldAddComponents) {
updatedList = list.concat(components);
} else {
updatedList = list.filter(comp => {
return components.indexOf(comp) === -1;
});
}
}
if (keys.length === 1 && keys.includes('target')) {
const { targetContentTypeAllowedRelations } = action;
let didChangeNatureBecauseOfRestrictedRelation = false;
return List(makeUnique(updatedList.toJS()));
})
.toJS();
}
case ON_CHANGE:
return state
.update('modifiedData', obj => {
const {
selectedContentTypeFriendlyName,
keys,
value,
oneThatIsCreatingARelationWithAnother,
} = action;
const hasDefaultValue = Boolean(obj.getIn(['default']));
// There is no need to remove the default key if the default value isn't defined
if (hasDefaultValue && keys.length === 1 && keys.includes('type')) {
const previousType = obj.getIn(['type']);
if (previousType && ['date', 'datetime', 'time'].includes(previousType)) {
return obj.updateIn(keys, () => value).remove('default');
}
}
if (keys.length === 1 && keys.includes('nature')) {
return obj
.update('nature', () => value)
.update('dominant', () => {
if (value === 'manyToMany') {
return true;
}
return null;
})
.update('name', oldValue => {
return pluralize(snakeCase(oldValue), shouldPluralizeName(value));
})
.update('targetAttribute', oldValue => {
if (['oneWay', 'manyWay'].includes(value)) {
return '-';
}
return pluralize(
oldValue === '-' ? snakeCase(oneThatIsCreatingARelationWithAnother) : oldValue,
shouldPluralizeTargetAttribute(value)
);
})
.update('targetColumnName', oldValue => {
if (['oneWay', 'manyWay'].includes(value)) {
return null;
}
return oldValue;
});
}
if (keys.length === 1 && keys.includes('target')) {
const { targetContentTypeAllowedRelations } = action;
let didChangeNatureBecauseOfRestrictedRelation = false;
return obj
.update('target', () => value)
.update('nature', currentNature => {
if (targetContentTypeAllowedRelations === null) {
return currentNature;
}
if (!targetContentTypeAllowedRelations.includes(currentNature)) {
didChangeNatureBecauseOfRestrictedRelation = true;
return targetContentTypeAllowedRelations[0];
}
return obj
.update('target', () => value)
.update('nature', currentNature => {
if (targetContentTypeAllowedRelations === null) {
return currentNature;
}
})
.update('name', () => {
if (didChangeNatureBecauseOfRestrictedRelation) {
return pluralize(
snakeCase(selectedContentTypeFriendlyName),
shouldPluralizeName(targetContentTypeAllowedRelations[0])
);
}
if (!targetContentTypeAllowedRelations.includes(currentNature)) {
didChangeNatureBecauseOfRestrictedRelation = true;
return targetContentTypeAllowedRelations[0];
}
return currentNature;
})
.update('name', () => {
if (didChangeNatureBecauseOfRestrictedRelation) {
return pluralize(
snakeCase(selectedContentTypeFriendlyName),
shouldPluralizeName(targetContentTypeAllowedRelations[0])
shouldPluralizeName(obj.get('nature'))
);
}
})
.update('targetAttribute', () => {
if (['oneWay', 'manyWay'].includes(obj.get('nature'))) {
return '-';
}
return pluralize(
snakeCase(selectedContentTypeFriendlyName),
if (
didChangeNatureBecauseOfRestrictedRelation &&
['oneWay', 'manyWay'].includes(targetContentTypeAllowedRelations[0])
) {
return '-';
}
shouldPluralizeName(obj.get('nature'))
);
})
.update('targetAttribute', () => {
if (['oneWay', 'manyWay'].includes(obj.get('nature'))) {
return '-';
}
if (
didChangeNatureBecauseOfRestrictedRelation &&
['oneWay', 'manyWay'].includes(targetContentTypeAllowedRelations[0])
) {
return '-';
}
return pluralize(
snakeCase(oneThatIsCreatingARelationWithAnother),
shouldPluralizeTargetAttribute(obj.get('nature'))
);
});
}
return obj.updateIn(keys, () => value);
});
case 'ON_CHANGE_ALLOWED_TYPE': {
if (action.name === 'all') {
return state.updateIn(['modifiedData', 'allowedTypes'], () => {
if (action.value) {
return fromJS(['images', 'videos', 'files']);
return pluralize(
snakeCase(oneThatIsCreatingARelationWithAnother),
shouldPluralizeTargetAttribute(obj.get('nature'))
);
});
}
return null;
});
return obj.updateIn(keys, () => value);
})
.toJS();
case ON_CHANGE_ALLOWED_TYPE: {
if (action.name === 'all') {
return state
.updateIn(['modifiedData', 'allowedTypes'], () => {
if (action.value) {
return fromJS(['images', 'videos', 'files']);
}
return null;
})
.toJS();
}
return state.updateIn(['modifiedData', 'allowedTypes'], currentList => {
@ -164,15 +185,15 @@ const reducer = (state, action) => {
return list.push(action.name);
});
}
case 'RESET_PROPS':
case RESET_PROPS:
return initialState;
case 'RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO': {
case RESET_PROPS_AND_SET_FORM_FOR_ADDING_AN_EXISTING_COMPO: {
// This is run when the user doesn't want to create a new component
return initialState.update('modifiedData', () =>
fromJS({ type: 'component', repeatable: true })
);
return fromJS(initialState)
.update('modifiedData', () => fromJS({ type: 'component', repeatable: true }))
.toJS();
}
case 'RESET_PROPS_AND_SAVE_CURRENT_DATA': {
case RESET_PROPS_AND_SAVE_CURRENT_DATA: {
// This is run when the user has created a new component
const componentToCreate = state.getIn(['modifiedData', 'componentToCreate']);
const modifiedData = fromJS({
@ -185,27 +206,31 @@ const reducer = (state, action) => {
),
});
return initialState
return fromJS(initialState)
.update('componentToCreate', () => componentToCreate)
.update('modifiedData', () => modifiedData)
.update('isCreatingComponentWhileAddingAField', () =>
state.getIn(['modifiedData', 'createComponent'])
);
)
.toJS();
}
case 'RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ': {
case RESET_PROPS_AND_SET_THE_FORM_FOR_ADDING_A_COMPO_TO_A_DZ: {
const createdDZ = state.get('modifiedData');
const dataToSet = createdDZ
.set('createComponent', true)
.set('componentToCreate', fromJS({ type: 'component' }));
return initialState.update('modifiedData', () => dataToSet);
return fromJS(initialState)
.update('modifiedData', () => dataToSet)
.toJS();
}
case 'SET_DATA_TO_EDIT': {
case SET_DATA_TO_EDIT: {
return state
.updateIn(['modifiedData'], () => fromJS(action.data))
.updateIn(['initialData'], () => fromJS(action.data));
.updateIn(['initialData'], () => fromJS(action.data))
.toJS();
}
case 'SET_ATTRIBUTE_DATA_SCHEMA': {
case SET_ATTRIBUTE_DATA_SCHEMA: {
const {
attributeType,
isEditing,
@ -218,7 +243,8 @@ const reducer = (state, action) => {
if (isEditing) {
return state
.update('modifiedData', () => fromJS(modifiedDataToSetForEditing))
.update('initialData', () => fromJS(modifiedDataToSetForEditing));
.update('initialData', () => fromJS(modifiedDataToSetForEditing))
.toJS();
}
let dataToSet;
@ -268,20 +294,21 @@ const reducer = (state, action) => {
dataToSet = { type: attributeType, default: null };
}
return state.update('modifiedData', () => fromJS(dataToSet));
return state.update('modifiedData', () => fromJS(dataToSet)).toJS();
}
case 'SET_DYNAMIC_ZONE_DATA_SCHEMA': {
case SET_DYNAMIC_ZONE_DATA_SCHEMA: {
return state
.update('modifiedData', () => fromJS(action.attributeToEdit))
.update('initialData', () => fromJS(action.attributeToEdit));
.update('initialData', () => fromJS(action.attributeToEdit))
.toJS();
}
case 'SET_ERRORS':
return state.update('formErrors', () => fromJS(action.errors));
case SET_ERRORS:
return state.update('formErrors', () => fromJS(action.errors)).toJS();
default:
return state;
return state.toJS();
}
};
export default reducer;
export default formModalReducer;
export { initialState };

View File

@ -0,0 +1,24 @@
import { createSelector } from 'reselect';
import pluginId from '../../pluginId';
import { initialState } from './reducer';
/**
* Direct selector to the formModal state domain
*/
const formModalDomain = () => state => state.get(`${pluginId}_formModal`) || initialState;
/**
* Other specific selectors
*/
/**
* Default selector used by formModal
*/
const makeSelectFormModal = () =>
createSelector(formModalDomain(), substate => {
return substate;
});
export default makeSelectFormModal;
export { formModalDomain };

View File

@ -28,6 +28,8 @@ describe('CTB | containers | FormModal | reducer | actions', () => {
})
);
console.log(expected.toJS());
expect(reducer(state, action)).toEqual(expected);
});

View File

@ -15,11 +15,13 @@ import lifecycles from './lifecycles';
import trads from './translations';
import pluginPermissions from './permissions';
import pluginId from './pluginId';
import reducers from './reducers';
export default strapi => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const icon = pluginPkg.strapi.icon;
const name = pluginPkg.strapi.name;
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
@ -54,6 +56,7 @@ export default strapi => {
name,
pluginLogo,
preventComponentRendering: false,
reducers,
trads,
menu: {
pluginsSectionLinks: [

View File

@ -0,0 +1,8 @@
import formModalReducer from './containers/FormModal/reducer';
import pluginId from './pluginId';
const reducers = {
[`${pluginId}_formModal`]: formModalReducer,
};
export default reducers;