Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4714fd4

Browse files
committedJan 19, 2021
Return the save promise in the submit function
1 parent 9f6e81a commit 4714fd4

20 files changed

+157
-52
lines changed
 

‎docs/CreateEdit.md

+42-1
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ export const PostCreate = (props) => (
704704
);
705705
```
706706

707-
**Tip**: `Create` and `Edit` inject more props to their child. So `SimpleForm` also expects these props to be set (but you shouldn't set them yourself):
707+
**Tip**: `Create` and `Edit` inject more props to their child. So `SimpleForm` also expects these props to be set (you should set them yourself only in particular cases like the [submission validation](#submission-validation)):
708708

709709
* `save`: The function invoked when the form is submitted.
710710
* `saving`: A boolean indicating whether a save operation is ongoing.
@@ -1251,6 +1251,47 @@ export const ProductEdit = ({ ...props }) => (
12511251

12521252
**Tip**: You can use *both* Form validation and input validation.
12531253

1254+
## Submission Validation
1255+
1256+
The form can be validated by the server after its submission. In order to display the validation errors, a custom `save` function needs to be used:
1257+
1258+
{% raw %}
1259+
```jsx
1260+
import { useMutation } from 'react-admin';
1261+
1262+
export const UserCreate = (props) => {
1263+
const [mutate] = useMutation();
1264+
const save = useCallback(
1265+
async (values) => {
1266+
try {
1267+
await mutate({
1268+
type: 'create',
1269+
resource: 'users',
1270+
payload: { data: values },
1271+
}, { returnPromise: true });
1272+
} catch (error) {
1273+
if (error.body.errors) {
1274+
return error.body.errors;
1275+
}
1276+
}
1277+
},
1278+
[mutate],
1279+
);
1280+
1281+
return (
1282+
<Create undoable={false} {...props}>
1283+
<SimpleForm save={save}>
1284+
<TextInput label="First Name" source="firstName" />
1285+
<TextInput label="Age" source="age" />
1286+
</SimpleForm>
1287+
</Create>
1288+
);
1289+
};
1290+
```
1291+
{% endraw %}
1292+
1293+
**Tip**: The shape of the returned validation errors must correspond to the form: a key needs to match a `source` prop.
1294+
12541295
## Submit On Enter
12551296

12561297
By default, pressing `ENTER` in any of the form fields submits the form - this is the expected behavior in most cases. However, some of your custom input components (e.g. Google Maps widget) may have special handlers for the `ENTER` key. In that case, to disable the automated form submission on enter, set the `submitOnEnter` prop of the form component to `false`:

‎packages/ra-core/src/dataProvider/Mutation.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface Props {
1616
event?: any,
1717
callTimePayload?: any,
1818
callTimeOptions?: any
19-
) => void,
19+
) => void | Promise<any>,
2020
params: ChildrenFuncParams
2121
) => JSX.Element;
2222
type: string;
@@ -35,6 +35,7 @@ interface Props {
3535
* @param {Object} options
3636
* @param {string} options.action Redux action type
3737
* @param {boolean} options.undoable Set to true to run the mutation locally before calling the dataProvider
38+
* @param {boolean} options.returnPromise Set to true to return the result promise of the mutation
3839
* @param {Function} options.onSuccess Side effect function to be executed upon success or failure, e.g. { onSuccess: response => refresh() } }
3940
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) } }
4041
*

‎packages/ra-core/src/dataProvider/useMutation.spec.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,42 @@ describe('useMutation', () => {
178178
expect(action.meta.resource).toBeUndefined();
179179
expect(dataProvider.mytype).toHaveBeenCalledWith(myPayload);
180180
});
181+
182+
it('should return a promise to dispatch a fetch action when returnPromise option is set and the mutation callback is triggered', async () => {
183+
const dataProvider = {
184+
mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
185+
};
186+
187+
let promise = null;
188+
const myPayload = {};
189+
const { getByText, dispatch } = renderWithRedux(
190+
<DataProviderContext.Provider value={dataProvider}>
191+
<Mutation
192+
type="mytype"
193+
resource="myresource"
194+
payload={myPayload}
195+
options={{ returnPromise: true }}
196+
>
197+
{(mutate, { loading }) => (
198+
<button
199+
className={loading ? 'loading' : 'idle'}
200+
onClick={() => (promise = mutate())}
201+
>
202+
Hello
203+
</button>
204+
)}
205+
</Mutation>
206+
</DataProviderContext.Provider>
207+
);
208+
const buttonElement = getByText('Hello');
209+
fireEvent.click(buttonElement);
210+
const action = dispatch.mock.calls[0][0];
211+
expect(action.type).toEqual('CUSTOM_FETCH');
212+
expect(action.payload).toEqual(myPayload);
213+
expect(action.meta.resource).toEqual('myresource');
214+
await waitForDomChange({ container: buttonElement });
215+
expect(promise).toBeInstanceOf(Promise);
216+
const result = await promise;
217+
expect(result).toMatchObject({ data: { foo: 'bar' } });
218+
});
181219
});

‎packages/ra-core/src/dataProvider/useMutation.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import useDataProviderWithDeclarativeSideEffects from './useDataProviderWithDecl
3131
* @param {Object} options
3232
* @param {string} options.action Redux action type
3333
* @param {boolean} options.undoable Set to true to run the mutation locally before calling the dataProvider
34+
* @param {boolean} options.returnPromise Set to true to return the result promise of the mutation
3435
* @param {Function} options.onSuccess Side effect function to be executed upon success or failure, e.g. { onSuccess: response => refresh() } }
3536
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) } }
3637
* @param {boolean} options.withDeclarativeSideEffectsSupport Set to true to support legacy side effects (e.g. { onSuccess: { refresh: true } })
@@ -52,6 +53,7 @@ import useDataProviderWithDeclarativeSideEffects from './useDataProviderWithDecl
5253
* - {Object} options
5354
* - {string} options.action Redux action type
5455
* - {boolean} options.undoable Set to true to run the mutation locally before calling the dataProvider
56+
* - {boolean} options.returnPromise Set to true to return the result promise of the mutation
5557
* - {Function} options.onSuccess Side effect function to be executed upon success or failure, e.g. { onSuccess: response => refresh() } }
5658
* - {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) } }
5759
* - {boolean} withDeclarativeSideEffectsSupport Set to true to support legacy side effects (e.g. { onSuccess: { refresh: true } })
@@ -140,7 +142,7 @@ const useMutation = (
140142
(
141143
callTimeQuery?: Mutation | Event,
142144
callTimeOptions?: MutationOptions
143-
): void => {
145+
): void | Promise<any> => {
144146
const finalDataProvider = hasDeclarativeSideEffectsSupport(
145147
options,
146148
callTimeOptions
@@ -156,21 +158,27 @@ const useMutation = (
156158

157159
setState(prevState => ({ ...prevState, loading: true }));
158160

159-
finalDataProvider[params.type]
161+
const returnPromise = params.options.returnPromise;
162+
163+
const promise = finalDataProvider[params.type]
160164
.apply(
161165
finalDataProvider,
162166
typeof params.resource !== 'undefined'
163167
? [params.resource, params.payload, params.options]
164168
: [params.payload, params.options]
165169
)
166-
.then(({ data, total }) => {
170+
.then(response => {
171+
const { data, total } = response;
167172
setState({
168173
data,
169174
error: null,
170175
loaded: true,
171176
loading: false,
172177
total,
173178
});
179+
if (returnPromise) {
180+
return response;
181+
}
174182
})
175183
.catch(errorFromResponse => {
176184
setState({
@@ -180,7 +188,14 @@ const useMutation = (
180188
loading: false,
181189
total: null,
182190
});
191+
if (returnPromise) {
192+
throw errorFromResponse;
193+
}
183194
});
195+
196+
if (returnPromise) {
197+
return promise;
198+
}
184199
},
185200
[
186201
// deep equality, see https://github.com/facebook/react/issues/14476#issuecomment-471199055
@@ -204,13 +219,17 @@ export interface Mutation {
204219
export interface MutationOptions {
205220
action?: string;
206221
undoable?: boolean;
222+
returnPromise?: boolean;
207223
onSuccess?: (response: any) => any | Object;
208224
onFailure?: (error?: any) => any | Object;
209225
withDeclarativeSideEffectsSupport?: boolean;
210226
}
211227

212228
export type UseMutationValue = [
213-
(query?: Partial<Mutation>, options?: Partial<MutationOptions>) => void,
229+
(
230+
query?: Partial<Mutation>,
231+
options?: Partial<MutationOptions>
232+
) => void | Promise<any>,
214233
{
215234
data?: any;
216235
total?: number;

‎packages/ra-core/src/form/FormWithRedirect.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ const FormWithRedirect: FC<FormWithRedirectProps> = ({
142142
finalInitialValues,
143143
values
144144
);
145-
onSave.current(sanitizedValues, finalRedirect);
145+
return onSave.current(sanitizedValues, finalRedirect);
146146
} else {
147-
onSave.current(values, finalRedirect);
147+
return onSave.current(values, finalRedirect);
148148
}
149149
};
150150

‎packages/ra-ui-materialui/src/detail/CreateView.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ export const CreateView = (props: CreateViewProps) => {
6666
? redirect
6767
: children.props.redirect,
6868
resource,
69-
save,
69+
save:
70+
typeof children.props.save === 'undefined'
71+
? save
72+
: children.props.save,
7073
saving,
7174
version,
7275
})}
@@ -76,7 +79,10 @@ export const CreateView = (props: CreateViewProps) => {
7679
basePath,
7780
record,
7881
resource,
79-
save,
82+
save:
83+
typeof children.props.save === 'undefined'
84+
? save
85+
: children.props.save,
8086
saving,
8187
version,
8288
})}

‎packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ const AutocompleteArrayInput: FunctionComponent<
167167
id,
168168
input,
169169
isRequired,
170-
meta: { touched, error },
170+
meta: { touched, error, submitError },
171171
} = useInput({
172172
format,
173173
id: idOverride,
@@ -427,7 +427,7 @@ const AutocompleteArrayInput: FunctionComponent<
427427
},
428428
onFocus,
429429
}}
430-
error={!!(touched && error)}
430+
error={!!(touched && (error || submitError))}
431431
label={
432432
<FieldTitle
433433
label={label}
@@ -448,7 +448,7 @@ const AutocompleteArrayInput: FunctionComponent<
448448
helperText={
449449
<InputHelperText
450450
touched={touched}
451-
error={error}
451+
error={error || submitError}
452452
helperText={helperText}
453453
/>
454454
}

‎packages/ra-ui-materialui/src/input/AutocompleteInput.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ const AutocompleteInput: FunctionComponent<AutocompleteInputProps> = props => {
184184
id,
185185
input,
186186
isRequired,
187-
meta: { touched, error },
187+
meta: { touched, error, submitError },
188188
} = useInput({
189189
format,
190190
id: idOverride,
@@ -484,7 +484,7 @@ const AutocompleteInput: FunctionComponent<AutocompleteInputProps> = props => {
484484
onFocus,
485485
...InputPropsWithoutEndAdornment,
486486
}}
487-
error={!!(touched && error)}
487+
error={!!(touched && (error || submitError))}
488488
label={
489489
<FieldTitle
490490
label={label}
@@ -505,7 +505,7 @@ const AutocompleteInput: FunctionComponent<AutocompleteInputProps> = props => {
505505
helperText={
506506
<InputHelperText
507507
touched={touched}
508-
error={error}
508+
error={error || submitError}
509509
helperText={helperText}
510510
/>
511511
}

‎packages/ra-ui-materialui/src/input/BooleanInput.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const BooleanInput: FunctionComponent<
3434
id,
3535
input: { onChange: finalFormOnChange, type, value, ...inputProps },
3636
isRequired,
37-
meta: { error, touched },
37+
meta: { error, submitError, touched },
3838
} = useInput({
3939
format,
4040
onBlur,
@@ -77,10 +77,10 @@ const BooleanInput: FunctionComponent<
7777
/>
7878
}
7979
/>
80-
<FormHelperText error={!!error}>
80+
<FormHelperText error={!!(error || submitError)}>
8181
<InputHelperText
8282
touched={touched}
83-
error={error}
83+
error={error || submitError}
8484
helperText={helperText}
8585
/>
8686
</FormHelperText>

‎packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const CheckboxGroupInput: FunctionComponent<
144144
id,
145145
input: { onChange: finalFormOnChange, onBlur: finalFormOnBlur, value },
146146
isRequired,
147-
meta: { error, touched },
147+
meta: { error, submitError, touched },
148148
} = useInput({
149149
format,
150150
onBlur,
@@ -195,7 +195,7 @@ const CheckboxGroupInput: FunctionComponent<
195195
<FormControl
196196
component="fieldset"
197197
margin={margin}
198-
error={touched && !!error}
198+
error={touched && !!(error || submitError)}
199199
className={classnames(classes.root, className)}
200200
{...sanitizeRestProps(rest)}
201201
>
@@ -225,7 +225,7 @@ const CheckboxGroupInput: FunctionComponent<
225225
<FormHelperText>
226226
<InputHelperText
227227
touched={touched}
228-
error={error}
228+
error={error || submitError}
229229
helperText={helperText}
230230
/>
231231
</FormHelperText>

‎packages/ra-ui-materialui/src/input/DateInput.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const DateInput: FunctionComponent<
6565
id,
6666
input,
6767
isRequired,
68-
meta: { error, touched },
68+
meta: { error, submitError, touched },
6969
} = useInput({
7070
format,
7171
onBlur,
@@ -85,11 +85,11 @@ const DateInput: FunctionComponent<
8585
variant={variant}
8686
margin={margin}
8787
type="date"
88-
error={!!(touched && error)}
88+
error={!!(touched && (error || submitError))}
8989
helperText={
9090
<InputHelperText
9191
touched={touched}
92-
error={error}
92+
error={error || submitError}
9393
helperText={helperText}
9494
/>
9595
}

‎packages/ra-ui-materialui/src/input/DateTimeInput.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const DateTimeInput: FunctionComponent<
8686
id,
8787
input,
8888
isRequired,
89-
meta: { error, touched },
89+
meta: { error, submitError, touched },
9090
} = useInput({
9191
format,
9292
onBlur,
@@ -106,11 +106,11 @@ const DateTimeInput: FunctionComponent<
106106
{...input}
107107
variant={variant}
108108
margin={margin}
109-
error={!!(touched && error)}
109+
error={!!(touched && (error || submitError))}
110110
helperText={
111111
<InputHelperText
112112
touched={touched}
113-
error={error}
113+
error={error || submitError}
114114
helperText={helperText}
115115
/>
116116
}

0 commit comments

Comments
 (0)
Please sign in to comment.