mirror of
https://github.com/strapi/strapi.git
synced 2025-08-04 23:03:00 +00:00
Split fetch data and data management
Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
parent
b51603dca2
commit
d30f965d29
@ -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);
|
@ -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 };
|
@ -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';
|
@ -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,137 +88,170 @@ const EditView = ({ components, currentEnvironment, models, plugins, slug }) =>
|
||||
models={models}
|
||||
>
|
||||
<ContentTypeLayoutContext.Provider value={layout}>
|
||||
<EditViewDataManagerProvider
|
||||
allowedActions={allowedActions}
|
||||
//
|
||||
allLayoutData={layout}
|
||||
redirectToPreviousPage={goBack}
|
||||
isSingleType={false}
|
||||
slug={slug}
|
||||
>
|
||||
<BackHeader onClick={goBack} />
|
||||
<Container className="container-fluid">
|
||||
<Header />
|
||||
<div className="row" style={{ paddingTop: 3 }}>
|
||||
<div className="col-md-12 col-lg-9" style={{ marginBottom: 13 }}>
|
||||
{formattedContentTypeLayout.map((block, blockIndex) => {
|
||||
if (isDynamicZone(block)) {
|
||||
const {
|
||||
0: {
|
||||
0: { name, fieldSchema, metadatas },
|
||||
},
|
||||
} = block;
|
||||
<CollectionTypeWrapper allLayoutData={layout} slug={slug}>
|
||||
{({
|
||||
componentsDataStructure,
|
||||
contentTypeDataStructure,
|
||||
data,
|
||||
isCreatingEntry,
|
||||
isLoadingForData,
|
||||
onPost,
|
||||
onPut,
|
||||
status,
|
||||
}) => {
|
||||
return (
|
||||
<EditViewDataManagerProvider
|
||||
allowedActions={allowedActions}
|
||||
allLayoutData={layout}
|
||||
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">
|
||||
<Header />
|
||||
<div className="row" style={{ paddingTop: 3 }}>
|
||||
<div className="col-md-12 col-lg-9" style={{ marginBottom: 13 }}>
|
||||
{formattedContentTypeLayout.map((block, blockIndex) => {
|
||||
if (isDynamicZone(block)) {
|
||||
const {
|
||||
0: {
|
||||
0: { name, fieldSchema, metadatas },
|
||||
},
|
||||
} = block;
|
||||
|
||||
return (
|
||||
<DynamicZone
|
||||
key={blockIndex}
|
||||
name={name}
|
||||
fieldSchema={fieldSchema}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DynamicZone
|
||||
key={blockIndex}
|
||||
name={name}
|
||||
fieldSchema={fieldSchema}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormWrapper key={blockIndex}>
|
||||
{block.map((fieldsBlock, fieldsBlockIndex) => {
|
||||
return (
|
||||
<div className="row" key={fieldsBlockIndex}>
|
||||
{fieldsBlock.map(
|
||||
({ name, size, fieldSchema, metadatas }, fieldIndex) => {
|
||||
const isComponent = fieldSchema.type === 'component';
|
||||
<FormWrapper key={blockIndex}>
|
||||
{block.map((fieldsBlock, fieldsBlockIndex) => {
|
||||
return (
|
||||
<div className="row" key={fieldsBlockIndex}>
|
||||
{fieldsBlock.map(
|
||||
({ name, size, fieldSchema, metadatas }, fieldIndex) => {
|
||||
const isComponent = fieldSchema.type === 'component';
|
||||
|
||||
if (isComponent) {
|
||||
const { component, max, min, repeatable = false } = fieldSchema;
|
||||
const componentUid = fieldSchema.component;
|
||||
if (isComponent) {
|
||||
const {
|
||||
component,
|
||||
max,
|
||||
min,
|
||||
repeatable = false,
|
||||
} = fieldSchema;
|
||||
const componentUid = fieldSchema.component;
|
||||
|
||||
return (
|
||||
<FieldComponent
|
||||
key={componentUid}
|
||||
componentUid={component}
|
||||
isRepeatable={repeatable}
|
||||
label={metadatas.label}
|
||||
max={max}
|
||||
min={min}
|
||||
name={name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`col-${size}`} key={name}>
|
||||
<Inputs
|
||||
autoFocus={
|
||||
blockIndex === 0 &&
|
||||
fieldsBlockIndex === 0 &&
|
||||
fieldIndex === 0
|
||||
return (
|
||||
<FieldComponent
|
||||
key={componentUid}
|
||||
componentUid={component}
|
||||
isRepeatable={repeatable}
|
||||
label={metadatas.label}
|
||||
max={max}
|
||||
min={min}
|
||||
name={name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
fieldSchema={fieldSchema}
|
||||
keys={name}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className={`col-${size}`} key={name}>
|
||||
<Inputs
|
||||
autoFocus={
|
||||
blockIndex === 0 &&
|
||||
fieldsBlockIndex === 0 &&
|
||||
fieldIndex === 0
|
||||
}
|
||||
fieldSchema={fieldSchema}
|
||||
keys={name}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</FormWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-3">
|
||||
<InformationCard />
|
||||
<Padded size="smd" top />
|
||||
{currentContentTypeLayoutData.layouts.editRelations.length > 0 && (
|
||||
<SubWrapper style={{ padding: '0 20px 1px', marginBottom: '25px' }}>
|
||||
<div style={{ paddingTop: '22px' }}>
|
||||
{currentContentTypeLayoutData.layouts.editRelations.map(
|
||||
({ name, fieldSchema, metadatas, queryInfos }) => {
|
||||
return (
|
||||
<SelectWrapper
|
||||
{...fieldSchema}
|
||||
{...metadatas}
|
||||
queryInfos={queryInfos}
|
||||
key={name}
|
||||
name={name}
|
||||
relationsType={fieldSchema.relationType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</FormWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-3">
|
||||
<InformationCard />
|
||||
<Padded size="smd" top />
|
||||
{currentContentTypeLayoutData.layouts.editRelations.length > 0 && (
|
||||
<SubWrapper style={{ padding: '0 20px 1px', marginBottom: '25px' }}>
|
||||
<div style={{ paddingTop: '22px' }}>
|
||||
{currentContentTypeLayoutData.layouts.editRelations.map(
|
||||
({ name, fieldSchema, metadatas, queryInfos }) => {
|
||||
return (
|
||||
<SelectWrapper
|
||||
{...fieldSchema}
|
||||
{...metadatas}
|
||||
queryInfos={queryInfos}
|
||||
key={name}
|
||||
name={name}
|
||||
relationsType={fieldSchema.relationType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
</SubWrapper>
|
||||
)}
|
||||
<LinkWrapper>
|
||||
<ul>
|
||||
<CheckPermissions
|
||||
permissions={pluginPermissions.collectionTypesConfigurations}
|
||||
>
|
||||
<LiLink
|
||||
message={{
|
||||
id: 'app.links.configure-view',
|
||||
}}
|
||||
icon="layout"
|
||||
url="ctm-configurations/edit-settings/content-types"
|
||||
onClick={() => {
|
||||
// emitEvent('willEditContentTypeLayoutFromEditView');
|
||||
}}
|
||||
/>
|
||||
</CheckPermissions>
|
||||
{getInjectedComponents(
|
||||
'editView',
|
||||
'right.links',
|
||||
plugins,
|
||||
currentEnvironment,
|
||||
slug
|
||||
)}
|
||||
{allowedActions.canDelete && <DeleteLink />}
|
||||
</ul>
|
||||
</LinkWrapper>
|
||||
</div>
|
||||
</SubWrapper>
|
||||
)}
|
||||
<LinkWrapper>
|
||||
<ul>
|
||||
<CheckPermissions permissions={pluginPermissions.collectionTypesConfigurations}>
|
||||
<LiLink
|
||||
message={{
|
||||
id: 'app.links.configure-view',
|
||||
}}
|
||||
icon="layout"
|
||||
url="ctm-configurations/edit-settings/content-types"
|
||||
onClick={() => {
|
||||
// emitEvent('willEditContentTypeLayoutFromEditView');
|
||||
}}
|
||||
/>
|
||||
</CheckPermissions>
|
||||
{getInjectedComponents(
|
||||
'editView',
|
||||
'right.links',
|
||||
plugins,
|
||||
currentEnvironment,
|
||||
slug
|
||||
)}
|
||||
{allowedActions.canDelete && <DeleteLink />}
|
||||
</ul>
|
||||
</LinkWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</EditViewDataManagerProvider>
|
||||
</div>
|
||||
</Container>
|
||||
</EditViewDataManagerProvider>
|
||||
);
|
||||
}}
|
||||
</CollectionTypeWrapper>
|
||||
</ContentTypeLayoutContext.Provider>
|
||||
</EditViewProvider>
|
||||
);
|
||||
|
@ -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;
|
@ -1,2 +1,3 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as createAttributesLayout } from './createAttributesLayout';
|
||||
export { default as getFieldsActionMatchingPermissions } from './getFieldsActionMatchingPermissions';
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
@ -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 };
|
@ -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),
|
||||
});
|
||||
} 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]);
|
||||
dispatch({
|
||||
type: 'INIT_FORM',
|
||||
initialValues,
|
||||
});
|
||||
}, [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;
|
||||
|
@ -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)
|
||||
|
@ -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';
|
||||
|
@ -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 };
|
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as testData } from './data';
|
@ -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;
|
@ -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';
|
||||
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user