Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add notifications new signature #6671

Merged
merged 7 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,11 @@ Here's how to migrate the *Altering the Form Values before Submitting* example f
import * as React from 'react';
import { useCallback } from 'react';
import { useForm } from 'react-final-form';
import { SaveButton, Toolbar, useCreate, useRedirect, useNotify } from 'react-admin';
import { SaveButton, Toolbar, useCreate, useRedirect } from 'react-admin';

const SaveWithNoteButton = ({ handleSubmit, handleSubmitWithRedirect, ...props }) => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath, redirect } = props;

const form = useForm();
Expand Down Expand Up @@ -171,9 +170,7 @@ const SaveWithNoteButton = props => {
},
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
notify('ra.notification.created', { messageArgs: { smart_count: 1 } });
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
Expand Down Expand Up @@ -472,7 +469,7 @@ const ExportButton = ({ sort, filter, maxResults = 1000, resource }) => {
const payload = { sort, filter, pagination: { page: 1, perPage: maxResults }}
const handleClick = dataProvider.getList(resource, payload)
.then(({ data }) => jsonExport(data, (err, csv) => downloadCSV(csv, resource)))
.catch(error => notify('ra.notification.http_error', 'warning'));
.catch(error => notify('ra.notification.http_error', { type: 'warning'}));

return (
<Button
Expand Down
39 changes: 25 additions & 14 deletions docs/Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ const ApproveButton = ({ record }) => {
})
.catch(error => {
// failure side effects go here
notify(`Comment approval error: ${error.message}`, 'warning');
notify(`Comment approval error: ${error.message}`, { type: 'warning' });
});

return <Button label="Approve" onClick={approve} disabled={loading} />;
Expand All @@ -635,7 +635,7 @@ import { useNotify } from 'react-admin';
const NotifyButton = () => {
const notify = useNotify();
const handleClick = () => {
notify(`Comment approved`, 'success');
notify(`Comment approved`, { type: 'success' });
}
return <button onClick={handleClick}>Notify</button>;
};
Expand All @@ -651,7 +651,7 @@ The callback takes 6 arguments:

Here are more examples of `useNotify` calls:

```jsx
```js
// notify a warning
notify(`This is a warning`, 'warning');
// pass translation arguments
Expand All @@ -660,6 +660,17 @@ notify('item.created', 'info', { resource: 'post' });
notify('Element updated', 'info', undefined, true);
```

**Tip**: The callback also allows a signature with only 2 arguments, the message to display and an object with the rest of the arguments

```js
// notify an undoable success message, with translation arguments
notify('Element deleted', {
type: 'success',
undoable: true,
messageArgs: { resource: 'post' }
});
```

**Tip**: When using `useNotify` as a side effect for an `undoable` Edit form, you MUST set the fourth argument to `true`, otherwise the "undo" button will not appear, and the actual update will never occur.

```jsx
Expand All @@ -670,7 +681,7 @@ const PostEdit = props => {
const notify = useNotify();

const onSuccess = () => {
notify(`Changes saved`, undefined, undefined, true);
notify('Changes saved`', { undoable: true });
};

return (
Expand Down Expand Up @@ -792,7 +803,7 @@ const ApproveButton = ({ record }) => {
redirect('/comments');
notify('Comment approved');
},
onFailure: (error) => notify(`Comment approval error: ${error.message}`, 'warning'),
onFailure: (error) => notify(`Comment approval error: ${error.message}`, { type: 'warning' }),
}
);
return <Button label="Approve" onClick={approve} disabled={loading} />;
Expand Down Expand Up @@ -842,9 +853,9 @@ const ApproveButton = ({ record }) => {
+ onSuccess: () => {
redirect('/comments');
- notify('Comment approved');
+ notify('Comment approved', 'info', {}, true);
+ notify('Comment approved', { undoable: true });
},
onFailure: (error) => notify(`Error: ${error.message}`, 'warning'),
onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
}
);
return <Button label="Approve" onClick={approve} disabled={loading} />;
Expand All @@ -871,9 +882,9 @@ const ApproveButton = ({ record }) => {
mutationMode: 'undoable',
onSuccess: () => {
redirect('/comments');
notify('Comment approved', 'info', {}, true);
notify('Comment approved', { undoable: true });
},
onFailure: (error) => notify(`Error: ${error.message}`, 'warning'),
onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
}
);
return <Button label="Approve" onClick={approve} disabled={loading} />;
Expand Down Expand Up @@ -906,9 +917,9 @@ const ApproveButton = ({ record }) => {
mutationMode: 'undoable',
onSuccess: ({ data }) => {
redirect('/comments');
notify('Comment approved', 'info', {}, true);
notify('Comment approved', { undoable: true });
},
onFailure: (error) => notify(`Error: ${error.message}`, 'warning'),
onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
}
);
return <Button label="Approve" onClick={approve} disabled={loading} />;
Expand Down Expand Up @@ -988,10 +999,10 @@ const ApproveButton = ({ record }) => {
const options = {
mutationMode: 'undoable',
onSuccess: ({ data }) => {
notify('Comment approved', 'info', {}, true);
notify('Comment approved', { undoable: true });
redirect('/comments');
},
onFailure: (error) => notify(`Error: ${error.message}`, 'warning'),
onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
};
return (
<Mutation
Expand Down Expand Up @@ -1081,7 +1092,7 @@ const ApproveButton = ({ record }) => {
redirect('/comments');
})
.catch((e) => {
notify('Error: comment not approved', 'warning')
notify('Error: comment not approved', { type: 'warning' })
})
.finally(() => {
setLoading(false);
Expand Down
31 changes: 16 additions & 15 deletions docs/CreateEdit.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ You can customize the `<Create>` and `<Edit>` components using the following pro

### CSS API

The `<Create>` and `<Edit>` components accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/components/#overriding-styles-with-classes)). This property accepts the following keys:
The `<Create>` and `<Edit>` components accepts the usual `className` prop, but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/components/#overriding-styles-with-classes)). This property accepts the following keys:

| Rule name | Description |
| ----------- | ------------------------------------------------------------------------------------------ |
Expand Down Expand Up @@ -401,7 +401,7 @@ const PostEdit = props => {
const redirect = useRedirect();

const onSuccess = () => {
notify(`Changes saved`)
notify(`Changes saved`);
redirect('/posts');
refresh();
};
Expand All @@ -423,13 +423,16 @@ The default `onSuccess` function is:
```jsx
// for the <Create> component:
() => {
notify('ra.notification.created', 'info', { smart_count: 1 });
notify('ra.notification.created', { messageArgs: { smart_count: 1 } });
redirect('edit', basePath, data.id, data);
}

// for the <Edit> component:
() => {
notify('ra.notification.updated', 'info', { smart_count: 1 }, mutationMode === 'undoable');
notify('ra.notification.created', {
messageArgs: { smart_count: 1 },
undoable: mutationMode === 'undoable'
});
redirect('list', basePath, data.id, data);
}
```
Expand All @@ -448,7 +451,7 @@ const PostEdit = props => {
const redirect = useRedirect();

const onSuccess = ({ data }) => {
notify(`Changes to post "${data.title}" saved`)
notify(`Changes to post "${data.title}" saved`);
redirect('/posts');
refresh();
};
Expand Down Expand Up @@ -483,7 +486,7 @@ const PostEdit = props => {
const redirect = useRedirect();

const onFailure = (error) => {
notify(`Could not edit post: ${error.message}`)
notify(`Could not edit post: ${error.message}`);
redirect('/posts');
refresh();
};
Expand All @@ -505,12 +508,12 @@ The default `onFailure` function is:
```jsx
// for the <Create> component:
(error) => {
notify(typeof error === 'string' ? error : error.message || 'ra.notification.http_error', 'warning');
notify(typeof error === 'string' ? error : error.message || 'ra.notification.http_error', { type: 'warning' });
}

// for the <Edit> component:
(error) => {
notify(typeof error === 'string' ? error : error.message || 'ra.notification.http_error', 'warning');
notify(typeof error === 'string' ? error : error.message || 'ra.notification.http_error', { type: 'warning' });
if (mutationMode === 'undoable' || mutationMode === 'pessimistic') {
refresh();
}
Expand Down Expand Up @@ -1387,7 +1390,7 @@ export default {

See the [Translation documentation](Translation.md#translation-messages) for details.

**Tip**: Make sure to define validation functions or array of functions in a variable outside of your component, instead of defining them directly in JSX. This can result in a new function or array at every render, and trigger infinite rerender.
**Tip**: Make sure to define validation functions or array of functions in a variable outside your component, instead of defining them directly in JSX. This can result in a new function or array at every render, and trigger infinite rerender.

{% raw %}
```jsx
Expand Down Expand Up @@ -1702,7 +1705,7 @@ Here are the props received by the `Toolbar` component when passed as the `toolb

### CSS API

The `<Toolbar>` accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/components/#overriding-styles-with-classes)). This property accepts the following keys:
The `<Toolbar>` accepts the usual `className` prop, but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/components/#overriding-styles-with-classes)). This property accepts the following keys:

| Rule name | Description |
| ---------------- | ------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -2224,9 +2227,9 @@ const PostEdit = props => {
const redirect = useRedirect();
const onFailure = (error) => {
if (error.code == 123) {
notify('Could not save changes: concurrent edition in progress', 'warning');
notify('Could not save changes: concurrent edition in progress', { type: 'warning' });
} else {
notify('ra.notification.http_error', 'warning')
notify('ra.notification.http_error', { type: 'warning' });
}
redirect('list', props.basePath);
}
Expand Down Expand Up @@ -2325,9 +2328,7 @@ const SaveWithNoteButton = props => {
},
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
notify('ra.notification.created', { messageArgs: { smart_count: 1 } });
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
Expand Down
8 changes: 4 additions & 4 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ const CustomResetViewsButton = ({ selectedIds }) => {
notify('Posts updated');
unselectAll('posts');
},
onFailure: error => notify('Error: posts not updated', 'warning'),
onFailure: error => notify('Error: posts not updated', { type: 'warning' }),
}
);

Expand Down Expand Up @@ -427,7 +427,7 @@ const CustomResetViewsButton = ({ selectedIds }) => {
notify('Posts updated');
unselectAll('posts');
},
onFailure: error => notify('Error: posts not updated', 'warning'),
onFailure: error => notify('Error: posts not updated', { type: 'warning' }),
}
);
const handleClick = () => setOpen(true);
Expand Down Expand Up @@ -489,10 +489,10 @@ const CustomResetViewsButton = ({ selectedIds }) => {
onSuccess: () => {
refresh();
- notify('Posts updated');
+ notify('Posts updated', 'info', '{}, true); // the last argument forces the display of 'undo' in the notification
+ notify('Posts updated', { undoable: true }); // the last argument forces the display of 'undo' in the notification
unselectAll('posts');
},
onFailure: error => notify('Error: posts not updated', 'warning'),
onFailure: error => notify('Error: posts not updated', { type: 'warning' }),
+ mutationMode: 'undoable'
}
);
Expand Down
18 changes: 15 additions & 3 deletions packages/ra-core/src/sideEffect/useNotify.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ const Notification = ({
message,
undoable = false,
autoHideDuration = 4000,
multiLine = true,
multiLine = false,
shortSignature = false,
}) => {
const notify = useNotify();
useEffect(() => {
if (shortSignature) {
// @ts-ignore
notify(message, {
type,
undoable,
Expand All @@ -28,8 +27,8 @@ const Notification = ({
message,
undoable,
autoHideDuration,
multiLine,
shortSignature,
multiLine,
type,
notify,
]);
Expand All @@ -48,4 +47,17 @@ describe('useNotify', () => {

expect(dispatch).toHaveBeenCalledTimes(1);
});

it('should show a notification message of type "warning"', () => {
const { dispatch } = renderWithRedux(
<Notification
type="warning"
message="Notification message"
autoHideDuration={4000}
shortSignature
/>
);

expect(dispatch).toHaveBeenCalledTimes(1);
});
});
31 changes: 22 additions & 9 deletions packages/ra-core/src/sideEffect/useNotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useDispatch } from 'react-redux';
import {
showNotification,
NotificationType,
NotificationOptions,
} from '../actions/notificationActions';
import warning from '../util/warning';

/**
* Hook for Notification Side Effect
Expand All @@ -25,20 +27,31 @@ const useNotify = () => {
return useCallback(
(
message: string,
type: NotificationType = 'info',
type?:
| NotificationType
| (NotificationOptions & { type: NotificationType }),
messageArgs: any = {},
undoable: boolean = false,
autoHideDuration?: number,
multiLine?: boolean
) => {
dispatch(
showNotification(message, type, {
messageArgs,
undoable,
autoHideDuration,
multiLine,
})
);
if (typeof type === 'string') {
warning(
true,
'This way of calling useNotify callback is deprecated. Please use the new syntax passing notify("[Your message]", { ...restOfArguments })'
);
dispatch(
showNotification(message, type || 'info', {
messageArgs,
undoable,
autoHideDuration,
multiLine,
})
);
} else {
const { type: messageType, ...options } = type;
dispatch(showNotification(message, messageType, options));
}
},
[dispatch]
);
Expand Down