Split fetch data and data management

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-10-28 12:15:09 +01:00
parent b51603dca2
commit d30f965d29
18 changed files with 1092 additions and 314 deletions

View File

@ -0,0 +1,218 @@
import { memo, useCallback, useEffect, useMemo, useRef, useReducer } from 'react';
import { useParams, 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 pluginId from '../../pluginId';
import { getRequestUrl } from './utils';
import reducer, { initialState } from './reducer';
const CollectionTypeWrapper = ({ allLayoutData, children, slug }) => {
const { emitEvent } = useGlobalContext();
const { push, replace } = useHistory();
const { state } = useLocation();
const { id } = useParams();
const [
{ componentsDataStructure, contentTypeDataStructure, data, isLoading, status },
dispatch,
] = useReducer(reducer, initialState);
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 isCreatingEntry = id === 'create';
const fetchURL = useMemo(() => {
if (isCreatingEntry) {
return null;
}
return getRequestUrl(`${slug}/${id}`);
}, [slug, id, isCreatingEntry]);
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]);
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
const getData = async signal => {
dispatch({ type: 'GET_DATA' });
try {
const data = await request(fetchURL, { method: 'GET', signal });
dispatch({
type: 'GET_DATA_SUCCEEDED',
data: cleanReceivedDataFromPasswords(data),
});
} catch (err) {
console.error(err);
const resStatus = get(err, 'response.status', null);
if (resStatus === 404) {
push(from);
return;
}
// Not allowed to read a document
if (resStatus === 403) {
strapi.notification.info(getTrad('permissions.not-allowed.update'));
push(from);
}
}
};
if (fetchURL) {
getData(signal);
} else {
dispatch({ type: 'INIT_FORM' });
}
return () => {
abortController.abort();
};
}, [fetchURL, push, from, cleanReceivedDataFromPasswords]);
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 formData = createFormData(data);
const endPoint = getRequestUrl(slug);
try {
// Show a loading button in the EditView/Header.js && lock the app => no navigation
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: response });
dispatch({ type: 'SET_STATUS', status: 'submit-pending' });
replace(`/plugins/${pluginId}/collectionType/${slug}/${response.id}`);
} catch (err) {
displayErrors(err);
emitEventRef.current('didNotCreateEntry', { error: err, trackerProperty });
// Enable navigation and remove loaders
dispatch({ type: 'SET_STATUS', status: 'resolved' });
}
},
[displayErrors, replace, 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, displayErrors, slug, id]
);
return children({
componentsDataStructure,
contentTypeDataStructure,
data,
isCreatingEntry,
isLoadingForData: isLoading,
onPost,
onPut,
status,
});
};
CollectionTypeWrapper.propTypes = {
allLayoutData: PropTypes.shape({
components: PropTypes.object.isRequired,
contentType: PropTypes.object.isRequired,
}).isRequired,
// allowedActions: PropTypes.object.isRequired,
children: PropTypes.func.isRequired,
slug: PropTypes.string.isRequired,
};
export default memo(CollectionTypeWrapper);

View File

