diff --git a/packages/core/admin/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js b/packages/core/admin/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js index 55edb4668b..11e89a54f0 100644 --- a/packages/core/admin/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js +++ b/packages/core/admin/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js @@ -203,8 +203,6 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin } const displayErrors = useCallback( err => { const errorPayload = err.response.data; - console.error(errorPayload); - let errorMessage = get(errorPayload, ['error', 'message'], 'Bad Request'); // TODO handle errors correctly when back-end ready @@ -272,10 +270,14 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin } dispatch(setStatus('resolved')); replace(`/content-manager/collectionType/${slug}/${data.id}${rawQuery}`); + + return Promise.resolve(data); } catch (err) { - trackUsageRef.current('didNotCreateEntry', { error: err, trackerProperty }); displayErrors(err); + trackUsageRef.current('didNotCreateEntry', { error: err, trackerProperty }); dispatch(setStatus('resolved')); + + return Promise.reject(err); } }, [ @@ -308,9 +310,13 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin } type: 'success', message: { id: getTrad('success.record.publish') }, }); + + return Promise.resolve(data); } catch (err) { displayErrors(err); dispatch(setStatus('resolved')); + + return Promise.reject(err); } }, [cleanReceivedData, displayErrors, id, slug, dispatch, toggleNotification]); @@ -334,11 +340,15 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin } dispatch(submitSucceeded(cleanReceivedData(data))); dispatch(setStatus('resolved')); + + return Promise.resolve(data); } catch (err) { trackUsageRef.current('didNotEditEntry', { error: err, trackerProperty }); displayErrors(err); dispatch(setStatus('resolved')); + + return Promise.reject(err); } }, [cleanReceivedData, displayErrors, slug, id, dispatch, toggleNotification] @@ -362,9 +372,13 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin } dispatch(submitSucceeded(cleanReceivedData(data))); dispatch(setStatus('resolved')); + + return Promise.resolve(data); } catch (err) { dispatch(setStatus('resolved')); displayErrors(err); + + return Promise.reject(err); } }, [cleanReceivedData, displayErrors, id, slug, dispatch, toggleNotification]); diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js index aff6f07e39..e81ac5bb07 100644 --- a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js @@ -1,5 +1,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useReducer } from 'react'; -import { cloneDeep, get, isEmpty, isEqual, set } from 'lodash'; +import isEmpty from 'lodash/isEmpty'; +import cloneDeep from 'lodash/cloneDeep'; +import get from 'lodash/get'; +import isEqual from 'lodash/isEqual'; +import set from 'lodash/set'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; import { Prompt, Redirect } from 'react-router-dom'; @@ -10,10 +14,13 @@ import { useNotification, useOverlayBlocker, useTracking, + getYupInnerErrors, } from '@strapi/helper-plugin'; + import { getTrad, removeKeyInObject } from '../../utils'; import reducer, { initialState } from './reducer'; -import { cleanData, createYupSchema, getYupInnerErrors } from './utils'; +import { cleanData, createYupSchema } from './utils'; +import { getAPIInnerError } from './utils/getAPIInnerError'; const EditViewDataManagerProvider = ({ allLayoutData, @@ -262,7 +269,7 @@ const EditViewDataManagerProvider = ({ ); const createFormData = useCallback( - data => { + (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 @@ -286,34 +293,31 @@ const EditViewDataManagerProvider = ({ }, [hasDraftAndPublish, shouldNotRunValidations]); const handleSubmit = useCallback( - async e => { + async (e) => { e.preventDefault(); let errors = {}; - // First validate the form try { await yupSchema.validate(modifiedData, { abortEarly: false }); + } catch (err) { + errors = getYupInnerErrors(err); + } - const formData = createFormData(modifiedData); + try { + if (isEmpty(errors)) { + const formData = createFormData(modifiedData); - if (isCreatingEntry) { - onPost(formData, trackerProperty); - } else { - onPut(formData, trackerProperty); + if (isCreatingEntry) { + await onPost(formData, trackerProperty); + } else { + await onPut(formData, trackerProperty); + } } } catch (err) { - console.log('ValidationError'); - console.log(err); - - errors = getYupInnerErrors(err); - - toggleNotification({ - type: 'warning', - message: { - id: getTrad('containers.EditView.notification.errors'), - defaultMessage: 'The form contains some errors', - }, - }); + errors = { + ...errors, + ...getAPIInnerError(err), + }; } dispatch({ @@ -321,16 +325,7 @@ const EditViewDataManagerProvider = ({ errors, }); }, - [ - createFormData, - isCreatingEntry, - modifiedData, - onPost, - onPut, - toggleNotification, - trackerProperty, - yupSchema, - ] + [createFormData, isCreatingEntry, modifiedData, onPost, onPut, trackerProperty, yupSchema] ); const handlePublish = useCallback(async () => { @@ -345,17 +340,22 @@ const EditViewDataManagerProvider = ({ 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); } + try { + if (isEmpty(errors)) { + await onPublish(); + } + } catch (err) { + errors = { + ...errors, + ...getAPIInnerError(err), + }; + } + dispatch({ type: 'SET_FORM_ERRORS', errors, @@ -363,8 +363,8 @@ const EditViewDataManagerProvider = ({ }, [allLayoutData, currentContentTypeLayout, isCreatingEntry, modifiedData, onPublish]); const shouldCheckDZErrors = useCallback( - dzName => { - const doesDZHaveError = Object.keys(formErrors).some(key => key.split('.')[0] === dzName); + (dzName) => { + const doesDZHaveError = Object.keys(formErrors).some((key) => key.split('.')[0] === dzName); const shouldCheckErrors = !isEmpty(formErrors) && doesDZHaveError; return shouldCheckErrors; @@ -418,7 +418,7 @@ const EditViewDataManagerProvider = ({ }); }, []); - const onRemoveRelation = useCallback(keys => { + const onRemoveRelation = useCallback((keys) => { dispatch({ type: 'REMOVE_RELATION', keys, diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/getAPIInnerError.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/getAPIInnerError.js new file mode 100644 index 0000000000..8c6c702dc6 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/getAPIInnerError.js @@ -0,0 +1,18 @@ +import { getTrad } from '../../../utils'; + +export function getAPIInnerError(error) { + const errorPayload = error.response.data.error.details.errors; + const validationErrors = errorPayload.reduce((acc, err) => { + acc[err.path.join('.')] = { + id: getTrad(`apiError.${err.message}`), + defaultMessage: err.message, + values: { + field: err.path[err.path.length - 1], + }, + }; + + return acc; + }, {}); + + return validationErrors; +} diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/getYupInnerErrors.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/getYupInnerErrors.js deleted file mode 100644 index 39c8340583..0000000000 --- a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/getYupInnerErrors.js +++ /dev/null @@ -1,17 +0,0 @@ -import { get } from 'lodash'; - -const getYupInnerErrors = error => { - return get(error, 'inner', []).reduce((acc, curr) => { - acc[ - curr.path - .split('[') - .join('.') - .split(']') - .join('') - ] = { id: curr.message }; - - return acc; - }, {}); -}; - -export default getYupInnerErrors; diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/handleAPIError.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/handleAPIError.js new file mode 100644 index 0000000000..8f31ee5e77 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/handleAPIError.js @@ -0,0 +1,15 @@ +import { getTrad } from '../../../utils'; + +export function handleAPIError(error) { + const errorPayload = error.response.data.error.details.errors; + const validationErrors = errorPayload.reduce((acc, err) => { + acc[err.path.join('.')] = { + id: getTrad(`apiError.${err.message}`), + defaultMessage: err.message, + }; + + return acc; + }, {}); + + return validationErrors; +} diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/index.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/index.js index 9e2da13df9..9a4f37c97f 100644 --- a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/index.js +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/index.js @@ -1,4 +1,3 @@ export { default as moveFields } from './moveFields'; export { default as cleanData } from './cleanData'; -export { default as getYupInnerErrors } from './getYupInnerErrors'; export { default as createYupSchema } from './schema'; diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/tests/getAPIInnerError.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/tests/getAPIInnerError.js new file mode 100644 index 0000000000..57152fa34a --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/tests/getAPIInnerError.js @@ -0,0 +1,36 @@ +import { getAPIInnerError } from '../getAPIInnerError'; + +const API_ERROR_FIXTURE = { + response: { + data: { + error: { + details: { + errors: [ + { + path: ['field', '0', 'name'], + message: 'Field contains errors', + }, + + { + path: ['field'], + message: 'Field must be unique', + }, + ], + }, + }, + }, + }, +}; + +describe('getAPIInnerError', () => { + test('transforms API errors into errors, which can be rendered by the CM', () => { + expect(getAPIInnerError(API_ERROR_FIXTURE)).toMatchObject({ + 'field.0.name': { + id: 'content-manager.apiError.Field contains errors', + }, + field: { + id: 'content-manager.apiError.Field must be unique', + }, + }); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/tests/handleAPIError.test.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/tests/handleAPIError.test.js new file mode 100644 index 0000000000..ecd0f97830 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/tests/handleAPIError.test.js @@ -0,0 +1,36 @@ +import { handleAPIError } from '../handleAPIError'; + +const API_ERROR_FIXTURE = { + response: { + data: { + error: { + details: { + errors: [ + { + path: ['field', '0', 'name'], + message: 'Field contains errors', + }, + + { + path: ['field'], + message: 'Field must be unique', + }, + ], + }, + }, + }, + }, +}; + +describe('handleAPIError', () => { + test('transforms API errors into errors, which can be rendered by the CM', () => { + expect(handleAPIError(API_ERROR_FIXTURE)).toMatchObject({ + 'field.0.name': { + id: 'content-manager.apiError.Field contains errors', + }, + field: { + id: 'content-manager.apiError.Field must be unique', + }, + }); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js index 5a76907578..40b1128f2b 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js @@ -82,7 +82,6 @@ const InputUID = ({ onChange({ target: { name, value: data, type: 'text' } }, shouldSetInitialValue); setIsLoading(false); } catch (err) { - console.error({ err }); setIsLoading(false); } }; @@ -107,7 +106,6 @@ const InputUID = ({ setIsLoading(false); } catch (err) { - console.error({ err }); setIsLoading(false); } }; @@ -184,12 +182,10 @@ const InputUID = ({ onChange(e); }; - const formattedError = error ? formatMessage({ id: error, defaultMessage: error }) : undefined; - return ( {availability && availability.isAvailable && !regenerateLabel && ( diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js index 04f382bc85..3f59cd56cf 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js @@ -42,10 +42,7 @@ function Inputs({ const disabled = useMemo(() => !get(metadatas, 'editable', true), [metadatas]); const type = fieldSchema.type; - - const errorId = useMemo(() => { - return get(formErrors, [keys, 'id'], null); - }, [formErrors, keys]); + const error = get(formErrors, [keys], null); const fieldName = useMemo(() => { return getFieldName(keys); @@ -160,10 +157,10 @@ function Inputs({ return disabled; }, [disabled, isCreatingEntry, isUserAllowedToEditField, isUserAllowedToReadField]); - const options = useMemo(() => generateOptions(fieldSchema.enum || [], isRequired), [ - fieldSchema, - isRequired, - ]); + const options = useMemo( + () => generateOptions(fieldSchema.enum || [], isRequired), + [fieldSchema, isRequired] + ); const { label, description, placeholder, visible } = metadatas; @@ -177,7 +174,7 @@ function Inputs({ description={description ? { id: description, defaultMessage: description } : null} intlLabel={{ id: label, defaultMessage: label }} labelAction={labelAction} - error={errorId} + error={error && formatMessage(error)} name={keys} required={isRequired} /> @@ -215,6 +212,7 @@ function Inputs({ } queryInfos={queryInfos} value={value} + error={error && formatMessage(error)} /> ); } @@ -228,7 +226,7 @@ function Inputs({ isNullable={inputType === 'bool' && [null, undefined].includes(fieldSchema.default)} description={description ? { id: description, defaultMessage: description } : null} disabled={shouldDisableField} - error={errorId} + error={error} labelAction={labelAction} contentTypeUID={currentContentTypeLayout.uid} customInputs={{ diff --git a/packages/core/admin/admin/src/content-manager/components/SingleTypeFormWrapper/index.js b/packages/core/admin/admin/src/content-manager/components/SingleTypeFormWrapper/index.js index 825d92e4d0..d154bc2140 100644 --- a/packages/core/admin/admin/src/content-manager/components/SingleTypeFormWrapper/index.js +++ b/packages/core/admin/admin/src/content-manager/components/SingleTypeFormWrapper/index.js @@ -144,8 +144,6 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => { const displayErrors = useCallback( err => { const errorPayload = err.response.payload; - console.error(errorPayload); - let errorMessage = get(errorPayload, ['message'], 'Bad Request'); // TODO handle errors correctly when back-end ready @@ -178,10 +176,12 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => { } catch (err) { trackUsageRef.current('didNotDeleteEntry', { error: err, ...trackerProperty }); + displayErrors(err); + return Promise.reject(err); } }, - [slug, toggleNotification, searchToSend] + [slug, displayErrors, toggleNotification, searchToSend] ); const onDeleteSucceeded = useCallback(() => { @@ -211,12 +211,16 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => { setIsCreatingEntry(false); dispatch(setStatus('resolved')); + + return Promise.resolve(data); } catch (err) { trackUsageRef.current('didNotCreateEntry', { error: err, trackerProperty }); displayErrors(err); dispatch(setStatus('resolved')); + + return Promise.reject(err); } }, [cleanReceivedData, displayErrors, slug, dispatch, rawQuery, toggleNotification, setCurrentStep] @@ -239,10 +243,14 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => { dispatch(submitSucceeded(cleanReceivedData(data))); dispatch(setStatus('resolved')); + + return Promise.resolve(data); } catch (err) { displayErrors(err); dispatch(setStatus('resolved')); + + return Promise.reject(err); } }, [cleanReceivedData, displayErrors, slug, searchToSend, dispatch, toggleNotification]); @@ -267,12 +275,16 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => { dispatch(submitSucceeded(cleanReceivedData(data))); dispatch(setStatus('resolved')); + + return Promise.resolve(data); } catch (err) { displayErrors(err); trackUsageRef.current('didNotEditEntry', { error: err, trackerProperty }); dispatch(setStatus('resolved')); + + return Promise.reject(err); } }, [cleanReceivedData, displayErrors, slug, dispatch, rawQuery, toggleNotification] diff --git a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js b/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js index 2375ff7d96..185b99df04 100644 --- a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js @@ -118,7 +118,6 @@ const Wysiwyg = ({ ) : ''; - const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : ''; const label = intlLabel.id ? formatMessage( { id: intlLabel.id, defaultMessage: intlLabel.defaultMessage }, @@ -157,7 +156,7 @@ const Wysiwyg = ({ disabled={disabled} isExpandMode={isExpandMode} editorRef={editorRef} - error={errorMessage} + error={error} isPreviewMode={isPreviewMode} name={name} onChange={onChange} @@ -171,10 +170,10 @@ const Wysiwyg = ({ - {errorMessage && ( + {error && ( - {errorMessage} + {error} )} diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 914dfd58d0..78c388f2ab 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -642,6 +642,8 @@ "content-manager.success.record.save": "Saved", "content-manager.success.record.unpublish": "Unpublished", "content-manager.utils.data-loaded": "The {number, plural, =1 {entry has} other {entries have}} successfully been loaded", + "content-manager.apiError.This attribute must be unique": "{field} must be unique", + "form.button.continue": "Continue", "form.button.done": "Done", "global.actions": "Actions", "global.back": "Back", diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 2f71fb98f2..3c71ed74e0 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -60,6 +60,30 @@ const GenericInput = ({ */ const valueWithEmptyStringFallback = value ?? ''; + function getErrorMessage(error) { + if (!error) { + return null; + } + + const values = { + ...error.values, + }; + + if (typeof error === 'string') { + return formatMessage({ id: error, defaultMessage: error }, values); + } + + return formatMessage( + { + id: error.id, + defaultMessage: error?.defaultMessage ?? error.id, + }, + values + ); + } + + const errorMessage = getErrorMessage(error); + if (CustomInput) { return ( { +const getYupInnerErrors = (error) => { return get(error, 'inner', []).reduce((acc, curr) => { - acc[ - curr.path - .split('[') - .join('.') - .split(']') - .join('') - ] = { id: curr.message }; + acc[curr.path.split('[').join('.').split(']').join('')] = { + id: curr.message, + defaultMessage: curr.message, + }; return acc; }, {}); diff --git a/packages/core/upload/admin/src/components/MediaLibraryInput/index.js b/packages/core/upload/admin/src/components/MediaLibraryInput/index.js index c2969e5904..4610cd4c77 100644 --- a/packages/core/upload/admin/src/components/MediaLibraryInput/index.js +++ b/packages/core/upload/admin/src/components/MediaLibraryInput/index.js @@ -117,7 +117,6 @@ export const MediaLibraryInput = ({ setUploadedFiles(prev => [...prev, ...uploadedFiles]); }; - const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : ''; const hint = description ? formatMessage( { id: description.id, defaultMessage: description.defaultMessage }, @@ -148,7 +147,7 @@ export const MediaLibraryInput = ({ onEditAsset={handleAssetEdit} onNext={handleNext} onPrevious={handlePrevious} - error={errorMessage} + error={error} hint={hint} required={required} selectedAssetIndex={selectedIndex} @@ -198,7 +197,7 @@ MediaLibraryInput.propTypes = { defaultMessage: PropTypes.string, values: PropTypes.shape({}), }), - error: PropTypes.shape({ id: PropTypes.string, defaultMessage: PropTypes.string }), + error: PropTypes.string, intlLabel: PropTypes.shape({ id: PropTypes.string, defaultMessage: PropTypes.string }), multiple: PropTypes.bool, onChange: PropTypes.func.isRequired,