Skip to content

Commit

Permalink
Merge pull request #3468 from marmelab/useDataProvider-without-saga
Browse files Browse the repository at this point in the history
[RFR] Use data provider without saga
  • Loading branch information
djhi committed Jul 30, 2019
2 parents db1571a + 1b987c2 commit 1361e47
Show file tree
Hide file tree
Showing 35 changed files with 617 additions and 187 deletions.
164 changes: 164 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,167 @@ const PostList = props => (
// rest of the view
);
```

## New DataProviderContext Requires Custom App Modification

The new dataProvider-related hooks (`useQuery`, `useMutation`, `useDataProvider`, etc.) grab the `dataProvider` instance from a new React context. If you use the `<Admin>` component, your app will continue to work and there is nothing to do, as `<Admin>` now provides that context. But if you use a Custom App, you'll need to set the value of that new `DataProvider` context:

```diff
-import { TranslationProvider, Resource } from 'react-admin';
+import { TranslationProvider, DataProviderContext, Resource } from 'react-admin';

const App = () => (
<Provider
store={createAdminStore({
authProvider,
dataProvider,
i18nProvider,
history,
})}
>
<TranslationProvider>
+ <DataProviderContext.Provider valuse={dataProvider} />
<ThemeProvider>
<Resource name="posts" intent="registration" />
...
<AppBar position="static" color="default">
<Toolbar>
<Typography variant="h6" color="inherit">
My admin
</Typography>
</Toolbar>
</AppBar>
<ConnectedRouter history={history}>
<Switch>
<Route exact path="/" component={Dashboard} />
<Route exact path="/posts" hasCreate render={(routeProps) => <PostList resource="posts" {...routeProps} />} />
<Route exact path="/posts/create" render={(routeProps) => <PostCreate resource="posts" {...routeProps} />} />
<Route exact path="/posts/:id" hasShow render={(routeProps) => <PostEdit resource="posts" {...routeProps} />} />
<Route exact path="/posts/:id/show" hasEdit render={(routeProps) => <PostShow resource="posts" {...routeProps} />} />
...
</Switch>
</ConnectedRouter>
</ThemeProvider>
+ </DataProviderContext.Provider>
</TranslationProvider>
</Provider>
);
```

Note that if you were unit testing controller components, you'll probably need to add a mock `dataProvider` via `<DataProviderContext>` in your tests, too.

## Custom Notification Must Emit UndoEvents

The undo feature is partially implemented in the `Notification` component. If you've overridden that component, you'll have to add a call to `undoableEventEmitter` in case of confirmation and undo:

```diff
// in src/MyNotification.js
import {
hideNotification,
getNotification,
translate,
undo,
complete,
+ undoableEventEmitter,
} from 'ra-core';