@ -0,0 +1,50 @@
/* eslint-disable consistent-return */
import produce from 'immer';
const initialState = {
componentsDataStructure: {},
contentTypeDataStructure: {},
isLoading: true,
data: {},
status: 'resolved',
};
const reducer = (state, 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 '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 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';

View File

@ -7,6 +7,7 @@ import {
LiLink,
LoadingIndicatorPage,
CheckPermissions,
useUser,
useUserPermissions,
} from 'strapi-helper-plugin';
import { Padded } from '@buffetjs/core';
@ -20,10 +21,11 @@ import SelectWrapper from '../../components/SelectWrapper';
import { ContentTypeLayoutContext } from '../../contexts';
import { useFetchContentTypeLayout } from '../../hooks';
import { generatePermissionsObject, getInjectedComponents } from '../../utils';
import CollectionTypeWrapper from '../CollectionTypeWrapper';
import EditViewDataManagerProvider from '../EditViewDataManagerProvider';
import EditViewProvider from '../EditViewProvider';
import Header from './Header';
import { createAttributesLayout } from './utils';
import { createAttributesLayout, getFieldsActionMatchingPermissions } from './utils';
import { LinkWrapper, SubWrapper } from './components';
import DeleteLink from './DeleteLink';
import InformationCard from './InformationCard';
@ -38,6 +40,15 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
const { allowedActions, isLoading: isLoadingForPermissions } = useUserPermissions(
viewPermissions
);
const userPermissions = useUser();
const {
createActionAllowedFields,
readActionAllowedFields,
updateActionAllowedFields,
} = useMemo(() => {
return getFieldsActionMatchingPermissions(userPermissions, slug);
}, [userPermissions, slug]);
const currentContentTypeLayoutData = useMemo(() => get(layout, ['contentType'], {}), [layout]);
@ -77,13 +88,36 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
models={models}
>
<ContentTypeLayoutContext.Provider value={layout}>
<CollectionTypeWrapper allLayoutData={layout} slug={slug}>
{({
componentsDataStructure,
contentTypeDataStructure,
data,
isCreatingEntry,
isLoadingForData,
onPost,
onPut,
status,
}) => {
return (
<EditViewDataManagerProvider
allowedActions={allowedActions}
//
allLayoutData={layout}
redirectToPreviousPage={goBack}
createActionAllowedFields={createActionAllowedFields}
componentsDataStructure={componentsDataStructure}
contentTypeDataStructure={contentTypeDataStructure}
initialValues={data}
isCreatingEntry={isCreatingEntry}
isLoadingForData={isLoadingForData}
isSingleType={false}
onPost={onPost}
onPut={onPut}
readActionAllowedFields={readActionAllowedFields}
// TODO check if needed
redirectToPreviousPage={goBack}
slug={slug}
status={status}
updateActionAllowedFields={updateActionAllowedFields}
>
<BackHeader onClick={goBack} />
<Container className="container-fluid">
@ -118,7 +152,12 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
const isComponent = fieldSchema.type === 'component';
if (isComponent) {
const { component, max, min, repeatable = false } = fieldSchema;
const {
component,
max,
min,
repeatable = false,
} = fieldSchema;
const componentUid = fieldSchema.component;
return (
@ -182,7 +221,9 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
)}
<LinkWrapper>
<ul>
<CheckPermissions permissions={pluginPermissions.collectionTypesConfigurations}>
<CheckPermissions
permissions={pluginPermissions.collectionTypesConfigurations}
>
<LiLink
message={{
id: 'app.links.configure-view',
@ -208,6 +249,9 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
</div>
</Container>
</EditViewDataManagerProvider>
);
}}
</CollectionTypeWrapper>
</ContentTypeLayoutContext.Provider>
</EditViewProvider>
);

View File

@ -0,0 +1,23 @@
import { uniq, flatMap } from 'lodash';
import { findMatchingPermissions } from 'strapi-helper-plugin';
const getFieldsActionMatchingPermissions = (userPermissions, slug) => {
const getMatchingPermissions = action => {
const matched = findMatchingPermissions(userPermissions, [
{
action: `plugins::content-manager.explorer.${action}`,
subject: slug,
},
]);
return uniq(flatMap(matched, 'fields'));
};
return {
createActionAllowedFields: getMatchingPermissions('create'),
readActionAllowedFields: getMatchingPermissions('read'),
updateActionAllowedFields: getMatchingPermissions('update'),
};
};
export default getFieldsActionMatchingPermissions;

View File

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

View File

@ -0,0 +1,18 @@
import getFieldsActionMatchingPermissions from '../getFieldsActionMatchingPermissions';
import { testData } from '../../../../testUtils';
const { permissions } = testData;
describe('CONTENT MANAGER | CONTAINERS | EditViewDataManager | utils | getFieldsActionMatchingPermissions', () => {
it('should return an object with all the allowed action for the fields', () => {
const expected = {
createActionAllowedFields: [],
readActionAllowedFields: ['name', 'description', 'test'],
updateActionAllowedFields: ['name', 'description'],
};
expect(getFieldsActionMatchingPermissions(permissions, 'application::article.article')).toEqual(
expected
);
});
});

View File

@ -0,0 +1,244 @@
const testData = {
contentType: {
apiID: 'test',
schema: {
attributes: {
created_at: { type: 'timestamp' },
dz: { type: 'dynamiczone', components: ['compos.test-compo', 'compos.sub-compo'] },
id: { type: 'integer' },
name: { type: 'string' },
notrepeatable: {
type: 'component',
repeatable: false,
component: 'compos.test-compo',
},
password: { type: 'password' },
repeatable: { type: 'component', repeatable: true, component: 'compos.test-compo' },
updated_at: { type: 'timestamp' },
},
},
},
components: {
'compos.sub-compo': {
uid: 'compos.sub-compo',
category: 'compos',
schema: {
attributes: {
id: { type: 'integer' },
name: { type: 'string' },
password: { type: 'password' },
},
},
},
'compos.test-compo': {
uid: 'compos.test-compo',
category: 'compos',
schema: {
attributes: {
id: { type: 'integer' },
name: { type: 'string' },
password: { type: 'password' },
subcomponotrepeatable: {
type: 'component',
repeatable: false,
component: 'compos.sub-compo',
},
subrepeatable: {
type: 'component',
repeatable: true,
component: 'compos.sub-compo',
},
},
},
},
},
modifiedData: {
created_at: '2020-04-28T13:22:13.033Z',
dz: [
{ __component: 'compos.sub-compo', id: 7, name: 'name', password: 'password' },
{
id: 4,
name: 'name',
password: 'password',
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
{
id: 5,
name: 'name',
password: 'password',
subcomponotrepeatable: { id: 9, name: 'name', password: 'password' },
subrepeatable: [{ id: 8, name: 'name', password: 'password' }],
__component: 'compos.test-compo',
},
{
id: 6,
name: null,
password: null,
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
],
id: 1,
name: 'name',
notrepeatable: {
id: 1,
name: 'name',
password: 'password',
subcomponotrepeatable: { id: 4, name: 'name', password: 'password' },
subrepeatable: [
{ id: 1, name: 'name', password: 'password' },
{ id: 2, name: 'name', password: 'password' },
{ id: 3, name: 'name', password: 'password' },
],
},
password: 'password',
repeatable: [
{
id: 2,
name: 'name',
password: 'password',
subrepeatable: [{ id: 5, name: 'name', password: 'password' }],
subcomponotrepeatable: { id: 6, name: 'name', password: 'password' },
},
{
id: 3,
name: 'name',
password: 'password',
subrepeatable: [],
subcomponotrepeatable: null,
},
],
updated_at: '2020-04-28T13:22:13.033Z',
},
expectedModifiedData: {
created_at: '2020-04-28T13:22:13.033Z',
dz: [
{ __component: 'compos.sub-compo', id: 7, name: 'name' },
{
id: 4,
name: 'name',
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
{
id: 5,
name: 'name',
subcomponotrepeatable: { id: 9, name: 'name' },
subrepeatable: [{ id: 8, name: 'name' }],
__component: 'compos.test-compo',
},
{
id: 6,
name: null,
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
],
id: 1,
name: 'name',
notrepeatable: {
id: 1,
name: 'name',
subcomponotrepeatable: { id: 4, name: 'name' },
subrepeatable: [
{ id: 1, name: 'name' },
{ id: 2, name: 'name' },
{ id: 3, name: 'name' },
],
},
repeatable: [
{
id: 2,
name: 'name',
subrepeatable: [{ id: 5, name: 'name' }],
subcomponotrepeatable: { id: 6, name: 'name' },
},
{
id: 3,
name: 'name',
subrepeatable: [],
subcomponotrepeatable: null,
},
],
updated_at: '2020-04-28T13:22:13.033Z',
},
};
const permissions = [
{
id: 11,
action: 'plugins::content-manager.explorer.read',
subject: 'application::article.article',
fields: ['name', 'description'],
conditions: ['admin::is-creator'],
},
{
id: 12,
action: 'plugins::content-manager.explorer.update',
subject: 'application::article.article',
fields: ['name', 'description'],
conditions: ['admin::is-creator'],
},
{
id: 22,
action: 'plugins::content-manager.explorer.read',
subject: 'plugins::users-permissions.user',
fields: [
'username',
'email',
'provider',
'password',
'resetPasswordToken',
'confirmed',
'blocked',
'role',
],
conditions: [],
},
{
id: 24,
action: 'plugins::content-manager.explorer.update',
subject: 'plugins::users-permissions.user',
fields: [
'username',
'email',
'provider',
'password',
'resetPasswordToken',
'confirmed',
'blocked',
'role',
],
conditions: [],
},
{
id: 28,
action: 'plugins::upload.read',
subject: null,
fields: null,
conditions: [],
},
{
id: 39,
action: 'plugins::users-permissions.roles.update',
subject: null,
fields: null,
conditions: [],
},
{
id: 63,
action: 'plugins::content-manager.explorer.read',
subject: 'application::article.article',
fields: ['name', 'description', 'test'],
conditions: [],
},
];
export default testData;
export { permissions };

View File

@ -1,28 +1,19 @@
import React, { useCallback, useEffect, useMemo, useRef, useReducer, useState } from 'react';
import { cloneDeep, get, isEmpty, isEqual, set } from 'lodash';
import PropTypes from 'prop-types';
import { Prompt, Redirect, useParams, useLocation, useHistory } from 'react-router-dom';
import { Prompt, Redirect, useLocation } from 'react-router-dom';
import {
LoadingIndicatorPage,
request,
useGlobalContext,
useUser,
OverlayBlocker,
} from 'strapi-helper-plugin';
import EditViewDataManagerContext from '../../contexts/EditViewDataManager';
import { getTrad } from '../../utils';
import { getTrad, removePasswordFieldsFromData } from '../../utils';
import pluginId from '../../pluginId';
import init from './init';
import reducer, { initialState } from './reducer';
import {
cleanData,
createDefaultForm,
createYupSchema,
getFieldsActionMatchingPermissions,
getFilesToUpload,
getYupInnerErrors,
removePasswordFieldsFromData,
} from './utils';
import { cleanData, createYupSchema, getFilesToUpload, getYupInnerErrors } from './utils';
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
@ -30,17 +21,26 @@ const EditViewDataManagerProvider = ({
allLayoutData,
allowedActions: { canCreate, canRead, canUpdate },
children,
componentsDataStructure,
contentTypeDataStructure,
createActionAllowedFields,
initialValues,
isCreatingEntry,
isLoadingForData,
isSingleType,
onPost,
onPut,
readActionAllowedFields,
// Not sure this is needed anymore
redirectToPreviousPage,
slug,
status,
updateActionAllowedFields,
}) => {
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const { id } = useParams();
const isCreatingEntry = id === 'create';
const { state } = useLocation();
const { push, replace } = useHistory();
// 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', '/');
@ -48,16 +48,13 @@ const EditViewDataManagerProvider = ({
const {
formErrors,
initialData,
isLoading,
modifiedData,
modifiedDZName,
shouldCheckErrors,
} = reducerState.toJS();
// This isCreatingEntry logic will be needed, but it needs to be passed from the parent
// TODO: this should be in the reducer
const [status, setStatus] = useState('resolved');
const [, setStatus] = useState('resolved');
const currentContentTypeLayout = get(allLayoutData, ['contentType'], {});
@ -71,15 +68,6 @@ const EditViewDataManagerProvider = ({
const { emitEvent, formatMessage } = useGlobalContext();
const emitEventRef = useRef(emitEvent);
const userPermissions = useUser();
const {
createActionAllowedFields,
readActionAllowedFields,
updateActionAllowedFields,
} = useMemo(() => {
return getFieldsActionMatchingPermissions(userPermissions, slug);
}, [userPermissions, slug]);
const cleanReceivedDataFromPasswords = useCallback(
data => {
@ -93,7 +81,7 @@ const EditViewDataManagerProvider = ({
);
const shouldRedirectToHomepageWhenCreatingEntry = useMemo(() => {
if (isLoading) {
if (isLoadingForData) {
return false;
}
@ -106,10 +94,10 @@ const EditViewDataManagerProvider = ({
}
return false;
}, [isCreatingEntry, canCreate, isLoading]);
}, [isCreatingEntry, canCreate, isLoadingForData]);
const shouldRedirectToHomepageWhenEditingEntry = useMemo(() => {
if (isLoading) {
if (isLoadingForData) {
return false;
}
@ -122,11 +110,11 @@ const EditViewDataManagerProvider = ({
}
return false;
}, [isLoading, isCreatingEntry, canRead, canUpdate]);
}, [isLoadingForData, isCreatingEntry, canRead, canUpdate]);
// TODO check this effect if it is really needed (not prio)
useEffect(() => {
if (!isLoading) {
if (!isLoadingForData) {
checkFormErrors();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -144,94 +132,20 @@ const EditViewDataManagerProvider = ({
}
}, [shouldRedirectToHomepageWhenCreatingEntry]);
// Reset all props when changing from one ct to another
useEffect(() => {
dispatch({ type: 'RESET_PROPS' });
}, [slug]);
// Reset all props when navigating from one entry to another in the same ct
useEffect(() => {
dispatch({ type: 'RESET_FORM' });
}, [id]);
// 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(
currentContentTypeLayout.schema.attributes,
allLayoutData.components
);
dispatch({
type: 'SET_DEFAULT_DATA_STRUCTURES',
componentsDataStructure,
contentTypeDataStructure,
});
}, [allLayoutData, currentContentTypeLayout.schema.attributes]);
}, [componentsDataStructure, contentTypeDataStructure]);
useEffect(() => {
if (isCreatingEntry) {
dispatch({ type: 'INITIALIZE_FORM' });
}
}, [isCreatingEntry]);
const fetchURL = useMemo(() => {
if (isCreatingEntry) {
return null;
}
return getRequestUrl(`${slug}/${id}`);
}, [slug, id, isCreatingEntry]);
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
const getData = async signal => {
dispatch({ type: 'GET_DATA' });
try {
const data = await request(fetchURL, { method: 'GET', signal });
dispatch({
type: 'GET_DATA_SUCCEEDED',
data: cleanReceivedDataFromPasswords(data),
type: 'INIT_FORM',
initialValues,
});
} catch (err) {
console.error(err);
const resStatus = get(err, 'response.status', null);
if (resStatus === 404) {
push(from);
return;
}
// Not allowed to read a document
if (resStatus === 403) {
strapi.notification.info(getTrad('permissions.not-allowed.update'));
push(from);
}
}
};
if (fetchURL) {
getData(signal);
}
return () => {
abortController.abort();
};
}, [fetchURL, push, from, cleanReceivedDataFromPasswords]);
}, [initialValues]);
const addComponentToDynamicZone = useCallback((keys, componentUid, shouldCheckErrors = false) => {
emitEventRef.current('didAddComponentToDynamicZone');
@ -367,7 +281,7 @@ const EditViewDataManagerProvider = ({
const createFormData = useCallback(
data => {
// Set the loading state in the plugin header
const filesToUpload = getFilesToUpload(modifiedData);
const filesToUpload = getFilesToUpload(data);
// Remove keys that are not needed
// Clean relations
const cleanedData = cleanData(data, currentContentTypeLayout, allLayoutData.components);
@ -389,7 +303,7 @@ const EditViewDataManagerProvider = ({
return formData;
},
[allLayoutData.components, currentContentTypeLayout, modifiedData]
[allLayoutData.components, currentContentTypeLayout]
);
const trackerProperty = useMemo(() => {
@ -416,74 +330,6 @@ const EditViewDataManagerProvider = ({
}
}, []);
const onPost = useCallback(
async data => {
const formData = createFormData(data);
const endPoint = getRequestUrl(slug);
try {
// Show a loading button in the EditView/Header.js && lock the app => no navigation
setStatus('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'));
// Enable navigation and remove loaders
setStatus('resolved');
dispatch({ type: 'SUBMIT_SUCCEEDED', data: response });
replace(`/plugins/${pluginId}/collectionType/${slug}/${response.id}`);
} catch (err) {
displayErrors(err);
emitEventRef.current('didNotCreateEntry', { error: err, trackerProperty });
// Enable navigation and remove loaders
setStatus('resolved');
}
},
[createFormData, displayErrors, replace, slug, trackerProperty]
);
const onPut = useCallback(
async data => {
const formData = createFormData(data);
const endPoint = getRequestUrl(`${slug}/${data.id}`);
try {
// Show a loading button in the EditView/Header.js && lock the app => no navigation
setStatus('submit-pending');
emitEventRef.current('willEditEntry', trackerProperty);
const response = await request(
endPoint,
{ method: 'PUT', headers: {}, body: formData },
false,
false
);
emitEventRef.current('didEditEntry', { trackerProperty });
// Enable navigation and remove loaders
setStatus('resolved');
dispatch({ type: 'SUBMIT_SUCCEEDED', data: cleanReceivedDataFromPasswords(response) });
} catch (err) {
displayErrors(err);
emitEventRef.current('didNotEditEntry', { error: err, trackerProperty });
// Enable navigation and remove loaders
setStatus('resolved');
}
},
[cleanReceivedDataFromPasswords, createFormData, displayErrors, slug, trackerProperty]
);
const handleSubmit = useCallback(
async e => {
e.preventDefault();
@ -492,11 +338,16 @@ const EditViewDataManagerProvider = ({
// First validate the form
try {
await yupSchema.validate(modifiedData, { abortEarly: false });
console.log({ modifiedData });
const formData = createFormData(modifiedData);
// console.log()
if (isCreatingEntry) {
onPost(modifiedData);
onPost(formData, trackerProperty);
} else {
onPut(modifiedData);
onPut(formData, trackerProperty);
}
} catch (err) {
console.error('ValidationError');
@ -510,7 +361,7 @@ const EditViewDataManagerProvider = ({
errors,
});
},
[isCreatingEntry, modifiedData, onPost, onPut, yupSchema]
[createFormData, isCreatingEntry, modifiedData, onPost, onPut, trackerProperty, yupSchema]
);
const handlePublish = useCallback(async () => {
@ -802,7 +653,7 @@ const EditViewDataManagerProvider = ({
isOpen={status !== 'resolved'}
{...overlayBlockerParams}
/>
{isLoading ? (
{isLoadingForData ? (
<LoadingIndicatorPage />
) : (
<>
@ -825,10 +676,21 @@ EditViewDataManagerProvider.defaultProps = {
EditViewDataManagerProvider.propTypes = {
allLayoutData: PropTypes.object.isRequired,
allowedActions: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
children: PropTypes.arrayOf(PropTypes.element).isRequired,
componentsDataStructure: PropTypes.object.isRequired,
contentTypeDataStructure: PropTypes.object.isRequired,
createActionAllowedFields: PropTypes.array.isRequired,
initialValues: PropTypes.object.isRequired,
isCreatingEntry: PropTypes.bool.isRequired,
isLoadingForData: PropTypes.bool.isRequired,
isSingleType: PropTypes.bool.isRequired,
onPost: PropTypes.func.isRequired,
onPut: PropTypes.func.isRequired,
readActionAllowedFields: PropTypes.array.isRequired,
redirectToPreviousPage: PropTypes.func,
slug: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
updateActionAllowedFields: PropTypes.array.isRequired,
};
export default EditViewDataManagerProvider;

View File

@ -84,6 +84,15 @@ const reducer = (state, action) => {
.update('initialData', () => fromJS(action.data))
.update('modifiedData', () => fromJS(action.data))
.update('isLoading', () => false);
case 'INIT_FORM': {
return state
.update('formErrors', () => fromJS({}))
.update('initialData', () => fromJS(action.initialValues))
.update('modifiedData', () => fromJS(action.initialValues))
.update('modifiedDZName', () => null)
.update('shouldCheckErrors', () => false);
}
case 'INITIALIZE_FORM': {
return state
.update('isLoading', () => false)

View File

@ -1,7 +1,7 @@
export { default as cleanData } from './cleanData';
export { default as createDefaultForm } from './createDefaultForm';
export { default as getFieldsActionMatchingPermissions } from './getFieldsActionMatchingPermissions';
// export { default as createDefaultForm } from './createDefaultForm';
// export { default as getFieldsActionMatchingPermissions } from './getFieldsActionMatchingPermissions';
export { default as getFilesToUpload } from './getFilesToUpload';
export { default as getYupInnerErrors } from './getYupInnerErrors';
export { default as createYupSchema } from './schema';
export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';
// export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';

View File

@ -0,0 +1,244 @@
const testData = {
contentType: {
apiID: 'test',
schema: {
attributes: {
created_at: { type: 'timestamp' },
dz: { type: 'dynamiczone', components: ['compos.test-compo', 'compos.sub-compo'] },
id: { type: 'integer' },
name: { type: 'string' },
notrepeatable: {
type: 'component',
repeatable: false,
component: 'compos.test-compo',
},
password: { type: 'password' },
repeatable: { type: 'component', repeatable: true, component: 'compos.test-compo' },
updated_at: { type: 'timestamp' },
},
},
},
components: {
'compos.sub-compo': {
uid: 'compos.sub-compo',
category: 'compos',
schema: {
attributes: {
id: { type: 'integer' },
name: { type: 'string' },
password: { type: 'password' },
},
},
},
'compos.test-compo': {
uid: 'compos.test-compo',
category: 'compos',
schema: {
attributes: {
id: { type: 'integer' },
name: { type: 'string' },
password: { type: 'password' },
subcomponotrepeatable: {
type: 'component',
repeatable: false,
component: 'compos.sub-compo',
},
subrepeatable: {
type: 'component',
repeatable: true,
component: 'compos.sub-compo',
},
},
},
},
},
modifiedData: {
created_at: '2020-04-28T13:22:13.033Z',
dz: [
{ __component: 'compos.sub-compo', id: 7, name: 'name', password: 'password' },
{
id: 4,
name: 'name',
password: 'password',
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
{
id: 5,
name: 'name',
password: 'password',
subcomponotrepeatable: { id: 9, name: 'name', password: 'password' },
subrepeatable: [{ id: 8, name: 'name', password: 'password' }],
__component: 'compos.test-compo',
},
{
id: 6,
name: null,
password: null,
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
],
id: 1,
name: 'name',
notrepeatable: {
id: 1,
name: 'name',
password: 'password',
subcomponotrepeatable: { id: 4, name: 'name', password: 'password' },
subrepeatable: [
{ id: 1, name: 'name', password: 'password' },
{ id: 2, name: 'name', password: 'password' },
{ id: 3, name: 'name', password: 'password' },
],
},
password: 'password',
repeatable: [
{
id: 2,
name: 'name',
password: 'password',
subrepeatable: [{ id: 5, name: 'name', password: 'password' }],
subcomponotrepeatable: { id: 6, name: 'name', password: 'password' },
},
{
id: 3,
name: 'name',
password: 'password',
subrepeatable: [],
subcomponotrepeatable: null,
},
],
updated_at: '2020-04-28T13:22:13.033Z',
},
expectedModifiedData: {
created_at: '2020-04-28T13:22:13.033Z',
dz: [
{ __component: 'compos.sub-compo', id: 7, name: 'name' },
{
id: 4,
name: 'name',
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
{
id: 5,
name: 'name',
subcomponotrepeatable: { id: 9, name: 'name' },
subrepeatable: [{ id: 8, name: 'name' }],
__component: 'compos.test-compo',
},
{
id: 6,
name: null,
subcomponotrepeatable: null,
subrepeatable: [],
__component: 'compos.test-compo',
},
],
id: 1,
name: 'name',
notrepeatable: {
id: 1,
name: 'name',
subcomponotrepeatable: { id: 4, name: 'name' },
subrepeatable: [
{ id: 1, name: 'name' },
{ id: 2, name: 'name' },
{ id: 3, name: 'name' },
],
},
repeatable: [
{
id: 2,
name: 'name',
subrepeatable: [{ id: 5, name: 'name' }],
subcomponotrepeatable: { id: 6, name: 'name' },
},
{
id: 3,
name: 'name',
subrepeatable: [],
subcomponotrepeatable: null,
},
],
updated_at: '2020-04-28T13:22:13.033Z',
},
};
const permissions = [
{
id: 11,
action: 'plugins::content-manager.explorer.read',
subject: 'application::article.article',
fields: ['name', 'description'],
conditions: ['admin::is-creator'],
},
{
id: 12,
action: 'plugins::content-manager.explorer.update',
subject: 'application::article.article',
fields: ['name', 'description'],
conditions: ['admin::is-creator'],
},
{
id: 22,
action: 'plugins::content-manager.explorer.read',
subject: 'plugins::users-permissions.user',
fields: [
'username',
'email',
'provider',
'password',
'resetPasswordToken',
'confirmed',
'blocked',
'role',
],
conditions: [],
},
{
id: 24,
action: 'plugins::content-manager.explorer.update',
subject: 'plugins::users-permissions.user',
fields: [
'username',
'email',
'provider',
'password',
'resetPasswordToken',
'confirmed',
'blocked',
'role',
],
conditions: [],
},
{
id: 28,
action: 'plugins::upload.read',
subject: null,
fields: null,
conditions: [],
},
{
id: 39,
action: 'plugins::users-permissions.roles.update',
subject: null,
fields: null,
conditions: [],
},
{
id: 63,
action: 'plugins::content-manager.explorer.read',
subject: 'application::article.article',
fields: ['name', 'description', 'test'],
conditions: [],
},
];
export default testData;
export { permissions };

View File

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

View File

@ -0,0 +1,54 @@
import { get } from 'lodash';
const createDefaultForm = (attributes, allComponentsSchema) => {
return Object.keys(attributes).reduce((acc, current) => {
const attribute = get(attributes, [current], {});
const { default: defaultValue, component, type, required, min, repeatable } = attribute;
if (type === 'json') {
acc[current] = null;
}
if (type === 'json' && required === true) {
acc[current] = {};
}
if (defaultValue !== undefined) {
acc[current] = defaultValue;
}
if (type === 'component') {
const currentComponentSchema = get(
allComponentsSchema,
[component, 'schema', 'attributes'],
{}
);
const currentComponentDefaultForm = createDefaultForm(
currentComponentSchema,
allComponentsSchema
);
if (required === true) {
acc[current] = repeatable === true ? [] : currentComponentDefaultForm;
}
if (min && repeatable === true && required) {
acc[current] = [];
for (let i = 0; i < min; i += 1) {
acc[current].push(currentComponentDefaultForm);
}
}
}
if (type === 'dynamiczone') {
if (required === true) {
acc[current] = [];
}
}
return acc;
}, {});
};
export default createDefaultForm;

View File

@ -1,4 +1,5 @@
export { default as checkIfAttributeIsDisplayable } from './checkIfAttributeIsDisplayable';
export { default as createDefaultForm } from './createDefaultForm';
export { default as dateFormats } from './dateFormats';
export { default as generatePermissionsObject } from './generatePermissionsObject';
export { default as getInjectedComponents } from './getComponents';
@ -6,3 +7,4 @@ export { default as getFieldName } from './getFieldName';
export { default as getRequestUrl } from './getRequestUrl';
export { default as getTrad } from './getTrad';
export { default as ItemTypes } from './ItemTypes';
export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';

View File

@ -1,7 +1,7 @@
import { testData } from '../../testUtils';
import removePasswordFieldsFromData from '../removePasswordFieldsFromData';
import testData from './testData';
describe('CONTENT MANAGER | containers | EditViewDataManager | utils', () => {
describe('CONTENT MANAGER | utils', () => {
describe('removePasswordFieldsFromData', () => {
it('should return an empty object', () => {
const { components, contentType } = testData;