Migrate CollectionTypeFormWrapper and SingleTypeFormWrapper to redux

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2021-03-15 15:37:14 +01:00
parent 6f29989e3b
commit 48da7db14e
9 changed files with 275 additions and 98 deletions

View File

@ -1,7 +1,8 @@
import { memo, useCallback, useEffect, useMemo, useRef, useReducer } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import { get } from 'lodash';
import { request, useGlobalContext } from 'strapi-helper-plugin';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
createDefaultForm,
@ -11,18 +12,31 @@ import {
removeFieldsFromClonedData,
} from '../../utils';
import pluginId from '../../pluginId';
import { crudInitialState, crudReducer } from '../../sharedReducers';
import {
getData,
getDataSucceeded,
initForm,
resetProps,
setDataStructures,
setStatus,
submitSucceeded,
} from '../../sharedReducers/crudReducer/actions';
import selectCrudReducer from '../../sharedReducers/crudReducer/selectors';
import { getRequestUrl } from './utils';
// This container is used to handle the CRUD
const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, origin }) => {
const { emitEvent } = useGlobalContext();
const { push, replace } = useHistory();
const dispatch = useDispatch();
const {
componentsDataStructure,
contentTypeDataStructure,
data,
isLoading,
status,
} = useSelector(selectCrudReducer);
const [
{ componentsDataStructure, contentTypeDataStructure, data, isLoading, status },
dispatch,
] = useReducer(crudReducer, crudInitialState);
const emitEventRef = useRef(emitEvent);
const isCreatingEntry = id === 'create';
@ -87,31 +101,32 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
allLayoutData.components
);
dispatch({
type: 'SET_DATA_STRUCTURES',
componentsDataStructure,
contentTypeDataStructure: formatComponentData(
contentTypeDataStructure,
allLayoutData.contentType,
allLayoutData.components
),
});
}, [allLayoutData]);
const contentTypeDataStructureFormatted = formatComponentData(
contentTypeDataStructure,
allLayoutData.contentType,
allLayoutData.components
);
dispatch(setDataStructures(componentsDataStructure, contentTypeDataStructureFormatted));
}, [allLayoutData, dispatch]);
useEffect(() => {
return () => {
dispatch(resetProps());
};
}, [dispatch]);
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
const getData = async signal => {
dispatch({ type: 'GET_DATA' });
const fetchData = async signal => {
dispatch(getData());
try {
const data = await request(requestURL, { method: 'GET', signal });
dispatch({
type: 'GET_DATA_SUCCEEDED',
data: cleanReceivedData(cleanClonedData(data)),
});
dispatch(getDataSucceeded(cleanReceivedData(cleanClonedData(data))));
} catch (err) {
if (err.name === 'AbortError') {
return;
@ -137,15 +152,15 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
};
if (requestURL) {
getData(signal);
fetchData(signal);
} else {
dispatch({ type: 'INIT_FORM' });
dispatch(initForm());
}
return () => {
abortController.abort();
};
}, [requestURL, push, from, cleanReceivedData, cleanClonedData]);
}, [requestURL, push, from, cleanReceivedData, cleanClonedData, dispatch]);
const displayErrors = useCallback(err => {
const errorPayload = err.response.payload;
@ -196,7 +211,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
try {
// Show a loading button in the EditView/Header.js && lock the app => no navigation
dispatch({ type: 'SET_STATUS', status: 'submit-pending' });
dispatch(setStatus('submit-pending'));
const response = await request(endPoint, { method: 'POST', body });
@ -206,18 +221,18 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
message: { id: getTrad('success.record.save') },
});
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(response) });
dispatch(submitSucceeded(cleanReceivedData(response)));
// Enable navigation and remove loaders
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
replace(`/plugins/${pluginId}/collectionType/${slug}/${response.id}`);
} catch (err) {
emitEventRef.current('didNotCreateEntry', { error: err, trackerProperty });
displayErrors(err);
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
}
},
[cleanReceivedData, displayErrors, replace, slug]
[cleanReceivedData, displayErrors, replace, slug, dispatch]
);
const onPublish = useCallback(async () => {
@ -225,14 +240,14 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
emitEventRef.current('willPublishEntry');
const endPoint = getRequestUrl(`${slug}/${id}/actions/publish`);
dispatch({ type: 'SET_STATUS', status: 'publish-pending' });
dispatch(setStatus('publish-pending'));
const data = await request(endPoint, { method: 'POST' });
emitEventRef.current('didPublishEntry');
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(data) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(submitSucceeded(cleanReceivedData(data)));
dispatch(setStatus('resolved'));
strapi.notification.toggle({
type: 'success',
@ -240,9 +255,9 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
});
} catch (err) {
displayErrors(err);
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
}
}, [cleanReceivedData, displayErrors, id, slug]);
}, [cleanReceivedData, displayErrors, id, slug, dispatch]);
const onPut = useCallback(
async (body, trackerProperty) => {
@ -251,7 +266,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
try {
emitEventRef.current('willEditEntry', trackerProperty);
dispatch({ type: 'SET_STATUS', status: 'submit-pending' });
dispatch(setStatus('submit-pending'));
const response = await request(endPoint, { method: 'PUT', body });
@ -261,21 +276,23 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
message: { id: getTrad('success.record.save') },
});
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(response) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(submitSucceeded(cleanReceivedData(response)));
dispatch(setStatus('resolved'));
} catch (err) {
emitEventRef.current('didNotEditEntry', { error: err, trackerProperty });
displayErrors(err);
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
}
},
[cleanReceivedData, displayErrors, slug, id]
[cleanReceivedData, displayErrors, slug, id, dispatch]
);
const onUnpublish = useCallback(async () => {
const endPoint = getRequestUrl(`${slug}/${id}/actions/unpublish`);
dispatch({ type: 'SET_STATUS', status: 'unpublish-pending' });
dispatch(setStatus('unpublish-pending'));
try {
emitEventRef.current('willUnpublishEntry');
@ -285,13 +302,13 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, from, slug, id, or
emitEventRef.current('didUnpublishEntry');
strapi.notification.success(getTrad('success.record.unpublish'));
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(response) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(submitSucceeded(cleanReceivedData(response)));
dispatch(setStatus('resolved'));
} catch (err) {
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
displayErrors(err);
}
}, [cleanReceivedData, displayErrors, id, slug]);
}, [cleanReceivedData, displayErrors, id, slug, dispatch]);
return children({
componentsDataStructure,

View File

@ -1,7 +1,8 @@
import { memo, useCallback, useEffect, useRef, useReducer, useState } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { get } from 'lodash';
import { request, useGlobalContext } from 'strapi-helper-plugin';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
createDefaultForm,
@ -9,7 +10,16 @@ import {
getTrad,
removePasswordFieldsFromData,
} from '../../utils';
import { crudInitialState, crudReducer } from '../../sharedReducers';
import {
getData,
getDataSucceeded,
initForm,
resetProps,
setDataStructures,
setStatus,
submitSucceeded,
} from '../../sharedReducers/crudReducer/actions';
import selectCrudReducer from '../../sharedReducers/crudReducer/selectors';
import { getRequestUrl } from './utils';
// This container is used to handle the CRUD
@ -19,10 +29,14 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
const emitEventRef = useRef(emitEvent);
const [isCreatingEntry, setIsCreatingEntry] = useState(true);
const [
{ componentsDataStructure, contentTypeDataStructure, data, isLoading, status },
dispatch,
] = useReducer(crudReducer, crudInitialState);
const dispatch = useDispatch();
const {
componentsDataStructure,
contentTypeDataStructure,
data,
isLoading,
status,
} = useSelector(selectCrudReducer);
const cleanReceivedData = useCallback(
data => {
@ -38,6 +52,12 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
[allLayoutData]
);
useEffect(() => {
return () => {
dispatch(resetProps());
};
}, [dispatch]);
useEffect(() => {
const componentsDataStructure = Object.keys(allLayoutData.components).reduce((acc, current) => {
const defaultComponentForm = createDefaultForm(
@ -58,17 +78,14 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
allLayoutData.contentType.attributes,
allLayoutData.components
);
const contentTypeDataStructureFormatted = formatComponentData(
contentTypeDataStructure,
allLayoutData.contentType,
allLayoutData.components
);
dispatch({
type: 'SET_DATA_STRUCTURES',
componentsDataStructure,
contentTypeDataStructure: formatComponentData(
contentTypeDataStructure,
allLayoutData.contentType,
allLayoutData.components
),
});
}, [allLayoutData]);
dispatch(setDataStructures(componentsDataStructure, contentTypeDataStructureFormatted));
}, [allLayoutData, dispatch]);
// Check if creation mode or editing mode
useEffect(() => {
@ -76,17 +93,15 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
const { signal } = abortController;
const fetchData = async signal => {
dispatch({ type: 'GET_DATA' });
dispatch(getData());
setIsCreatingEntry(true);
try {
const data = await request(getRequestUrl(slug), { method: 'GET', signal });
dispatch({
type: 'GET_DATA_SUCCEEDED',
data: cleanReceivedData(data),
});
dispatch(getDataSucceeded(cleanReceivedData(data)));
setIsCreatingEntry(false);
} catch (err) {
if (err.name === 'AbortError') {
@ -97,7 +112,7 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
// Creating a single type
if (responseStatus === 404) {
dispatch({ type: 'INIT_FORM' });
dispatch(initForm());
}
if (responseStatus === 403) {
@ -111,7 +126,7 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
fetchData(signal);
return () => abortController.abort();
}, [cleanReceivedData, from, push, slug]);
}, [cleanReceivedData, from, push, slug, dispatch]);
const displayErrors = useCallback(err => {
const errorPayload = err.response.payload;
@ -155,15 +170,15 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
const onDeleteSucceeded = useCallback(() => {
setIsCreatingEntry(true);
dispatch({ type: 'INIT_FORM' });
}, []);
dispatch(initForm());
}, [dispatch]);
const onPost = useCallback(
async (body, trackerProperty) => {
const endPoint = getRequestUrl(slug);
try {
dispatch({ type: 'SET_STATUS', status: 'submit-pending' });
dispatch(setStatus('submit-pending'));
const response = await request(endPoint, { method: 'PUT', body });
@ -173,24 +188,26 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
message: { id: getTrad('success.record.save') },
});
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(response) });
dispatch(submitSucceeded(cleanReceivedData(response)));
setIsCreatingEntry(false);
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
} catch (err) {
emitEventRef.current('didNotCreateEntry', { error: err, trackerProperty });
displayErrors(err);
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
}
},
[cleanReceivedData, displayErrors, slug]
[cleanReceivedData, displayErrors, slug, dispatch]
);
const onPublish = useCallback(async () => {
try {
emitEventRef.current('willPublishEntry');
const endPoint = getRequestUrl(`${slug}/actions/publish`);
dispatch({ type: 'SET_STATUS', status: 'publish-pending' });
dispatch(setStatus('publish-pending'));
const data = await request(endPoint, { method: 'POST' });
@ -200,13 +217,15 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
message: { id: getTrad('success.record.publish') },
});
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(data) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(submitSucceeded(cleanReceivedData(data)));
dispatch(setStatus('resolved'));
} catch (err) {
displayErrors(err);
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
}
}, [cleanReceivedData, displayErrors, slug]);
}, [cleanReceivedData, displayErrors, slug, dispatch]);
const onPut = useCallback(
async (body, trackerProperty) => {
@ -215,7 +234,7 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
try {
emitEventRef.current('willEditEntry', trackerProperty);
dispatch({ type: 'SET_STATUS', status: 'submit-pending' });
dispatch(setStatus('submit-pending'));
const response = await request(endPoint, { method: 'PUT', body });
@ -226,22 +245,25 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
emitEventRef.current('didEditEntry', { trackerProperty });
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(response) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(submitSucceeded(cleanReceivedData(response)));
dispatch(setStatus('resolved'));
} catch (err) {
displayErrors(err);
emitEventRef.current('didNotEditEntry', { error: err, trackerProperty });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
}
},
[cleanReceivedData, displayErrors, slug]
[cleanReceivedData, displayErrors, slug, dispatch]
);
// The publish and unpublish method could be refactored but let's leave the duplication for now
const onUnpublish = useCallback(async () => {
const endPoint = getRequestUrl(`${slug}/actions/unpublish`);
dispatch({ type: 'SET_STATUS', status: 'unpublish-pending' });
dispatch(setStatus('unpublish-pending'));
try {
emitEventRef.current('willUnpublishEntry');
@ -251,13 +273,14 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, from, slug }) => {
emitEventRef.current('didUnpublishEntry');
strapi.notification.success(getTrad('success.record.unpublish'));
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedData(response) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(submitSucceeded(cleanReceivedData(response)));
dispatch(setStatus('resolved'));
} catch (err) {
dispatch({ type: 'SET_STATUS', status: 'resolved' });
dispatch(setStatus('resolved'));
displayErrors(err);
}
}, [cleanReceivedData, displayErrors, slug]);
}, [cleanReceivedData, displayErrors, slug, dispatch]);
return children({
componentsDataStructure,

View File

@ -2,6 +2,7 @@ import mainReducer from './containers/Main/reducer';
import editViewLayoutManagerReducer from './containers/EditViewLayoutManager/reducer';
import listViewReducer from './containers/ListView/reducer';
import rbacManagerReducer from './containers/RBACManager/reducer';
import editViewCrudReducer from './sharedReducers/crudReducer/reducer';
import pluginId from './pluginId';
const reducers = {
@ -9,6 +10,7 @@ const reducers = {
[`${pluginId}_listView`]: listViewReducer,
[`${pluginId}_rbacManager`]: rbacManagerReducer,
[`${pluginId}_editViewLayoutManager`]: editViewLayoutManagerReducer,
[`${pluginId}_editViewCrudReducer`]: editViewCrudReducer,
};
export default reducers;

View File

@ -0,0 +1,42 @@
import {
GET_DATA,
GET_DATA_SUCCEEDED,
INIT_FORM,
RESET_PROPS,
SET_DATA_STRUCTURES,
SET_STATUS,
SUBMIT_SUCCEEDED,
} from './constants';
export const getData = () => {
return {
type: GET_DATA,
};
};
export const getDataSucceeded = data => ({
type: GET_DATA_SUCCEEDED,
data,
});
export const initForm = () => ({
type: INIT_FORM,
});
export const resetProps = () => ({ type: RESET_PROPS });
export const setDataStructures = (componentsDataStructure, contentTypeDataStructure) => ({
type: SET_DATA_STRUCTURES,
componentsDataStructure,
contentTypeDataStructure,
});
export const setStatus = status => ({
type: SET_STATUS,
status,
});
export const submitSucceeded = data => ({
type: SUBMIT_SUCCEEDED,
data,
});

View File

@ -0,0 +1,7 @@
export const GET_DATA = 'ContentManager/CrudReducer/GET_DATA';
export const GET_DATA_SUCCEEDED = 'ContentManager/CrudReducer/GET_DATA_SUCCEEDED';
export const INIT_FORM = 'ContentManager/CrudReducer/INIT_FORM';
export const RESET_PROPS = 'ContentManager/CrudReducer/RESET_PROPS';
export const SET_DATA_STRUCTURES = 'ContentManager/CrudReducer/SET_DATA_STRUCTURES';
export const SET_STATUS = 'ContentManager/CrudReducer/SET_STATUS';
export const SUBMIT_SUCCEEDED = 'ContentManager/CrudReducer/SUBMIT_SUCCEEDED';

View File

@ -7,6 +7,16 @@ import produce from 'immer';
// require us to add the dispatch to the array wich is not wanted. This refacto does not require us to
// to do any of this.
import {
GET_DATA,
GET_DATA_SUCCEEDED,
INIT_FORM,
RESET_PROPS,
SET_DATA_STRUCTURES,
SET_STATUS,
SUBMIT_SUCCEEDED,
} from './constants';
const crudInitialState = {
componentsDataStructure: {},
contentTypeDataStructure: {},
@ -15,34 +25,37 @@ const crudInitialState = {
status: 'resolved',
};
const crudReducer = (state, action) =>
const crudReducer = (state = crudInitialState, action) =>
produce(state, draftState => {
switch (action.type) {
case 'GET_DATA': {
case GET_DATA: {
draftState.isLoading = true;
draftState.data = {};
break;
}
case 'GET_DATA_SUCCEEDED': {
case GET_DATA_SUCCEEDED: {
draftState.isLoading = false;
draftState.data = action.data;
break;
}
case 'INIT_FORM': {
case INIT_FORM: {
draftState.isLoading = false;
draftState.data = state.contentTypeDataStructure;
break;
}
case 'SET_DATA_STRUCTURES': {
case RESET_PROPS: {
return crudInitialState;
}
case SET_DATA_STRUCTURES: {
draftState.componentsDataStructure = action.componentsDataStructure;
draftState.contentTypeDataStructure = action.contentTypeDataStructure;
break;
}
case 'SET_STATUS': {
case SET_STATUS: {
draftState.status = action.status;
break;
}
case 'SUBMIT_SUCCEEDED': {
case SUBMIT_SUCCEEDED: {
draftState.data = action.data;
break;
}

View File

@ -0,0 +1,68 @@
/* eslint-disable consistent-return */
import produce from 'immer';
// NOTE: instead of creating a shared reducer here, we could also create a hook
// that returns the dispatch and the state, however it will mess with the linter
// and force us to either disable the linter for the hooks dependencies array rule or
// require us to add the dispatch to the array wich is not wanted. This refacto does not require us to
// to do any of this.
import {
GET_DATA,
GET_DATA_SUCCEEDED,
INIT_FORM,
RESET_PROPS,
SET_DATA_STRUCTURES,
SET_STATUS,
SUBMIT_SUCCEEDED,
} from './constants';
const crudInitialState = {
componentsDataStructure: {},
contentTypeDataStructure: {},
isLoading: true,
data: {},
status: 'resolved',
};
const crudReducer = (state = crudInitialState, action) =>
produce(state, draftState => {
switch (action.type) {
case GET_DATA: {
draftState.isLoading = true;
draftState.data = {};
break;
}
case GET_DATA_SUCCEEDED: {
draftState.isLoading = false;
draftState.data = action.data;
break;
}
case INIT_FORM: {
draftState.isLoading = false;
draftState.data = state.contentTypeDataStructure;
break;
}
case RESET_PROPS: {
return crudInitialState;
}
case SET_DATA_STRUCTURES: {
draftState.componentsDataStructure = action.componentsDataStructure;
draftState.contentTypeDataStructure = action.contentTypeDataStructure;
break;
}
case SET_STATUS: {
draftState.status = action.status;
break;
}
case SUBMIT_SUCCEEDED: {
draftState.data = action.data;
break;
}
default:
return draftState;
}
});
export default crudReducer;
export { crudInitialState };

View File

@ -0,0 +1,5 @@
import pluginId from '../../pluginId';
const selectCrudReducer = state => state.get(`${pluginId}_editViewCrudReducer`);
export default selectCrudReducer;

View File

@ -1,5 +1,5 @@
import produce from 'immer';
import crudReducer from '../crudReducer';
import crudReducer from '../reducer';
describe('CONTENT MANAGER | sharedReducers | crudReducer', () => {
let state;