strapi-plugin-content-manager
Version:
A powerful UI to easily manage your data.
520 lines (450 loc) • 13.6 kB
JavaScript
import React, { useCallback, useEffect, useMemo, useRef, useReducer } from 'react';
import { cloneDeep, get, isEmpty, isEqual, set } from 'lodash';
import PropTypes from 'prop-types';
import { Prompt, Redirect } from 'react-router-dom';
import {
LoadingIndicatorPage,
useGlobalContext,
OverlayBlocker,
ContentManagerEditViewDataManagerContext,
} from 'strapi-helper-plugin';
import { getTrad, removeKeyInObject } from '../../utils';
import reducer, { initialState } from './reducer';
import { cleanData, createYupSchema, getYupInnerErrors } from './utils';
const EditViewDataManagerProvider = ({
allLayoutData,
allowedActions: { canRead, canUpdate },
children,
componentsDataStructure,
contentTypeDataStructure,
createActionAllowedFields,
from,
initialValues,
isCreatingEntry,
isLoadingForData,
isSingleType,
onPost,
onPublish,
onPut,
onUnpublish,
readActionAllowedFields,
// Not sure this is needed anymore
redirectToPreviousPage,
slug,
status,
updateActionAllowedFields,
}) => {
const [reducerState, dispatch] = useReducer(reducer, initialState);
const {
formErrors,
initialData,
modifiedData,
modifiedDZName,
shouldCheckErrors,
} = reducerState.toJS();
const currentContentTypeLayout = get(allLayoutData, ['contentType'], {});
const hasDraftAndPublish = useMemo(() => {
return get(currentContentTypeLayout, ['options', 'draftAndPublish'], false);
}, [currentContentTypeLayout]);
const shouldNotRunValidations = useMemo(() => {
return hasDraftAndPublish && !initialData.published_at;
}, [hasDraftAndPublish, initialData.published_at]);
const { emitEvent, formatMessage } = useGlobalContext();
const emitEventRef = useRef(emitEvent);
const shouldRedirectToHomepageWhenEditingEntry = useMemo(() => {
if (isLoadingForData) {
return false;
}
if (isCreatingEntry) {
return false;
}
if (canRead === false && canUpdate === false) {
return true;
}
return false;
}, [isLoadingForData, isCreatingEntry, canRead, canUpdate]);
// TODO check this effect if it is really needed (not prio)
useEffect(() => {
if (!isLoadingForData) {
checkFormErrors();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldCheckErrors]);
useEffect(() => {
if (shouldRedirectToHomepageWhenEditingEntry) {
strapi.notification.info(getTrad('permissions.not-allowed.update'));
}
}, [shouldRedirectToHomepageWhenEditingEntry]);
useEffect(() => {
dispatch({
type: 'SET_DEFAULT_DATA_STRUCTURES',
componentsDataStructure,
contentTypeDataStructure,
});
}, [componentsDataStructure, contentTypeDataStructure]);
useEffect(() => {
dispatch({
type: 'INIT_FORM',
initialValues,
});
}, [initialValues]);
const addComponentToDynamicZone = useCallback((keys, componentUid, shouldCheckErrors = false) => {
emitEventRef.current('didAddComponentToDynamicZone');
dispatch({
type: 'ADD_COMPONENT_TO_DYNAMIC_ZONE',
keys: keys.split('.'),
componentUid,
shouldCheckErrors,
});
}, []);
const addNonRepeatableComponentToField = useCallback((keys, componentUid) => {
dispatch({
type: 'ADD_NON_REPEATABLE_COMPONENT_TO_FIELD',
keys: keys.split('.'),
componentUid,
});
}, []);
const addRelation = useCallback(({ target: { name, value } }) => {
dispatch({
type: 'ADD_RELATION',
keys: name.split('.'),
value,
});
}, []);
const addRepeatableComponentToField = useCallback(
(keys, componentUid, shouldCheckErrors = false) => {
dispatch({
type: 'ADD_REPEATABLE_COMPONENT_TO_FIELD',
keys: keys.split('.'),
componentUid,
shouldCheckErrors,
});
},
[]
);
const yupSchema = useMemo(() => {
const options = { isCreatingEntry, isDraft: shouldNotRunValidations, isFromComponent: false };
return createYupSchema(
currentContentTypeLayout,
{
components: allLayoutData.components || {},
},
options
);
}, [
allLayoutData.components,
currentContentTypeLayout,
isCreatingEntry,
shouldNotRunValidations,
]);
const checkFormErrors = useCallback(
async (dataToSet = {}) => {
let errors = {};
const updatedData = cloneDeep(modifiedData);
if (!isEmpty(updatedData)) {
set(updatedData, dataToSet.path, dataToSet.value);
}
try {
// Validate the form using yup
await yupSchema.validate(updatedData, { abortEarly: false });
} catch (err) {
errors = getYupInnerErrors(err);
if (modifiedDZName) {
errors = Object.keys(errors).reduce((acc, current) => {
const dzName = current.split('.')[0];
if (dzName !== modifiedDZName) {
acc[current] = errors[current];
}
return acc;
}, {});
}
}
dispatch({
type: 'SET_FORM_ERRORS',
errors,
});
},
[modifiedDZName, modifiedData, yupSchema]
);
const handleChange = useCallback(
({ target: { name, value, type } }, shouldSetInitialValue = false) => {
let inputValue = value;
// Empty string is not a valid date,
// Set the date to null when it's empty
if (type === 'date' && value === '') {
inputValue = null;
}
if (type === 'password' && !value) {
dispatch({
type: 'REMOVE_PASSWORD_FIELD',
keys: name.split('.'),
});
return;
}
// Allow to reset enum
if (type === 'select-one' && value === '') {
inputValue = null;
}
// Allow to reset number input
if (type === 'number' && value === '') {
inputValue = null;
}
dispatch({
type: 'ON_CHANGE',
keys: name.split('.'),
value: inputValue,
shouldSetInitialValue,
});
},
[]
);
const createFormData = useCallback(
data => {
// First we need to remove the added keys needed for the dnd
const preparedData = removeKeyInObject(cloneDeep(data), '__temp_key__');
// Then we need to apply our helper
const cleanedData = cleanData(
preparedData,
currentContentTypeLayout,
allLayoutData.components
);
return cleanedData;
},
[allLayoutData.components, currentContentTypeLayout]
);
const trackerProperty = useMemo(() => {
if (!hasDraftAndPublish) {
return {};
}
return shouldNotRunValidations ? { status: 'draft' } : {};
}, [hasDraftAndPublish, shouldNotRunValidations]);
const handleSubmit = useCallback(
async e => {
e.preventDefault();
let errors = {};
// First validate the form
try {
await yupSchema.validate(modifiedData, { abortEarly: false });
const formData = createFormData(modifiedData);
if (isCreatingEntry) {
onPost(formData, trackerProperty);
} else {
onPut(formData, trackerProperty);
}
} catch (err) {
console.error('ValidationError');
console.error(err);
errors = getYupInnerErrors(err);
}
dispatch({
type: 'SET_FORM_ERRORS',
errors,
});
},
[createFormData, isCreatingEntry, modifiedData, onPost, onPut, trackerProperty, yupSchema]
);
const handlePublish = useCallback(async () => {
// Create yup schema here's we need to apply all the validations
const schema = createYupSchema(
currentContentTypeLayout,
{
components: get(allLayoutData, 'components', {}),
},
{ isCreatingEntry, isDraft: false, isFromComponent: false }
);
let errors = {};
try {
// Validate the form using yup
await schema.validate(modifiedData, { abortEarly: false });
onPublish();
} catch (err) {
console.error('ValidationError');
console.error(err);
errors = getYupInnerErrors(err);
}
dispatch({
type: 'SET_FORM_ERRORS',
errors,
});
}, [allLayoutData, currentContentTypeLayout, isCreatingEntry, modifiedData, onPublish]);
const shouldCheckDZErrors = useCallback(
dzName => {
const doesDZHaveError = Object.keys(formErrors).some(key => key.split('.')[0] === dzName);
const shouldCheckErrors = !isEmpty(formErrors) && doesDZHaveError;
return shouldCheckErrors;
},
[formErrors]
);
const moveComponentDown = useCallback(
(dynamicZoneName, currentIndex) => {
emitEventRef.current('changeComponentsOrder');
dispatch({
type: 'MOVE_COMPONENT_DOWN',
dynamicZoneName,
currentIndex,
shouldCheckErrors: shouldCheckDZErrors(dynamicZoneName),
});
},
[shouldCheckDZErrors]
);
const moveComponentUp = useCallback(
(dynamicZoneName, currentIndex) => {
emitEventRef.current('changeComponentsOrder');
dispatch({
type: 'MOVE_COMPONENT_UP',
dynamicZoneName,
currentIndex,
shouldCheckErrors: shouldCheckDZErrors(dynamicZoneName),
});
},
[shouldCheckDZErrors]
);
const moveComponentField = useCallback((pathToComponent, dragIndex, hoverIndex) => {
dispatch({
type: 'MOVE_COMPONENT_FIELD',
pathToComponent,
dragIndex,
hoverIndex,
});
}, []);
const moveRelation = useCallback((dragIndex, overIndex, name) => {
dispatch({
type: 'MOVE_FIELD',
dragIndex,
overIndex,
keys: name.split('.'),
});
}, []);
const onRemoveRelation = useCallback(keys => {
dispatch({
type: 'REMOVE_RELATION',
keys,
});
}, []);
const removeComponentFromDynamicZone = useCallback(
(dynamicZoneName, index) => {
emitEventRef.current('removeComponentFromDynamicZone');
dispatch({
type: 'REMOVE_COMPONENT_FROM_DYNAMIC_ZONE',
dynamicZoneName,
index,
shouldCheckErrors: shouldCheckDZErrors(dynamicZoneName),
});
},
[shouldCheckDZErrors]
);
const removeComponentFromField = useCallback((keys, componentUid) => {
dispatch({
type: 'REMOVE_COMPONENT_FROM_FIELD',
keys: keys.split('.'),
componentUid,
});
}, []);
const removeRepeatableField = useCallback((keys, componentUid) => {
dispatch({
type: 'REMOVE_REPEATABLE_FIELD',
keys: keys.split('.'),
componentUid,
});
}, []);
const triggerFormValidation = useCallback(() => {
dispatch({
type: 'TRIGGER_FORM_VALIDATION',
});
}, []);
const overlayBlockerParams = useMemo(
() => ({
children: <div />,
noGradient: true,
}),
[]
);
// Redirect the user to the previous page if he is not allowed to read/update a document
if (shouldRedirectToHomepageWhenEditingEntry) {
return <Redirect to={from} />;
}
return (
<ContentManagerEditViewDataManagerContext.Provider
value={{
addComponentToDynamicZone,
addNonRepeatableComponentToField,
addRelation,
addRepeatableComponentToField,
allLayoutData,
checkFormErrors,
createActionAllowedFields,
formErrors,
hasDraftAndPublish,
initialData,
isCreatingEntry,
isSingleType,
shouldNotRunValidations,
status,
layout: currentContentTypeLayout,
modifiedData,
moveComponentDown,
moveComponentField,
moveComponentUp,
moveRelation,
onChange: handleChange,
onPublish: handlePublish,
onUnpublish,
onRemoveRelation,
readActionAllowedFields,
redirectToPreviousPage,
removeComponentFromDynamicZone,
removeComponentFromField,
removeRepeatableField,
slug,
triggerFormValidation,
updateActionAllowedFields,
}}
>
<>
<OverlayBlocker
key="overlayBlocker"
isOpen={status !== 'resolved'}
{...overlayBlockerParams}
/>
{isLoadingForData ? (
<LoadingIndicatorPage />
) : (
<>
<Prompt
when={!isEqual(modifiedData, initialData)}
message={formatMessage({ id: 'global.prompt.unsaved' })}
/>
<form onSubmit={handleSubmit}>{children}</form>
</>
)}
</>
</ContentManagerEditViewDataManagerContext.Provider>
);
};
EditViewDataManagerProvider.defaultProps = {
from: '/',
redirectToPreviousPage: () => {},
};
EditViewDataManagerProvider.propTypes = {
allLayoutData: PropTypes.object.isRequired,
allowedActions: PropTypes.object.isRequired,
children: PropTypes.arrayOf(PropTypes.element).isRequired,
componentsDataStructure: PropTypes.object.isRequired,
contentTypeDataStructure: PropTypes.object.isRequired,
createActionAllowedFields: PropTypes.array.isRequired,
from: PropTypes.string,
initialValues: PropTypes.object.isRequired,
isCreatingEntry: PropTypes.bool.isRequired,
isLoadingForData: PropTypes.bool.isRequired,
isSingleType: PropTypes.bool.isRequired,
onPost: PropTypes.func.isRequired,
onPublish: PropTypes.func.isRequired,
onPut: PropTypes.func.isRequired,
onUnpublish: 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;