class Notification extends React.Component {
state = {
open: false,
};
componentWillMount = () => {
this.setOpenState(this.props);
};
componentWillReceiveProps = nextProps => {
this.setOpenState(nextProps);
};

setOpenState = ({ notification }) => {
this.setState({
open: !!notification,
});
};

handleRequestClose = () => {
this.setState({
open: false,
});
};

handleExited = () => {
const { notification, hideNotification, complete } = this.props;
if (notification && notification.undoable) {
complete();
+ undoableEventEmitter.emit('end', { isUndo: false });
}
hideNotification();
};

handleUndo = () => {
const { undo } = this.props;
undo();
+ undoableEventEmitter.emit('end', { isUndo: true });
};

render() {
const {
undo,
complete,
classes,
className,
type,
translate,
notification,
autoHideDuration,
hideNotification,
...rest
} = this.props;
const {
warning,
confirm,
undo: undoClass, // Rename classes.undo to undoClass in this scope to avoid name conflicts
...snackbarClasses
} = classes;
return (
<Snackbar
open={this.state.open}
message={
notification &&
notification.message &&
translate(notification.message, notification.messageArgs)
}
autoHideDuration={
(notification && notification.autoHideDuration) ||
autoHideDuration
}
disableWindowBlurListener={
notification && notification.undoable
}
onExited={this.handleExited}
onClose={this.handleRequestClose}
ContentProps={{
className: classnames(
classes[(notification && notification.type) || type],
className
),
}}
action={
notification && notification.undoable ? (
<Button
color="primary"
className={undoClass}
size="small"
- onClick={undo}
+ onClick={this.handleUndo}
>
{translate('ra.action.undo')}
</Button>
) : null
}
classes={snackbarClasses}
{...rest}
/>
);
}
}
```
1 change: 1 addition & 0 deletions packages/ra-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"classnames": "~2.2.5",
"connected-react-router": "^6.4.0",
"date-fns": "^1.29.0",
"eventemitter3": "^3.0.0",
"inflection": "~1.12.0",
"lodash": "~4.17.5",
"node-polyglot": "^2.2.2",
Expand Down
77 changes: 40 additions & 37 deletions packages/ra-core/src/CoreAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { History } from 'history';
import { createHashHistory } from 'history';
import { Switch, Route } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import withContext from 'recompose/withContext';

import AuthContext from './auth/AuthContext';
import DataProviderContext from './dataProvider/DataProviderContext';
import createAdminStore from './createAdminStore';
import TranslationProvider from './i18n/TranslationProvider';
import CoreAdminRouter from './CoreAdminRouter';
Expand Down Expand Up @@ -92,6 +92,7 @@ React-admin requires a valid dataProvider function to work.`);
layout,
appLayout,
authProvider,
dataProvider,
children,
customRoutes = [],
dashboard,
Expand All @@ -118,44 +119,46 @@ React-admin requires a valid dataProvider function to work.`);
}

return (
<TranslationProvider>
<ConnectedRouter history={this.history}>
<Switch>
{loginPage !== false && loginPage !== true ? (
<DataProviderContext.Provider value={dataProvider}>
<TranslationProvider>
<ConnectedRouter history={this.history}>
<Switch>
{loginPage !== false && loginPage !== true ? (
<Route
exact
path="/login"
render={props =>
createElement(loginPage, {
...props,
title,
theme,
})
}
/>
) : null}
<Route
exact
path="/login"
render={props =>
createElement(loginPage, {
...props,
title,
theme,
})
}
path="/"
render={props => (
<CoreAdminRouter
layout={appLayout || layout}
catchAll={catchAll}
customRoutes={customRoutes}
dashboard={dashboard}
loading={loading}
logout={logout}
menu={menu}
theme={theme}
title={title}
{...props}
>
{children}
</CoreAdminRouter>
)}
/>
) : null}
<Route
path="/"
render={props => (
<CoreAdminRouter
layout={appLayout || layout}
catchAll={catchAll}
customRoutes={customRoutes}
dashboard={dashboard}
loading={loading}
logout={logout}
menu={menu}
theme={theme}
title={title}
{...props}
>
{children}
</CoreAdminRouter>
)}
/>
</Switch>
</ConnectedRouter>
</TranslationProvider>
</Switch>
</ConnectedRouter>
</TranslationProvider>
</DataProviderContext.Provider>
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useCreateController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useCallback } from 'react';
import inflection from 'inflection';
import { parse } from 'query-string';

import { useCreate } from '../fetch';
import { useCreate } from '../dataProvider';
import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps';
import { Location } from 'history';
import { match as Match } from 'react-router';
Expand Down
25 changes: 17 additions & 8 deletions packages/ra-core/src/controller/useEditController.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('useEditController', () => {
{({ record }) => <div>{record && record.title}</div>}
</EditController>
);
const formResetAction = dispatch.mock.calls[1][0];
const formResetAction = dispatch.mock.calls[3][0];
expect(formResetAction.type).toEqual('@@redux-form/RESET');
expect(formResetAction.meta).toEqual({ form: 'record-form' });
});
Expand All @@ -69,15 +69,17 @@ describe('useEditController', () => {
</EditController>
);
act(() => saveCallback({ foo: 'bar' }));
const crudUpdateAction = dispatch.mock.calls[2][0];
expect(crudUpdateAction.type).toEqual('RA/UNDOABLE');
expect(crudUpdateAction.payload.action.type).toEqual('RA/CRUD_UPDATE');
expect(crudUpdateAction.payload.action.payload).toEqual({
const call = dispatch.mock.calls.find(
params => params[0].type === 'RA/CRUD_UPDATE_OPTIMISTIC'
);
expect(call).not.toBeUndefined();
const crudUpdateAction = call[0];
expect(crudUpdateAction.payload).toEqual({
id: 12,
data: { foo: 'bar' },
previousData: null,
});
expect(crudUpdateAction.payload.action.meta.resource).toEqual('posts');
expect(crudUpdateAction.meta.resource).toEqual('posts');
});

it('should return a save callback when undoable is false', () => {
Expand All @@ -91,8 +93,15 @@ describe('useEditController', () => {
</EditController>
);
act(() => saveCallback({ foo: 'bar' }));
const crudUpdateAction = dispatch.mock.calls[2][0];
expect(crudUpdateAction.type).toEqual('RA/CRUD_UPDATE');
const call = dispatch.mock.calls.find(
params => params[0].type === 'RA/CRUD_UPDATE_OPTIMISTIC'
);
expect(call).toBeUndefined();
const call2 = dispatch.mock.calls.find(
params => params[0].type === 'RA/CRUD_UPDATE'
);
expect(call2).not.toBeUndefined();
const crudUpdateAction = call2[0];
expect(crudUpdateAction.payload).toEqual({
id: 12,
data: { foo: 'bar' },
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useEditController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
useRefresh,
RedirectionSideEffect,
} from '../sideEffect';
import { useGetOne, useUpdate } from '../fetch';
import { useGetOne, useUpdate } from '../dataProvider';
import { useTranslate } from '../i18n';

export interface EditProps {
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SORT_ASC } from '../reducer/admin/resource/list/queryReducer';
import { ListParams } from '../actions/listActions';
import { useNotify } from '../sideEffect';
import { Sort, RecordMap, Identifier } from '../types';
import useGetList from './../fetch/useGetList';
import useGetList from '../dataProvider/useGetList';

export interface ListProps {
// the props you can change
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useShowController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import inflection from 'inflection';
import useVersion from './useVersion';
import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps';
import { Record, Identifier } from '../types';
import { useGetOne } from '../fetch';
import { useGetOne } from '../dataProvider';
import { useTranslate } from '../i18n';
import { useNotify, useRedirect, useRefresh } from '../sideEffect';

Expand Down
7 changes: 7 additions & 0 deletions packages/ra-core/src/dataProvider/DataProviderContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext } from 'react';

import { DataProvider } from '../types';

const DataProviderContext = createContext<DataProvider>(null);

export default DataProviderContext;
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 1361e47

Please sign in to comment.