From 271e0db8d80694b738e0f76acb0b569fed951a97 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 2 Sep 2019 22:19:25 +0200 Subject: [PATCH 1/5] [BC Break] Migrate ExportButton to hooks --- .../src/button/ExportButton.js | 183 ++++++++---------- 1 file changed, 80 insertions(+), 103 deletions(-) diff --git a/packages/ra-ui-materialui/src/button/ExportButton.js b/packages/ra-ui-materialui/src/button/ExportButton.js index ae4857a8393..761d0deb6b6 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.js +++ b/packages/ra-ui-materialui/src/button/ExportButton.js @@ -1,23 +1,18 @@ -import React, { Component } from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import GetApp from '@material-ui/icons/GetApp'; -import { crudGetAll, downloadCSV, CRUD_GET_MANY, GET_MANY } from 'ra-core'; +import DownloadIcon from '@material-ui/icons/GetApp'; +import { + downloadCSV, + useDataProvider, + useNotify, + GET_MANY, + GET_LIST, +} from 'ra-core'; import jsonExport from 'jsonexport/dist'; import Button from './Button'; -const sanitizeRestProps = ({ - basePath, - crudGetAll, - dispatch, - exporter, - filter, - maxResults, - resource, - sort, - ...rest -}) => rest; +const sanitizeRestProps = ({ basePath, ...rest }) => rest; /** * Extracts, aggregates and deduplicates the ids of related records @@ -62,101 +57,83 @@ export const getRelatedIds = (records, field) => * post_title: posts[record.post_id].title, * })); */ -const fetchRelatedRecords = dispatch => (data, field, resource) => - new Promise((resolve, reject) => { - dispatch({ - type: CRUD_GET_MANY, - payload: { ids: getRelatedIds(data, field) }, - meta: { - resource, - fetch: GET_MANY, - onSuccess: { - callback: ({ payload: { data } }) => { - resolve( - data.reduce((acc, post) => { - acc[post.id] = post; - return acc; - }, {}) - ); - }, - }, - onFailure: { - notification: { - body: 'ra.notification.http_error', - level: 'warning', - }, - callback: ({ error }) => reject(error), - }, - }, - }); - }); - -class ExportButton extends Component { - static propTypes = { - basePath: PropTypes.string, - dispatch: PropTypes.func, - exporter: PropTypes.func, - filter: PropTypes.object, - label: PropTypes.string, - maxResults: PropTypes.number.isRequired, - resource: PropTypes.string.isRequired, - sort: PropTypes.object, - icon: PropTypes.element, - }; +const fetchRelatedRecords = dataProvider => (data, field, resource) => + dataProvider(GET_MANY, resource, { ids: getRelatedIds(data, field) }).then( + ({ data }) => + data.reduce((acc, post) => { + acc[post.id] = post; + return acc; + }, {}) + ); - static defaultProps = { - label: 'ra.action.export', - maxResults: 1000, - icon: , - }; +const DefaultIcon = ; - handleClick = () => { - const { - dispatch, - exporter, - filter, - maxResults, +const ExportButton = ({ + exporter, + sort, + filter, + maxResults = 1000, + resource, + onClick, + label = 'ra.action.export', + icon = DefaultIcon, + ...rest +}) => { + const dataProvider = useDataProvider(); + const notify = useNotify(); + const handleClick = useCallback(() => { + dataProvider(GET_LIST, resource, { sort, - resource, - onClick, - } = this.props; - dispatch( - crudGetAll( - resource, - sort, - filter, - maxResults, - ({ payload: { data } }) => - exporter - ? exporter( - data, - fetchRelatedRecords(dispatch), - dispatch - ) - : jsonExport(data, (err, csv) => - downloadCSV(csv, resource) - ) + filter, + pagination: { page: 1, perPage: maxResults }, + }) + .then(({ data }) => + exporter + ? exporter( + data, + fetchRelatedRecords(dataProvider), + dataProvider + ) + : jsonExport(data, (err, csv) => downloadCSV(csv, resource)) ) - ); - + .catch(error => { + console.error(error); + notify('ra.notification.http_error', 'warning'); + }); if (typeof onClick === 'function') { onClick(); } - }; + }, [ + dataProvider, + exporter, + filter, + maxResults, + notify, + onClick, + resource, + sort, + ]); - render() { - const { label, icon, ...rest } = this.props; + return ( + + ); +}; - return ( - - ); - } -} +ExportButton.propTypes = { + basePath: PropTypes.string, + exporter: PropTypes.func, + filter: PropTypes.object, + label: PropTypes.string, + maxResults: PropTypes.number, + resource: PropTypes.string.isRequired, + sort: PropTypes.object, + icon: PropTypes.element, +}; -export default connect()(ExportButton); // inject redux dispatch +export default ExportButton; From adb6a181f65336cf0bdc1fe58011ff0801025538 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 2 Sep 2019 22:44:04 +0200 Subject: [PATCH 2/5] update doc and add upgrade guide --- UPGRADE.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ docs/List.md | 7 +++---- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index ffbb87cfd34..950481fdb6d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -906,3 +906,54 @@ There are two breaking changes in the new ``: - highlightFirstSuggestion={true} /> ``` + +## The `exporter` function has changed signature + +In a `List`, you can pass a custom `exporter` function to control the data downloaded by users when they cllick on the "Export" button. + +```jsx +const CommentList = props => ( + + // ... + +) +``` + +In react-admin v3, you can styll pass an `exporter` function this way, but its signature has changed: + +```diff +-const exportComments = (data, fetchRelaterRecords, dispatch) => { ++const exportComments = (data, fetchRelaterRecords, dataProvider) => { + // ... +} +``` + +If you used `dispatch` to call the dataProvider using an action creator with a `callback` side effect, you will see that the v3 version makes your exporter code much simpler. If you used it to dispatch custom side effects (like notification or redirect), we recommend that you override the `` component completely - it'll be much easier to maintain. + +As a base, here is the simplified `ExportButton` code: + +```jsx +import { + downloadCSV, + useDataProvider, + useNotify, + GET_LIST, +} from 'react-admin'; +import jsonExport from 'jsonexport/dist'; + +const ExportButton = ({ sort, filter, maxResults = 1000, resource }) => { + const dataProvider = useDataProvider(); + const notify = useNotify(); + const payload = { sort, filter, pagination: { page: 1, perPage: maxResults }} + const handleClick = dataProvider(GET_LIST, resource, payload) + .then(({ data }) => jsonExport(data, (err, csv) => downloadCSV(csv, resource))) + .catch(error => notify('ra.notification.http_error', 'warning')); + + return ( +