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

[RFR] Use data provider without saga #3468

Merged
merged 6 commits into from
Jul 30, 2019
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
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;
Loading