mirror of
https://github.com/strapi/strapi.git
synced 2025-12-29 08:04:51 +00:00
Create single type wrapper
Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
parent
430da2da60
commit
948ddd5180
@ -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(() => {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
@ -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 };
|
||||
@ -0,0 +1,5 @@
|
||||
import pluginId from '../../../pluginId';
|
||||
|
||||
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
|
||||
|
||||
export default getRequestUrl;
|
||||
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as getRequestUrl } from './getRequestUrl';
|
||||
Loading…
x
Reference in New Issue
Block a user