Create single type wrapper

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-10-28 18:38:42 +01:00
parent 430da2da60
commit 948ddd5180
9 changed files with 342 additions and 13 deletions

View File

@ -11,6 +11,9 @@ function useSelect({ isUserAllowedToEditField, isUserAllowedToReadField, name, t
// slug,
updateActionAllowedFields,
} = useDataManager();
// TODO important! remove models dependency from the useEditView hook,
// This info should be handle in the layout?
const { models } = useEditView();
const displayNavigationLink = useMemo(() => {

View File

@ -8,6 +8,7 @@ import pluginId from '../../pluginId';
import { getRequestUrl } from './utils';
import reducer, { initialState } from './reducer';
// This container is used to handle the data fetching management part
const CollectionTypeWrapper = ({ allLayoutData, children, slug }) => {
const { emitEvent } = useGlobalContext();
const { push, replace } = useHistory();

View File

@ -1,7 +1,7 @@
import React, { memo, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import {
BackHeader,
LiLink,
@ -24,6 +24,7 @@ import { generatePermissionsObject, getInjectedComponents } from '../../utils';
import CollectionTypeWrapper from '../CollectionTypeWrapper';
import EditViewDataManagerProvider from '../EditViewDataManagerProvider';
import EditViewProvider from '../EditViewProvider';
import SingleTypeWrapper from '../SingleTypeWrapper';
import Header from './Header';
import { createAttributesLayout, getFieldsActionMatchingPermissions } from './utils';
import { LinkWrapper, SubWrapper } from './components';
@ -32,7 +33,8 @@ import InformationCard from './InformationCard';
/* eslint-disable react/no-array-index-key */
const EditView = ({ components, currentEnvironment, models, plugins, slug }) => {
// TODO check needed props
const EditView = ({ components, currentEnvironment, models, isSingleType, plugins, slug }) => {
const { isLoading, layout } = useFetchContentTypeLayout(slug);
const { goBack } = useHistory();
// Permissions
@ -41,7 +43,8 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
viewPermissions
);
const userPermissions = useUser();
// Legacy to remove for the configurations
const { pathname } = useLocation();
const {
createActionAllowedFields,
readActionAllowedFields,
@ -49,9 +52,23 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
} = useMemo(() => {
return getFieldsActionMatchingPermissions(userPermissions, slug);
}, [userPermissions, slug]);
const configurationPermissions = useMemo(() => {
return isSingleType
? pluginPermissions.singleTypesConfigurations
: pluginPermissions.collectionTypesConfigurations;
}, [isSingleType]);
// TODO check why the routing needs to be different... (not prio)
const configurationsURL = isSingleType
? `${pathname}/ctm-configurations/edit-settings/content-types`
: 'ctm-configurations/edit-settings/content-types';
const currentContentTypeLayoutData = useMemo(() => get(layout, ['contentType'], {}), [layout]);
const DataManagementWrapper = useMemo(
() => (isSingleType ? SingleTypeWrapper : CollectionTypeWrapper),
[isSingleType]
);
// Check if a block is a dynamic zone
const isDynamicZone = useCallback(block => {
return block.every(subBlock => {
@ -83,12 +100,13 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
allowedActions={allowedActions}
allLayoutData={layout}
components={components}
isSingleType={false}
isSingleType={isSingleType}
layout={currentContentTypeLayoutData}
// TODO: check if still needed
models={models}
>
<ContentTypeLayoutContext.Provider value={layout}>
<CollectionTypeWrapper allLayoutData={layout} slug={slug}>
<DataManagementWrapper allLayoutData={layout} slug={slug}>
{({
componentsDataStructure,
contentTypeDataStructure,
@ -111,7 +129,7 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
initialValues={data}
isCreatingEntry={isCreatingEntry}
isLoadingForData={isLoadingForData}
isSingleType={false}
isSingleType={isSingleType}
onPost={onPost}
onPublish={onPublish}
onPut={onPut}
@ -225,15 +243,13 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
)}
<LinkWrapper>
<ul>
<CheckPermissions
permissions={pluginPermissions.collectionTypesConfigurations}
>
<CheckPermissions permissions={configurationPermissions}>
<LiLink
message={{
id: 'app.links.configure-view',
}}
icon="layout"
url="ctm-configurations/edit-settings/content-types"
url={configurationsURL}
onClick={() => {
// emitEvent('willEditContentTypeLayoutFromEditView');
}}
@ -255,22 +271,25 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
</EditViewDataManagerProvider>
);
}}
</CollectionTypeWrapper>
</DataManagementWrapper>
</ContentTypeLayoutContext.Provider>
</EditViewProvider>
);
};
EditView.defaultProps = {
// TODO
currentEnvironment: 'production',
emitEvent: () => {},
plugins: {},
isSingleType: false,
};
EditView.propTypes = {
components: PropTypes.array.isRequired,
currentEnvironment: PropTypes.string,
emitEvent: PropTypes.func,
isSingleType: PropTypes.bool,
models: PropTypes.array.isRequired,
plugins: PropTypes.object,
slug: PropTypes.string.isRequired,

View File

@ -36,6 +36,7 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
const { goBack } = useHistory();
// DIFF WITH CT
const { pathname } = useLocation();
const viewPermissions = useMemo(() => generatePermissionsObject(slug), [slug]);
const { allowedActions, isLoading: isLoadingForPermissions } = useUserPermissions(
viewPermissions

View File

@ -3,7 +3,7 @@ import { Switch, Route, useRouteMatch, useParams } from 'react-router-dom';
import { LoadingIndicatorPage, CheckPagePermissions } from 'strapi-helper-plugin';
import pluginPermissions from '../../permissions';
const EditView = lazy(() => import('../SingleTypeEditView'));
const EditView = lazy(() => import('../EditView'));
const EditSettingsView = lazy(() => import('../EditSettingsView'));
const SingleTypeRecursivePath = props => {
@ -23,7 +23,7 @@ const SingleTypeRecursivePath = props => {
/>
<Route
path={`${url}`}
render={routeProps => <EditView {...props} {...routeProps} slug={slug} />}
render={routeProps => <EditView {...props} {...routeProps} slug={slug} isSingleType />}
/>
</Switch>
</Suspense>

View File

@ -0,0 +1,243 @@
import { memo, useCallback, useEffect, useRef, useReducer } from 'react';
import { useLocation, useHistory } from 'react-router-dom';
import { get } from 'lodash';
import { request, useGlobalContext } from 'strapi-helper-plugin';
import PropTypes from 'prop-types';
import { createDefaultForm, getTrad, removePasswordFieldsFromData } from '../../utils';
import { getRequestUrl } from './utils';
import reducer, { initialState } from './reducer';
// This container is used to handle the data fetching management part
const SingleTypeWrapper = ({ allLayoutData, children, slug }) => {
const { emitEvent } = useGlobalContext();
const { push } = useHistory();
const { state } = useLocation();
const emitEventRef = useRef(emitEvent);
// Here in case of a 403 response when fetching data we will either redirect to the previous page
// Or to the homepage if there's no state in the history stack
const from = get(state, 'from', '/');
const [
{ componentsDataStructure, contentTypeDataStructure, data, isCreatingEntry, isLoading, status },
dispatch,
] = useReducer(reducer, initialState);
const id = get(data, 'id', '');
const cleanReceivedDataFromPasswords = useCallback(
data => {
return removePasswordFieldsFromData(
data,
allLayoutData.contentType,
allLayoutData.components
);
},
[allLayoutData.components, allLayoutData.contentType]
);
// SET THE DEFAULT LAYOUT the effect is applied when the slug changes
useEffect(() => {
const componentsDataStructure = Object.keys(allLayoutData.components).reduce((acc, current) => {
acc[current] = createDefaultForm(
get(allLayoutData, ['components', current, 'schema', 'attributes'], {}),
allLayoutData.components
);
return acc;
}, {});
const contentTypeDataStructure = createDefaultForm(
allLayoutData.contentType.schema.attributes,
allLayoutData.components
);
dispatch({
type: 'SET_DATA_STRUCTURES',
componentsDataStructure,
contentTypeDataStructure,
});
}, [allLayoutData]);
// Check if creation mode or editing mode
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
const fetchData = async signal => {
dispatch({ type: 'GET_DATA' });
try {
const data = await request(getRequestUrl(slug), { method: 'GET', signal });
dispatch({
type: 'GET_DATA_SUCCEEDED',
data: cleanReceivedDataFromPasswords(data),
});
} catch (err) {
const responseStatus = get(err, 'response.status', null);
// Creating an st
if (responseStatus === 404) {
dispatch({ type: 'INIT_FORM' });
// setIsCreatingEntry(true);
}
if (responseStatus === 403) {
strapi.notification.info(getTrad('permissions.not-allowed.update'));
push(from);
}
}
};
fetchData(signal);
return () => abortController.abort();
}, [cleanReceivedDataFromPasswords, from, push, slug]);
const displayErrors = useCallback(err => {
const errorPayload = err.response.payload;
console.error(errorPayload);
let errorMessage = get(errorPayload, ['message'], 'Bad Request');
// TODO handle errors correctly when back-end ready
if (Array.isArray(errorMessage)) {
errorMessage = get(errorMessage, ['0', 'messages', '0', 'id']);
}
if (typeof errorMessage === 'string') {
strapi.notification.error(errorMessage);
}
}, []);
const onPost = useCallback(
async (formData, trackerProperty) => {
const endPoint = getRequestUrl(slug);
try {
dispatch({ type: 'SET_STATUS', status: 'submit-pending' });
const response = await request(
endPoint,
{ method: 'POST', headers: {}, body: formData },
false,
false
);
emitEventRef.current('didCreateEntry', trackerProperty);
strapi.notification.success(getTrad('success.record.save'));
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedDataFromPasswords(response) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
} catch (err) {
displayErrors(err);
emitEventRef.current('didNotCreateEntry', { error: err, trackerProperty });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
}
},
[cleanReceivedDataFromPasswords, displayErrors, slug]
);
const onPublish = useCallback(async () => {
try {
emitEventRef.current('willPublishEntry');
const endPoint = getRequestUrl(`${slug}/publish/${id}`);
dispatch({ type: 'SET_STATUS', status: 'publish-pending' });
const data = await request(endPoint, { method: 'POST' });
emitEventRef.current('didPublishEntry');
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedDataFromPasswords(data) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
strapi.notification.success(getTrad('success.record.publish'));
} catch (err) {
displayErrors(err);
dispatch({ type: 'SET_STATUS', status: 'resolved' });
}
}, [cleanReceivedDataFromPasswords, displayErrors, id, slug]);
const onPut = useCallback(
async (formData, trackerProperty) => {
const endPoint = getRequestUrl(`${slug}/${id}`);
try {
// Show a loading button in the EditView/Header.js && lock the app => no navigation
dispatch({ type: 'SET_STATUS', status: 'submit-pending' });
emitEventRef.current('willEditEntry', trackerProperty);
const response = await request(
endPoint,
{ method: 'PUT', headers: {}, body: formData },
false,
false
);
emitEventRef.current('didEditEntry', { trackerProperty });
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedDataFromPasswords(response) });
// Enable navigation and remove loaders
dispatch({ type: 'SET_STATUS', status: 'resolved' });
} catch (err) {
displayErrors(err);
emitEventRef.current('didNotEditEntry', { error: err, trackerProperty });
// Enable navigation and remove loaders
dispatch({ type: 'SET_STATUS', status: 'resolved' });
}
},
[cleanReceivedDataFromPasswords, id, displayErrors, slug]
);
// The publish and unpublish method could be refactored but let's leave the duplication for now
const onUnpublish = useCallback(async () => {
const endPoint = getRequestUrl(`${slug}/unpublish/${id}`);
dispatch({ type: 'SET_STATUS', status: 'unpublish-pending' });
try {
emitEventRef.current('willUnpublishEntry');
const response = await request(endPoint, { method: 'POST' });
emitEventRef.current('didUnpublishEntry');
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedDataFromPasswords(response) });
dispatch({ type: 'SET_STATUS', status: 'resolved' });
strapi.notification.success(getTrad('success.record.unpublish'));
} catch (err) {
dispatch({ type: 'SET_STATUS', status: 'resolved' });
displayErrors(err);
}
}, [cleanReceivedDataFromPasswords, displayErrors, id, slug]);
return children({
componentsDataStructure,
contentTypeDataStructure,
data,
isCreatingEntry,
isLoadingForData: isLoading,
onPost,
onPublish,
onPut,
onUnpublish,
status,
});
};
SingleTypeWrapper.propTypes = {
allLayoutData: PropTypes.shape({
components: PropTypes.object.isRequired,
contentType: PropTypes.object.isRequired,
}).isRequired,
children: PropTypes.func.isRequired,
slug: PropTypes.string.isRequired,
};
export default memo(SingleTypeWrapper);

View File

@ -0,0 +1,55 @@
/* eslint-disable consistent-return */
import produce from 'immer';
const initialState = {
componentsDataStructure: {},
contentTypeDataStructure: {},
isCreatingEntry: true,
isLoading: true,
data: {},
status: 'resolved',
};
const reducer = (state, action) =>
produce(state, draftState => {
switch (action.type) {
case 'GET_DATA': {
draftState.isCreatingEntry = true;
draftState.isLoading = true;
draftState.data = {};
break;
}
case 'GET_DATA_SUCCEEDED': {
draftState.isCreatingEntry = false;
draftState.isLoading = false;
draftState.data = action.data;
break;
}
case 'INIT_FORM': {
draftState.isCreatingEntry = true;
draftState.isLoading = false;
draftState.data = state.contentTypeDataStructure;
break;
}
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;
draftState.isCreatingEntry = false;
break;
}
default:
return draftState;
}
});
export default reducer;
export { initialState };

View File

@ -0,0 +1,5 @@
import pluginId from '../../../pluginId';
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
export default getRequestUrl;

View File

@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as getRequestUrl } from './getRequestUrl';