UNPKG

strapi-plugin-content-manager

Version:

A powerful UI to easily manage your data.

520 lines (450 loc) 13.6 kB
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;