diff --git a/src/components/react-admin/CustomToolbar.js b/src/components/react-admin/CustomToolbar.js index b2691deb2..163512495 100644 --- a/src/components/react-admin/CustomToolbar.js +++ b/src/components/react-admin/CustomToolbar.js @@ -58,7 +58,7 @@ const CustomToolbar = ({ ); diff --git a/src/components/react-admin/ServerSideSimpleForm.js b/src/components/react-admin/ServerSideSimpleForm.js new file mode 100644 index 000000000..0066ca2a1 --- /dev/null +++ b/src/components/react-admin/ServerSideSimpleForm.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { SimpleForm } from 'react-admin'; +import useSaveWithErrorHandling from '../../hooks/useSaveWithErrorHandling'; + +function ServerSideSimpleForm ({ children, record, resource, ...props }) { + const save = useSaveWithErrorHandling(record.id !== undefined, resource); + + return ( + + {children} + + ); +} + +export default ServerSideSimpleForm; diff --git a/src/components/react-admin/ServerSideTabbedForm.js b/src/components/react-admin/ServerSideTabbedForm.js new file mode 100644 index 000000000..27446376f --- /dev/null +++ b/src/components/react-admin/ServerSideTabbedForm.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { TabbedForm } from 'react-admin'; +import useSaveWithErrorHandling from '../../hooks/useSaveWithErrorHandling'; + +function ServerSideTabbedForm ({ children, record, resource, ...props }) { + const save = useSaveWithErrorHandling(record.id !== undefined, resource); + + return ( + + {children} + + ); +} + +export default ServerSideTabbedForm; diff --git a/src/hooks/useSaveWithErrorHandling.js b/src/hooks/useSaveWithErrorHandling.js new file mode 100644 index 000000000..2ccd0423b --- /dev/null +++ b/src/hooks/useSaveWithErrorHandling.js @@ -0,0 +1,41 @@ +import { useMutation, useNotify, useRedirect } from 'ra-core'; +import { useCallback } from 'react'; + +/** + * Convert the django error response to an object react-admin understands + * + * See https://marmelab.com/react-admin/doc/3.19/CreateEdit.html#server-side-validation + */ +function useSaveWithErrorHandling (edit, resource) { + const [mutate] = useMutation(); + const notify = useNotify(); + const redirect = useRedirect(); + + const save = useCallback( + async values => { + try { + await mutate({ + type: edit ? 'update' : 'create', + resource, + payload: { id: values.id, data: values }, + }, { returnPromise: true }); + redirect(`/${resource}`); + } catch (error) { + const errorData = error.body ?? error.data; + if (error.status === 400 && errorData) { + const errorObject = { ...errorData }; + Object.entries(errorObject).forEach(([key, value]) => { + errorObject[key] = value.reduce((acc, current) => `${acc} ${current}`); + }); + notify('ra.message.invalid_form', { type: 'error' }); + return errorObject; + } + } + return null; + }, + [mutate, edit, resource, notify, redirect], + ); + return save; +} + +export default useSaveWithErrorHandling; diff --git a/src/modules/RA/BaseLayer/components/BaseLayerForm.js b/src/modules/RA/BaseLayer/components/BaseLayerForm.js index ac61295d7..43ab22cf1 100644 --- a/src/modules/RA/BaseLayer/components/BaseLayerForm.js +++ b/src/modules/RA/BaseLayer/components/BaseLayerForm.js @@ -9,7 +9,6 @@ import { required, NumberInput, SelectInput, - SimpleForm, TextInput, SimpleFormIterator, translate, @@ -21,6 +20,7 @@ import ZoomInput from '../../../../components/react-admin/ZoomInput'; import FieldGroup from '../../../../components/react-admin/FieldGroup'; import compose from '../../../../utils/compose'; +import ServerSideSimpleForm from '../../../../components/react-admin/ServerSideSimpleForm'; const styles = { inline: { @@ -93,7 +93,7 @@ const BaseLayerForm = ({ basePath, classes, translate: t, ...props }) => { }, [t, validateURL]); return ( - + { )} - + ); }; export default compose( diff --git a/src/modules/RA/BaseLayer/views/Edit.js b/src/modules/RA/BaseLayer/views/Edit.js index 4b74f16f9..77f3f14c5 100644 --- a/src/modules/RA/BaseLayer/views/Edit.js +++ b/src/modules/RA/BaseLayer/views/Edit.js @@ -6,7 +6,7 @@ import DefaultActions from '../../../../components/react-admin/DefaultActions'; export const BaseLayerEdit = props => ( } > diff --git a/src/modules/RA/Campaign/components/CampaignFields.js b/src/modules/RA/Campaign/components/CampaignFields.js index 34b575466..459e63e22 100644 --- a/src/modules/RA/Campaign/components/CampaignFields.js +++ b/src/modules/RA/Campaign/components/CampaignFields.js @@ -5,7 +5,6 @@ import { FormTab, ReferenceInput, SelectInput, - TabbedForm, TextInput, } from 'react-admin'; @@ -23,6 +22,7 @@ import CampaignState from './CampaignState'; import UserNameField from '../../User/components/UserNameField'; import StatsBar from './StatsBar'; +import ServerSideTabbedForm from '../../../../components/react-admin/ServerSideTabbedForm'; const useStyles = makeStyles({ inline: { @@ -47,7 +47,7 @@ const CampaignFields = ({ edit, location, ...props }) => { const isEditable = edit && state !== 'draft'; return ( - } initialValues={{ viewpoints: [], state: 'draft' }} @@ -95,7 +95,7 @@ const CampaignFields = ({ edit, location, ...props }) => { - + ); }; diff --git a/src/modules/RA/Campaign/views/Edit.js b/src/modules/RA/Campaign/views/Edit.js index 228917d8d..ab4dec856 100644 --- a/src/modules/RA/Campaign/views/Edit.js +++ b/src/modules/RA/Campaign/views/Edit.js @@ -15,10 +15,10 @@ export const CampaignEdit = ({ staticContext, ...props }) => { return ( } > - + ); }; diff --git a/src/modules/RA/DataLayer/components/DataLayerForm.js b/src/modules/RA/DataLayer/components/DataLayerForm.js index 62b40bea7..fab4aad1a 100644 --- a/src/modules/RA/DataLayer/components/DataLayerForm.js +++ b/src/modules/RA/DataLayer/components/DataLayerForm.js @@ -1,6 +1,6 @@ import React from 'react'; -import { ArrayInput, FormTab, SimpleFormIterator, TabbedForm, TabbedFormTabs } from 'react-admin'; +import { ArrayInput, FormTab, SimpleFormIterator, TabbedFormTabs } from 'react-admin'; import CustomFormTab from '../../../../components/react-admin/CustomFormTab'; @@ -15,6 +15,7 @@ import ReportingTab from './tabs/ReportingTab'; import StyleTab from './tabs/StyleTab'; import TableTab from './tabs/TableTab'; import WidgetsTab from './tabs/WidgetsTab/WidgetsTab'; +import ServerSideTabbedForm from '../../../../components/react-admin/ServerSideTabbedForm'; const initialErrorState = { definition: false, @@ -154,7 +155,7 @@ const DataLayerForm = React.memo(props => { }, []); return ( - } sanitizeEmptyValues={false} {...props} @@ -258,7 +259,7 @@ const DataLayerForm = React.memo(props => { > - + ); }); diff --git a/src/modules/RA/DataLayer/views/Edit.js b/src/modules/RA/DataLayer/views/Edit.js index 8f7a3d8eb..5b90d484c 100644 --- a/src/modules/RA/DataLayer/views/Edit.js +++ b/src/modules/RA/DataLayer/views/Edit.js @@ -9,7 +9,7 @@ const EditDataLayerForm = PreventPartialData('description', DataLayerForm); export const DataLayerEdit = props => ( } > diff --git a/src/modules/RA/DataSource/components/DataSourceReadOnlyForm.js b/src/modules/RA/DataSource/components/DataSourceReadOnlyForm.js index 3cf7abd98..f65848f23 100644 --- a/src/modules/RA/DataSource/components/DataSourceReadOnlyForm.js +++ b/src/modules/RA/DataSource/components/DataSourceReadOnlyForm.js @@ -2,14 +2,14 @@ import React from 'react'; import { TextInput, Labeled, - SimpleForm, translate, } from 'react-admin'; import DataSourceMainFields from './DataSourceMainFields'; +import ServerSideSimpleForm from '../../../../components/react-admin/ServerSideSimpleForm'; const DataSourceReadOnlyForm = ({ translate: t, ...props }) => ( - + @@ -22,7 +22,7 @@ const DataSourceReadOnlyForm = ({ translate: t, ...props }) => ( helperText={t('datasource.form.uid-field-help')} fullWidth /> - + ); export default translate(DataSourceReadOnlyForm); diff --git a/src/modules/RA/DataSource/components/DataSourceTabbedForm.js b/src/modules/RA/DataSource/components/DataSourceTabbedForm.js index e4b515786..43d4ff554 100644 --- a/src/modules/RA/DataSource/components/DataSourceTabbedForm.js +++ b/src/modules/RA/DataSource/components/DataSourceTabbedForm.js @@ -1,6 +1,5 @@ import React from 'react'; import { - TabbedForm, TextInput, BooleanInput, SelectInput, @@ -18,6 +17,7 @@ import { fieldTypeChoices } from '..'; import MainTab from './MainTab'; import ReportTab from './ReportTab'; import GeneralInfoTab from './GeneralInfoTab'; +import ServerSideTabbedForm from '../../../../components/react-admin/ServerSideTabbedForm'; const DataSourceTabbedForm = ({ translate: t, ...props }) => { @@ -25,7 +25,7 @@ const DataSourceTabbedForm = ({ translate: t, ...props }) => { // report is null when a source has been created and no refresh has been done const errors = report?.errors ?? []; return ( - + @@ -59,7 +59,7 @@ const DataSourceTabbedForm = ({ translate: t, ...props }) => { - + ); }; diff --git a/src/modules/RA/DataSource/views/Edit.js b/src/modules/RA/DataSource/views/Edit.js index 8c011b060..51de7115a 100644 --- a/src/modules/RA/DataSource/views/Edit.js +++ b/src/modules/RA/DataSource/views/Edit.js @@ -17,7 +17,7 @@ export const DataSourceEdit = props => { return ( } {...props} > diff --git a/src/modules/RA/Picture/components/CustomToolbar.js b/src/modules/RA/Picture/components/CustomToolbar.js index 0cde98e6b..09086f098 100644 --- a/src/modules/RA/Picture/components/CustomToolbar.js +++ b/src/modules/RA/Picture/components/CustomToolbar.js @@ -127,7 +127,7 @@ const CustomToolbar = ({ basePath, redirect, ...props }) => { { const { record: { state } } = props; return ( - } initialValues={{ state: 'draft' }}> + } initialValues={{ state: 'draft' }}> @@ -167,7 +167,7 @@ const PictureFields = ({ edit, mapConfig, location, ...props }) => { style={{ width: '50%' }} /> - + ); }; diff --git a/src/modules/RA/Picture/views/Edit.js b/src/modules/RA/Picture/views/Edit.js index 3f4fab608..31c596d73 100644 --- a/src/modules/RA/Picture/views/Edit.js +++ b/src/modules/RA/Picture/views/Edit.js @@ -14,10 +14,10 @@ export const PictureEdit = ({ staticContext, ...props }) => { return ( } > - + ); }; diff --git a/src/modules/RA/Scene/components/SceneForm.js b/src/modules/RA/Scene/components/SceneForm.js index 6e816779c..990ffca10 100644 --- a/src/modules/RA/Scene/components/SceneForm.js +++ b/src/modules/RA/Scene/components/SceneForm.js @@ -10,7 +10,6 @@ import { ImageField, NumberInput, SelectInput, - SimpleForm, TextInput, FormDataConsumer, required, @@ -27,6 +26,7 @@ import SceneFormNameField from './SceneFormNameField'; import { RES_BASELAYER } from '../../ra-modules'; import MapExtentInput from './MapExtentInput'; +import ServerSideSimpleForm from '../../../../components/react-admin/ServerSideSimpleForm'; const styles = { inline: { @@ -51,14 +51,14 @@ const ReportField = ({ record, source, className, label, ...rest }) => { ); }; -const SceneForm = ({ edit = false, translate: t, classes, ...props }) => { +const SceneForm = ({ translate: t, classes, ...props }) => { const { record } = props; - + const edit = record.id !== undefined; /* sanitizeEmptyValues is false for this form to prevent * this issue with the layer tree https://github.com/marmelab/react-admin/issues/5427 */ return ( - + {edit && } {isObjectEmpty(record) && ( @@ -111,7 +111,7 @@ const SceneForm = ({ edit = false, translate: t, classes, ...props }) => { ()} - + ); }; diff --git a/src/modules/RA/Scene/views/Edit.js b/src/modules/RA/Scene/views/Edit.js index 90a6ea1fa..ef9b183d1 100644 --- a/src/modules/RA/Scene/views/Edit.js +++ b/src/modules/RA/Scene/views/Edit.js @@ -9,12 +9,12 @@ const EditSceneForm = PreventPartialData('config', SceneForm); export const SceneEdit = props => ( } > - + ); diff --git a/src/modules/RA/User/components/UserFields.js b/src/modules/RA/User/components/UserFields.js index b72c1c573..929557be9 100644 --- a/src/modules/RA/User/components/UserFields.js +++ b/src/modules/RA/User/components/UserFields.js @@ -1,7 +1,6 @@ import React from 'react'; import { TextInput, - SimpleForm, BooleanInput, ReferenceArrayInput, SelectArrayInput, @@ -22,6 +21,7 @@ import UserFieldsHelp from './UserFieldsHelp'; import { RES_USERGROUP } from '../../ra-modules'; import UserPasswordInput from './UserPasswordInput'; +import ServerSideSimpleForm from '../../../../components/react-admin/ServerSideSimpleForm'; const useStyles = makeStyles({ inline: { @@ -43,7 +43,7 @@ const UserFields = ({ user: { is_superuser: isSuperUser } = {}, ...props }) => { }; return ( - + { - + ); }; diff --git a/src/modules/RA/User/views/Edit.js b/src/modules/RA/User/views/Edit.js index 7f932991d..32fdc888a 100644 --- a/src/modules/RA/User/views/Edit.js +++ b/src/modules/RA/User/views/Edit.js @@ -10,7 +10,7 @@ export const UserEdit = props => ( } - mutationMode="optimistic" + mutationMode="pessimistic" actions={} > diff --git a/src/modules/RA/UserGroup/components/UserGroupFields.js b/src/modules/RA/UserGroup/components/UserGroupFields.js index 6a14cf13f..da57ffc57 100644 --- a/src/modules/RA/UserGroup/components/UserGroupFields.js +++ b/src/modules/RA/UserGroup/components/UserGroupFields.js @@ -2,28 +2,31 @@ import React from 'react'; import { TextInput, - SimpleForm, ReferenceArrayInput, SelectArrayInput, } from 'react-admin'; import UserNameField from '../../User/components/UserNameField'; import { RES_USER, RES_PERMISSION } from '../../ra-modules'; +import ServerSideSimpleForm from '../../../../components/react-admin/ServerSideSimpleForm'; -const UserGroupFields = ({ edit = false, ...props }) => ( - - {edit && } +const UserGroupFields = ({ record, ...props }) => { + const edit = record.id !== undefined; + return ( + + {edit && } - + - - UserNameField({ record })} /> - + + UserNameField({ record: r })} /> + - - - - -); + + + + + ); +}; export default UserGroupFields; diff --git a/src/modules/RA/UserGroup/views/Edit.js b/src/modules/RA/UserGroup/views/Edit.js index b667f63a2..91c82437a 100644 --- a/src/modules/RA/UserGroup/views/Edit.js +++ b/src/modules/RA/UserGroup/views/Edit.js @@ -7,10 +7,10 @@ import DefaultActions from '../../../../components/react-admin/DefaultActions'; export const UserEdit = props => ( } > - + ); diff --git a/src/modules/RA/Viewpoint/components/ViewpointFields.js b/src/modules/RA/Viewpoint/components/ViewpointFields.js index f062fd0f5..9f931f149 100644 --- a/src/modules/RA/Viewpoint/components/ViewpointFields.js +++ b/src/modules/RA/Viewpoint/components/ViewpointFields.js @@ -23,7 +23,6 @@ import { ReferenceArrayField, required, SimpleFormIterator, - TabbedForm, TextField, SelectInput, TextInput, @@ -45,6 +44,7 @@ import { useGetListAllPages } from '../../../../utils/react-admin/hooks'; import useAppSettings from '../../../../hooks/useAppSettings'; import themeRenderer from './themeRenderer'; +import ServerSideTabbedForm from '../../../../components/react-admin/ServerSideTabbedForm'; const styles = { inline: { @@ -162,7 +162,7 @@ const ViewpointFields = ({ translate: t, edit, classes, mapConfig, record, ...pr }, [getRemoteData]); return ( - )} - + ); }; diff --git a/src/modules/RA/Viewpoint/views/Edit.js b/src/modules/RA/Viewpoint/views/Edit.js index c8cbd4bd1..c85ea4fd3 100644 --- a/src/modules/RA/Viewpoint/views/Edit.js +++ b/src/modules/RA/Viewpoint/views/Edit.js @@ -7,10 +7,10 @@ import DefaultActions from '../../../../components/react-admin/DefaultActions'; export const ViewpointEdit = props => ( } > - + ); diff --git a/src/modules/RA/ViewpointCity/components/CityFields.js b/src/modules/RA/ViewpointCity/components/CityFields.js index 5cd68b37f..2210307a2 100644 --- a/src/modules/RA/ViewpointCity/components/CityFields.js +++ b/src/modules/RA/ViewpointCity/components/CityFields.js @@ -1,17 +1,17 @@ import React from 'react'; import { TextInput, - SimpleForm, } from 'react-admin'; import { connectAuthProvider } from '@terralego/core/modules/Auth'; +import ServerSideSimpleForm from '../../../../components/react-admin/ServerSideSimpleForm'; const UserFields = props => ( - + - + ); export default connectAuthProvider('user')(UserFields); diff --git a/src/modules/RA/ViewpointCity/views/Edit.js b/src/modules/RA/ViewpointCity/views/Edit.js index 42f8e46e6..54dbfeb3b 100644 --- a/src/modules/RA/ViewpointCity/views/Edit.js +++ b/src/modules/RA/ViewpointCity/views/Edit.js @@ -7,10 +7,10 @@ import DefaultActions from '../../../../components/react-admin/DefaultActions'; export const ViewpointEdit = props => ( } > - + ); diff --git a/src/modules/RA/ViewpointTheme/components/ThemeFields.js b/src/modules/RA/ViewpointTheme/components/ThemeFields.js index 34c501194..d1dc66a08 100644 --- a/src/modules/RA/ViewpointTheme/components/ThemeFields.js +++ b/src/modules/RA/ViewpointTheme/components/ThemeFields.js @@ -2,12 +2,12 @@ import React from 'react'; import { TextInput, SelectInput, - SimpleForm, } from 'react-admin'; import { connectAuthProvider } from '@terralego/core/modules/Auth'; import useAppSettings from '../../../../hooks/useAppSettings'; import optionRenderer from './categoryRenderer'; +import ServerSideSimpleForm from '../../../../components/react-admin/ServerSideSimpleForm'; const UserFields = props => { const { modules: @@ -17,7 +17,7 @@ const UserFields = props => { } = {}, } = {} } = useAppSettings(); return ( - + @@ -26,7 +26,7 @@ const UserFields = props => { choices={themeCategories} optionText={optionRenderer} /> - + ); }; diff --git a/src/modules/RA/ViewpointTheme/views/Edit.js b/src/modules/RA/ViewpointTheme/views/Edit.js index 80f3d96ab..63403b017 100644 --- a/src/modules/RA/ViewpointTheme/views/Edit.js +++ b/src/modules/RA/ViewpointTheme/views/Edit.js @@ -7,10 +7,10 @@ import DefaultActions from '../../../../components/react-admin/DefaultActions'; export const ViewpointEdit = props => ( } > - + ); diff --git a/src/services/react-admin/dataProvider.js b/src/services/react-admin/dataProvider.js index 9f40aa670..a380fea26 100644 --- a/src/services/react-admin/dataProvider.js +++ b/src/services/react-admin/dataProvider.js @@ -10,6 +10,11 @@ const httpClient = (url, options = {}) => token: `JWT ${auth.getToken()}`, }, ...options, + }).catch(e => { + if (e.body !== undefined) { + e.message = `Error: ${JSON.stringify(e.body)}`; + } + throw e; }); export default (...args) => drfProvider(Api.host, httpClient)(...args);