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] Fix <Query> does not pass total from dataProvider result #3046

Merged
merged 1 commit into from
Mar 25, 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
28 changes: 28 additions & 0 deletions docs/Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,34 @@ const UserProfile = ({ record }) => (
```
{% endraw %}

Or a user list on the dashboard:

{% raw %}
```jsx
const payload = {
pagination: { page: 1, perPage: 10 },
sort: { field: 'username', order: 'ASC' },
};

const UserList = () => (
<Query type="GET_LIST" resource="users" payload={payload}>
{({ data, total, loading, error }) => {
if (loading) { return <Loading />; }
if (error) { return <p>ERROR</p>; }
return (
<div>
<p>Total users: {total}</p>
<ul>
{data.map(user => <li key={user.username}>{user.username}</li>)}
</ul>
</div>
);
}}
</Query>
);
```
{% endraw %}

Just like the `dataProvider` injected prop, the `<Query>` component expects three parameters: `type`, `resource`, and `payload`. It fetches the data provider on mount, and passes the data to its child component once the response from the API arrives.

The `<Query>` component is designed to read data from the API. When calling the API to update ("mutate") data, use the `<Mutation>` component instead. It passes a callback to trigger the API call to its child function. And the `<ApproveButton>` component from previous sections is a great use case for demonstrating `<Mutation>`:
Expand Down
69 changes: 38 additions & 31 deletions packages/ra-core/src/util/Query.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import {
cleanup,
fireEvent,
// @ts-ignore
waitForDomChange,
waitForDomChange
} from 'react-testing-library';
import expect from 'expect';
import Query from './Query';
import CoreAdmin from '../CoreAdmin';
import Resource from '../Resource';
import TestContext from './TestContext';

describe('Mutation', () => {
describe('Query', () => {
afterEach(cleanup);

it('should render its child', () => {
Expand All @@ -36,11 +36,7 @@ describe('Mutation', () => {
{({ store }) => {
dispatchSpy = jest.spyOn(store, 'dispatch');
return (
<Query
type="mytype"
resource="myresource"
payload={myPayload}
>
<Query type="mytype" resource="myresource" payload={myPayload}>
{() => <div>Hello</div>}
</Query>
);
Expand All @@ -59,16 +55,8 @@ describe('Mutation', () => {
const { getByText } = render(
<TestContext>
{() => (
<Query
type="mytype"
resource="myresource"
payload={myPayload}
>
{({ loading }) => (
<div className={loading ? 'loading' : 'idle'}>
Hello
</div>
)}
<Query type="mytype" resource="myresource" payload={myPayload}>
{({ loading }) => <div className={loading ? 'loading' : 'idle'}>Hello</div>}
</Query>
)}
</TestContext>
Expand All @@ -78,16 +66,11 @@ describe('Mutation', () => {

it('should update the data state after a success response', async () => {
const dataProvider = jest.fn();
dataProvider.mockImplementationOnce(() =>
Promise.resolve({ data: { foo: 'bar' } })
);
dataProvider.mockImplementationOnce(() => Promise.resolve({ data: { foo: 'bar' } }));
const Foo = () => (
<Query type="mytype" resource="foo">
{({ loading, data }) => (
<div
data-testid="test"
className={loading ? 'loading' : 'idle'}
>
<div data-testid="test" className={loading ? 'loading' : 'idle'}>
{data ? data.foo : 'no data'}
</div>
)}
Expand All @@ -106,18 +89,42 @@ describe('Mutation', () => {
expect(testElement.className).toEqual('idle');
});

it('should update the error state after an error response', async () => {
it('should return the total prop if available', async () => {
const dataProvider = jest.fn();
dataProvider.mockImplementationOnce(() =>
Promise.reject({ message: 'provider error' })
dataProvider.mockImplementationOnce(() => Promise.resolve({ data: [{ foo: 'bar' }], total: 42 }));

const Foo = () => (
<Query type="mytype" resource="foo">
{({ loading, data, total }) => (
<div data-testid="test" className={loading ? 'loading' : 'idle'}>
{loading ? 'no data' : total}
</div>
)}
</Query>
);

const { getByTestId } = render(
<CoreAdmin dataProvider={dataProvider}>
<Resource name="foo" list={Foo} />
</CoreAdmin>
);

const testElement = getByTestId('test');
expect(testElement.className).toEqual('loading');
expect(testElement.textContent).toBe('no data');

await waitForDomChange({ container: testElement });
expect(testElement.className).toEqual('idle');
expect(testElement.textContent).toEqual('42');
});

it('should update the error state after an error response', async () => {
const dataProvider = jest.fn();
dataProvider.mockImplementationOnce(() => Promise.reject({ message: 'provider error' }));
const Foo = () => (
<Query type="mytype" resource="foo">
{({ loading, error }) => (
<div
data-testid="test"
className={loading ? 'loading' : 'idle'}
>
<div data-testid="test" className={loading ? 'loading' : 'idle'}>
{error ? error.message : 'no data'}
</div>
)}
Expand Down
42 changes: 32 additions & 10 deletions packages/ra-core/src/util/Query.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { Component, ReactNode } from 'react';
import withDataProvider from './withDataProvider';

type DataProviderCallback = (
type: string,
resource: string,
payload?: any,
options?: any
) => Promise<any>;
type DataProviderCallback = (type: string, resource: string, payload?: any, options?: any) => Promise<any>;

interface ChildrenFuncParams {
data?: any;
total?: number;
loading: boolean;
error?: any;
}
Expand All @@ -28,6 +24,7 @@ interface Props extends RawProps {

interface State {
data?: any;
total?: number;
loading: boolean;
error?: any;
}
Expand All @@ -46,27 +43,52 @@ interface State {
* }}
* </Query>
* );
*
* @example
*
* const payload = {
* pagination: { page: 1, perPage: 10 },
* sort: { field: 'username', order: 'ASC' },
* };
* const UserList = () => (
* <Query type="GET_LIST" resource="users" payload={payload}>
* {({ data, total, loading, error }) => {
* if (loading) { return <Loading />; }
* if (error) { return <p>ERROR</p>; }
* return (
* <div>
* <p>Total users: {total}</p>
* <ul>
* {data.map(user => <li key={user.username}>{user.username}</li>)}
* </ul>
* </div>
* );
* }}
* </Query>
* );
*/
class Query extends Component<Props, State> {
state = {
data: null,
total: null,
loading: true,
error: null,
error: null
};

componentDidMount = () => {
const { dataProvider, type, resource, payload, options } = this.props;
dataProvider(type, resource, payload, options)
.then(({ data }) => {
.then(({ data, total }) => {
this.setState({
data,
loading: false,
total,
loading: false
});
})
.catch(error => {
this.setState({
error,
loading: false,
loading: false
});
});
};
Expand Down