mirror of
https://github.com/strapi/strapi.git
synced 2025-12-24 13:43:41 +00:00
Handle errors
This commit is contained in:
parent
de15224e75
commit
7c2633cc53
@ -7,16 +7,14 @@
|
||||
},
|
||||
"options": {
|
||||
"increments": true,
|
||||
"timestamps": [
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"timestamps": ["created_at", "updated_at"],
|
||||
"comment": ""
|
||||
},
|
||||
"attributes": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
"required": true,
|
||||
"minLength": 3
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
@ -25,4 +23,4 @@
|
||||
"minLength": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get, omit } from 'lodash';
|
||||
// import { InputsIndex } from 'strapi-helper-plugin';
|
||||
import { get, isEmpty, omit, toLower } from 'lodash';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { InputFileWithErrors } from 'strapi-helper-plugin';
|
||||
import { Inputs as InputsIndex } from '@buffetjs/custom';
|
||||
|
||||
@ -10,7 +10,7 @@ import InputJSONWithErrors from '../InputJSONWithErrors';
|
||||
import WysiwygWithErrors from '../WysiwygWithErrors';
|
||||
|
||||
const getInputType = (type = '') => {
|
||||
switch (type.toLowerCase()) {
|
||||
switch (toLower(type)) {
|
||||
case 'boolean':
|
||||
return 'bool';
|
||||
case 'biginteger':
|
||||
@ -50,12 +50,12 @@ const getInputType = (type = '') => {
|
||||
function Inputs({ autoFocus, keys, name, onBlur }) {
|
||||
const {
|
||||
didCheckErrors,
|
||||
errors,
|
||||
formErrors,
|
||||
layout,
|
||||
modifiedData,
|
||||
onChange,
|
||||
} = useDataManager();
|
||||
console.log({ errors });
|
||||
|
||||
const attribute = useMemo(
|
||||
() => get(layout, ['schema', 'attributes', name], {}),
|
||||
[layout, name]
|
||||
@ -85,37 +85,57 @@ function Inputs({ autoFocus, keys, name, onBlur }) {
|
||||
if (visible === false) {
|
||||
return null;
|
||||
}
|
||||
const temporaryErrorIdUntilBuffetjsSupportsFormattedMessage =
|
||||
'app.utils.defaultMessage';
|
||||
const errorId = get(
|
||||
formErrors,
|
||||
[keys, 'id'],
|
||||
temporaryErrorIdUntilBuffetjsSupportsFormattedMessage
|
||||
);
|
||||
|
||||
// const inputErrors = get(errors, keys, []);
|
||||
// TODO add the option placeholder to buffetjs
|
||||
// check https://github.com/strapi/strapi/issues/2471
|
||||
// const withOptionPlaceholder = get(attribute, 'type', '') === 'enumeration';
|
||||
|
||||
// TODO format error for the JSON, the WYSIWYG and also the file inputs
|
||||
// TODO check if the height for the textarea is 196px (not mandatory)
|
||||
|
||||
return (
|
||||
<InputsIndex
|
||||
{...metadatas}
|
||||
autoFocus={autoFocus}
|
||||
didCheckErrors={didCheckErrors}
|
||||
disabled={disabled}
|
||||
// errors={errors}
|
||||
// errors={inputErrors}
|
||||
// inputDescription={description}
|
||||
description={description}
|
||||
// inputStyle={inputStyle}
|
||||
customInputs={{
|
||||
media: InputFileWithErrors,
|
||||
json: InputJSONWithErrors,
|
||||
wysiwyg: WysiwygWithErrors,
|
||||
<FormattedMessage id={errorId}>
|
||||
{error => {
|
||||
return (
|
||||
<InputsIndex
|
||||
{...metadatas}
|
||||
autoFocus={autoFocus}
|
||||
didCheckErrors={didCheckErrors}
|
||||
disabled={disabled}
|
||||
error={
|
||||
isEmpty(error) ||
|
||||
errorId === temporaryErrorIdUntilBuffetjsSupportsFormattedMessage
|
||||
? null
|
||||
: error
|
||||
}
|
||||
inputDescription={description}
|
||||
description={description}
|
||||
// inputStyle={inputStyle} used to set the height of the text area
|
||||
customInputs={{
|
||||
media: InputFileWithErrors,
|
||||
json: InputJSONWithErrors,
|
||||
wysiwyg: WysiwygWithErrors,
|
||||
}}
|
||||
multiple={get(attribute, 'multiple', false)}
|
||||
name={name}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
options={get(attribute, 'enum', [])}
|
||||
type={getInputType(type)}
|
||||
validations={validations}
|
||||
value={value}
|
||||
// withOptionPlaceholder={withOptionPlaceholder}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
multiple={get(attribute, 'multiple', false)}
|
||||
name={name}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
options={get(attribute, 'enum', [])}
|
||||
type={getInputType(type)}
|
||||
// validations={null}
|
||||
validations={validations}
|
||||
value={value}
|
||||
// withOptionPlaceholder={withOptionPlaceholder}
|
||||
/>
|
||||
</FormattedMessage>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -14,11 +14,11 @@ const Header = () => {
|
||||
const [showWarningDelete, setWarningDelete] = useState(false);
|
||||
|
||||
const { id } = useParams();
|
||||
const { initialData, layout } = useDataManager();
|
||||
const { initialData, layout, shouldShowLoadingState } = useDataManager();
|
||||
|
||||
const currentContentTypeMainField = get(
|
||||
layout,
|
||||
['contentType', 'settings', 'mainField'],
|
||||
['settings', 'mainField'],
|
||||
'id'
|
||||
);
|
||||
const isCreatingEntry = id === 'create';
|
||||
@ -39,6 +39,8 @@ const Header = () => {
|
||||
toggleWarningDelete();
|
||||
};
|
||||
|
||||
console.log({ shouldShowLoadingState });
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginHeader
|
||||
@ -50,15 +52,15 @@ const Header = () => {
|
||||
toggleWarningCancel();
|
||||
},
|
||||
type: 'button',
|
||||
// disabled: isSubmitting, // TODO STATE WHEN SUBMITING
|
||||
disabled: shouldShowLoadingState,
|
||||
},
|
||||
{
|
||||
kind: 'primary',
|
||||
label: `${pluginId}.containers.Edit.submit`,
|
||||
type: 'submit',
|
||||
// loader: isSubmitting,
|
||||
// style: isSubmitting ? { marginRight: '18px' } : {},
|
||||
// disabled: isSubmitting, // TODO STATE WHEN SUBMITING
|
||||
loader: shouldShowLoadingState,
|
||||
style: shouldShowLoadingState ? { marginRight: '18px' } : {},
|
||||
disabled: shouldShowLoadingState,
|
||||
},
|
||||
]}
|
||||
subActions={
|
||||
@ -72,7 +74,7 @@ const Header = () => {
|
||||
toggleWarningDelete();
|
||||
},
|
||||
type: 'button',
|
||||
// disabled: isSubmitting, // TODO STATE WHEN SUBMITING
|
||||
disabled: shouldShowLoadingState,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@ -49,9 +49,13 @@ const EditView = ({
|
||||
const [reducerState, dispatch] = useReducer(reducer, initialState, () =>
|
||||
init(initialState)
|
||||
);
|
||||
const allLayoutData = useMemo(() => get(layouts, [slug], {}), [
|
||||
layouts,
|
||||
slug,
|
||||
]);
|
||||
const currentContentTypeLayoutData = useMemo(
|
||||
() => get(layouts, [slug, 'contentType'], {}),
|
||||
[layouts, slug]
|
||||
() => get(allLayoutData, ['contentType'], {}),
|
||||
[allLayoutData]
|
||||
);
|
||||
const currentContentTypeLayout = useMemo(
|
||||
() => get(currentContentTypeLayoutData, ['layouts', 'edit'], []),
|
||||
@ -110,7 +114,7 @@ const EditView = ({
|
||||
|
||||
return (
|
||||
<EditViewProvider layout={currentContentTypeLayoutData}>
|
||||
<EditViewDataManagerProvider layout={currentContentTypeLayoutData}>
|
||||
<EditViewDataManagerProvider allLayoutData={allLayoutData} slug={slug}>
|
||||
<BackHeader onClick={() => redirectToPreviousPage()} />
|
||||
<Container className="container-fluid">
|
||||
<Header />
|
||||
@ -133,7 +137,6 @@ const EditView = ({
|
||||
return (
|
||||
<div className="row" key={fieldsBlockIndex}>
|
||||
{fieldsBlock.map(({ name, size }, fieldIndex) => {
|
||||
console.log({ fieldIndex });
|
||||
return (
|
||||
<div className={`col-${size}`} key={name}>
|
||||
<Inputs
|
||||
|
||||
@ -1,17 +1,74 @@
|
||||
import React, { useReducer } from 'react';
|
||||
// import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
// import { get } from 'lodash';
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
getQueryParameters,
|
||||
request,
|
||||
LoadingIndicatorPage,
|
||||
} from 'strapi-helper-plugin';
|
||||
import pluginId from '../../pluginId';
|
||||
import EditViewDataManagerContext from '../../contexts/EditViewDataManager';
|
||||
|
||||
import createYupSchema from './utils/schema';
|
||||
import init from './init';
|
||||
import reducer, { initialState } from './reducer';
|
||||
|
||||
// const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
|
||||
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
|
||||
|
||||
const EditViewDataManagerProvider = ({ children, layout }) => {
|
||||
const EditViewDataManagerProvider = ({ allLayoutData, children, slug }) => {
|
||||
const { id } = useParams();
|
||||
// Retrieve the search
|
||||
const { search } = useLocation();
|
||||
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
|
||||
const { initialData, modifiedData } = reducerState.toJS();
|
||||
const {
|
||||
formErrors,
|
||||
initialData,
|
||||
isLoading,
|
||||
modifiedData,
|
||||
shouldShowLoadingState,
|
||||
} = reducerState.toJS();
|
||||
|
||||
const currentContentTypeLayout = get(allLayoutData, ['contentType'], {});
|
||||
const abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
const isCreatingEntry = id === 'create';
|
||||
const source = getQueryParameters(search, 'source');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const data = await request(getRequestUrl(`${slug}/${id}`), {
|
||||
method: 'GET',
|
||||
params: { source },
|
||||
signal,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'GET_DATA_SUCCEEDED',
|
||||
data,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code !== 20) {
|
||||
strapi.notification.error(`${pluginId}.error.record.fetch`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Force state to be cleared when navigation from one entry to another
|
||||
dispatch({ type: 'RESET_PROPS' });
|
||||
|
||||
if (!isCreatingEntry) {
|
||||
fetchData();
|
||||
} else {
|
||||
// Will create default form
|
||||
console.log('will create default form');
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, slug, source]);
|
||||
|
||||
const addRelation = ({ target: { name, value } }) => {
|
||||
dispatch({
|
||||
@ -37,11 +94,39 @@ const EditViewDataManagerProvider = ({ children, layout }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = e => {
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
dispatch({
|
||||
type: 'SUBMIT_SUCCEEDED',
|
||||
|
||||
const schema = createYupSchema(currentContentTypeLayout, {
|
||||
components: get(allLayoutData, 'components', {}),
|
||||
});
|
||||
|
||||
try {
|
||||
// Validate the form using yup
|
||||
await schema.validate(modifiedData, { abortEarly: false });
|
||||
// Set the loading state in the plugin header
|
||||
dispatch({ type: 'IS_SUBMITING' });
|
||||
} catch (err) {
|
||||
const errors = get(err, 'inner', []).reduce((acc, curr) => {
|
||||
acc[
|
||||
curr.path
|
||||
.split('[')
|
||||
.join('.')
|
||||
.split(']')
|
||||
.join('')
|
||||
] = { id: curr.message };
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
dispatch({
|
||||
type: 'SUBMIT_ERRORS',
|
||||
errors,
|
||||
});
|
||||
console.log({ errors });
|
||||
}
|
||||
// dispatch({
|
||||
// type: 'SUBMIT_SUCCEEDED',
|
||||
// });
|
||||
};
|
||||
|
||||
const moveRelation = (dragIndex, overIndex, name) => {
|
||||
@ -60,27 +145,36 @@ const EditViewDataManagerProvider = ({ children, layout }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const showLoader = !isCreatingEntry && isLoading;
|
||||
|
||||
return (
|
||||
<EditViewDataManagerContext.Provider
|
||||
value={{
|
||||
addRelation,
|
||||
formErrors,
|
||||
initialData,
|
||||
layout,
|
||||
layout: currentContentTypeLayout,
|
||||
modifiedData,
|
||||
moveRelation,
|
||||
onChange: handleChange,
|
||||
onRemoveRelation,
|
||||
shouldShowLoadingState,
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>{children}</form>
|
||||
{showLoader ? (
|
||||
<LoadingIndicatorPage />
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>{children}</form>
|
||||
)}
|
||||
</EditViewDataManagerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
EditViewDataManagerProvider.defaultProps = {};
|
||||
EditViewDataManagerProvider.propTypes = {
|
||||
allLayoutData: PropTypes.object.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
layout: PropTypes.object.isRequired,
|
||||
slug: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default EditViewDataManagerProvider;
|
||||
|
||||
@ -4,9 +4,11 @@ import {
|
||||
} from 'immutable';
|
||||
|
||||
const initialState = fromJS({
|
||||
formErrors: {},
|
||||
isLoading: true,
|
||||
initialData: {},
|
||||
modifiedData: {},
|
||||
shouldShowLoadingState: false,
|
||||
});
|
||||
|
||||
const reducer = (state, action) => {
|
||||
@ -25,6 +27,13 @@ const reducer = (state, action) => {
|
||||
|
||||
return fromJS([el]);
|
||||
});
|
||||
case 'GET_DATA_SUCCEEDED':
|
||||
return state
|
||||
.update('initialData', () => fromJS(action.data))
|
||||
.update('modifiedData', () => fromJS(action.data))
|
||||
.update('isLoading', () => false);
|
||||
case 'IS_SUBMITING':
|
||||
return state.update('shouldShowLoadingState', () => true);
|
||||
case 'MOVE_FIELD':
|
||||
return state.updateIn(['modifiedData', ...action.keys], list => {
|
||||
return list
|
||||
@ -51,6 +60,12 @@ const reducer = (state, action) => {
|
||||
}
|
||||
case 'REMOVE_RELATION':
|
||||
return state.removeIn(['modifiedData', ...action.keys.split('.')]);
|
||||
case 'RESET_PROPS':
|
||||
return initialState;
|
||||
case 'SUBMIT_ERRORS':
|
||||
return state
|
||||
.update('formErrors', () => fromJS(action.errors))
|
||||
.update('shouldShowLoadingState', () => false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -0,0 +1,198 @@
|
||||
import {
|
||||
get,
|
||||
isBoolean,
|
||||
isNaN,
|
||||
isNumber,
|
||||
isNull,
|
||||
isArray,
|
||||
isObject,
|
||||
} from 'lodash';
|
||||
import * as yup from 'yup';
|
||||
import { translatedErrors as errorsTrads } from 'strapi-helper-plugin';
|
||||
|
||||
yup.addMethod(yup.mixed, 'defined', function() {
|
||||
return this.test(
|
||||
'defined',
|
||||
errorsTrads.required,
|
||||
value => value !== undefined
|
||||
);
|
||||
});
|
||||
|
||||
const getAttributes = data => get(data, ['schema', 'attributes'], {});
|
||||
|
||||
const createYupSchema = (model, { components }) => {
|
||||
const attributes = getAttributes(model);
|
||||
|
||||
return yup.object().shape(
|
||||
Object.keys(attributes).reduce((acc, current) => {
|
||||
const attribute = attributes[current];
|
||||
if (attribute.type !== 'relation' && attribute.type !== 'component') {
|
||||
const formatted = createYupSchemaAttribute(attribute.type, attribute);
|
||||
acc[current] = formatted;
|
||||
}
|
||||
|
||||
if (attribute.type === 'relation') {
|
||||
acc[current] = [
|
||||
'oneWay',
|
||||
'oneToOne',
|
||||
'manyToOne',
|
||||
'oneToManyMorph',
|
||||
'oneToOneMorph',
|
||||
].includes(attribute.relationType)
|
||||
? yup.object().nullable()
|
||||
: yup.array().nullable();
|
||||
}
|
||||
|
||||
if (attribute.type === 'component') {
|
||||
const componentFieldSchema = createYupSchema(
|
||||
components[attribute.component],
|
||||
{
|
||||
components,
|
||||
}
|
||||
);
|
||||
|
||||
if (attribute.repeatable === true) {
|
||||
const componentSchema =
|
||||
attribute.required === true
|
||||
? yup
|
||||
.array()
|
||||
.of(componentFieldSchema)
|
||||
.defined()
|
||||
: yup
|
||||
.array()
|
||||
.of(componentFieldSchema)
|
||||
.nullable();
|
||||
|
||||
acc[current] = componentSchema;
|
||||
|
||||
return acc;
|
||||
} else {
|
||||
const componentSchema = yup.lazy(obj => {
|
||||
if (obj !== undefined) {
|
||||
return attribute.required === true
|
||||
? componentFieldSchema.defined()
|
||||
: componentFieldSchema.nullable();
|
||||
}
|
||||
|
||||
return attribute.required === true
|
||||
? yup.object().defined()
|
||||
: yup.object().nullable();
|
||||
});
|
||||
|
||||
acc[current] = componentSchema;
|
||||
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
};
|
||||
const createYupSchemaAttribute = (type, validations) => {
|
||||
let schema = yup.mixed();
|
||||
if (
|
||||
['string', 'text', 'richtext', 'email', 'password', 'enumeration'].includes(
|
||||
type
|
||||
)
|
||||
) {
|
||||
schema = yup.string();
|
||||
}
|
||||
if (type === 'json') {
|
||||
schema = yup
|
||||
.mixed(errorsTrads.json)
|
||||
.test('isJSON', errorsTrads.json, value => {
|
||||
try {
|
||||
if (
|
||||
isObject(value) ||
|
||||
isBoolean(value) ||
|
||||
isNumber(value) ||
|
||||
isArray(value) ||
|
||||
isNaN(value) ||
|
||||
isNull(value)
|
||||
) {
|
||||
JSON.parse(JSON.stringify(value));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.nullable();
|
||||
}
|
||||
|
||||
if (type === 'email') {
|
||||
schema = schema.email(errorsTrads.email);
|
||||
}
|
||||
if (['number', 'integer', 'biginteger', 'float', 'decimal'].includes(type)) {
|
||||
schema = yup
|
||||
.number()
|
||||
.transform(cv => (isNaN(cv) ? undefined : cv))
|
||||
.typeError();
|
||||
}
|
||||
if (['date', 'datetime'].includes(type)) {
|
||||
schema = yup.date();
|
||||
}
|
||||
|
||||
Object.keys(validations).forEach(validation => {
|
||||
const validationValue = validations[validation];
|
||||
if (
|
||||
!!validationValue ||
|
||||
((!isBoolean(validationValue) &&
|
||||
Number.isInteger(Math.floor(validationValue))) ||
|
||||
validationValue === 0)
|
||||
) {
|
||||
switch (validation) {
|
||||
case 'required':
|
||||
schema = schema.required(errorsTrads.required);
|
||||
break;
|
||||
case 'max':
|
||||
schema = schema.max(validationValue, errorsTrads.max);
|
||||
break;
|
||||
case 'maxLength':
|
||||
schema = schema.max(validationValue, errorsTrads.maxLength);
|
||||
break;
|
||||
case 'min':
|
||||
schema = schema.min(validationValue, errorsTrads.min);
|
||||
break;
|
||||
case 'minLength':
|
||||
schema = schema.min(validationValue, errorsTrads.minLength);
|
||||
break;
|
||||
case 'regex':
|
||||
schema = schema.matches(validationValue, errorsTrads.regex);
|
||||
break;
|
||||
case 'lowercase':
|
||||
if (['text', 'textarea', 'email', 'string'].includes(type)) {
|
||||
schema = schema.strict().lowercase();
|
||||
}
|
||||
break;
|
||||
case 'uppercase':
|
||||
if (['text', 'textarea', 'email', 'string'].includes(type)) {
|
||||
schema = schema.strict().uppercase();
|
||||
}
|
||||
break;
|
||||
case 'positive':
|
||||
if (
|
||||
['number', 'integer', 'bigint', 'float', 'decimal'].includes(type)
|
||||
) {
|
||||
schema = schema.positive();
|
||||
}
|
||||
break;
|
||||
case 'negative':
|
||||
if (
|
||||
['number', 'integer', 'bigint', 'float', 'decimal'].includes(type)
|
||||
) {
|
||||
schema = schema.negative();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
schema = schema.nullable();
|
||||
}
|
||||
}
|
||||
});
|
||||
return schema;
|
||||
};
|
||||
|
||||
export default createYupSchema;
|
||||
Loading…
x
Reference in New Issue
Block a user