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

Prepare 2.8.0 #2985

Merged
merged 41 commits into from
Mar 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e0229fc
Add an optional emptyValue to SelectInput
edy Jan 17, 2019
e4fe385
added ability to pass in disableRemove property in SimpleFormIterator…
Feb 4, 2019
1e84c44
use defaultProps
edy Feb 6, 2019
5157995
Refactor Dashboard data fetching
fzaninotto Feb 15, 2019
05eac6d
Add helper
fzaninotto Feb 15, 2019
391785c
add withReduxFetch hoc to allow to fetch data via redux in a promise way
ThieryMichel Feb 15, 2019
79a1235
rename withReduxFetch to withDataProvider and have it mimic dataprovider
ThieryMichel Feb 16, 2019
bcc2b93
Use async await
fzaninotto Feb 18, 2019
325817e
Add documentation
fzaninotto Feb 18, 2019
1a4f998
Add Mutation and Query components
fzaninotto Feb 18, 2019
87fe2ed
Use Mutation in demo
fzaninotto Feb 18, 2019
6d8f741
Document Query and Mutation parmeters
fzaninotto Feb 18, 2019
d58bf33
Fix formatting
fzaninotto Feb 18, 2019
393c10b
Fix TOC
fzaninotto Feb 18, 2019
96bb56f
Proofreading
fzaninotto Feb 18, 2019
aff648f
Fix loading state in Mutation
fzaninotto Feb 20, 2019
885dbc4
Add integration tests for Mutation (WIP)
fzaninotto Feb 21, 2019
6467c44
Use a generic to improve withDataProvider type
fzaninotto Feb 21, 2019
80b38e9
Fix missing required props in Mutation test
fzaninotto Feb 22, 2019
83f5c64
Finish Mutation tests
fzaninotto Feb 22, 2019
8c95cc1
Add tests to Query
fzaninotto Feb 22, 2019
e558e5e
Review
fzaninotto Feb 25, 2019
9238f43
Review
fzaninotto Feb 25, 2019
aab31fe
Merge pull request #2899 from marmelab/dashboard-custom-action
Kmaschta Feb 28, 2019
4d3eb16
Add WithConfirm variants of DeleteButton and BulkDeleteButton
fzaninotto Mar 1, 2019
1b3534f
Make DeleteButton a simple switch
fzaninotto Mar 1, 2019
c771b96
Pass undoable from Edit to Toolbar buttons
fzaninotto Mar 1, 2019
41bff7a
Add docs
fzaninotto Mar 1, 2019
615fab0
Fix after rebase
djhi Mar 4, 2019
ea68291
Fix build
djhi Mar 4, 2019
eeb136d
Merge pull request #2954 from marmelab/backport_master
fzaninotto Mar 4, 2019
0ab72ea
Add BulkDeleteWithConfirm
fzaninotto Mar 4, 2019
cab38f6
Avoid double submission in confiormation dialog
fzaninotto Mar 4, 2019
733c1f2
Fix typo
fzaninotto Mar 4, 2019
50944c5
Merge pull request #2955 from marmelab/withConfirm
Mar 4, 2019
e529326
Describe emptyValue in `<SelectInput>`
edy Mar 4, 2019
1356103
Merge pull request #2850 from travisMichael/disable_certain_fields_in…
fzaninotto Mar 7, 2019
adc653c
Merge pull request #2780 from edy/master
fzaninotto Mar 8, 2019
2ef1fe9
Merge branch 'master' into next
fzaninotto Mar 12, 2019
9cac30a
Fix injected styles stype
fzaninotto Mar 12, 2019
a4dc65a
Fix warning about missing BulkDeleteButton props
fzaninotto Mar 12, 2019
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
462 changes: 285 additions & 177 deletions docs/Actions.md

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions docs/CreateEdit.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Here are all the props accepted by the `<Create>` and `<Edit>` components:
* [`title`](#page-title)
* [`actions`](#actions)
* [`aside`](#aside-component)
* [`undoable`](#undoable) (`<Edit>` only)

Here is the minimal code necessary to display a form to create and edit comments:

Expand Down Expand Up @@ -176,6 +177,53 @@ const Aside = ({ record }) => (

**Tip**: Always test that the `record` is defined before using it, as react-admin starts rendering the UI before the API call is over.

### Undoable

By default, the Save and Delete actions are undoable, i.e. react-admin only sends the related request to the data provider after a short delay, during which the user can cancel the action. This is part of the "optimistic rendering" strategy of react-admin ; it makes the user interactions more reactive.

You can disable this behavior by setting `undoable={false}`. With that setting, clicking on the Delete button displays a confirmation dialog. Both the Save and the Delete actions become blocking, and delay the refresh of the screen until the data provider responds.

```jsx
const PostEdit = props => (
<Edit undoable={false} {...props}>
...
</Edit>
```

**Tip**: If you want a confirmation dialog for the Delete button but don't mind undoable Edits, then pass a [custom toolbar](#toolbar) to the form, as follows:

```jsx
import {
Toolbar,
SaveButton,
DeleteButton,
Edit,
SimpleForm,
} from 'react-admin';
import { withStyles } from '@material-ui/core';

const toolbarStyles = {
toolbar: {
display: 'flex',
justifyContent: 'space-between',
},
};

const CustomToolbar = withStyles(toolbarStyles)(props => (
<Toolbar {...props}>
<SaveButton />
<DeleteButton undoable={false} />
</Toolbar>
));

const PostEdit = props => (
<Edit {...props}>
<SimpleForm toolbar={<CustomToolbar />}>
...
</SimpleForm>
</Edit>
```

## Prefilling a `<Create>` Record

You may need to prepopulate a record based on another one. For that use case, use the `<CloneButton>` component. It expects a `record` and a `basePath` (usually injected to children of `<Datagrid>`, `<SimpleForm>`, `<SimpleShowLayout>`, etc.), so it's as simple to use as a regular field or input.
Expand Down
4 changes: 2 additions & 2 deletions docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -993,10 +993,10 @@ const FullNameField = ({ record }) => <span>{record.first_name} {record.last_nam
<SelectInput source="gender" choices={choices} optionText={<FullNameField />}/>
```

Enabling the `allowEmpty` props adds an empty choice (with `null` value) on top of the options, and makes the value nullable:
Enabling the `allowEmpty` props adds an empty choice (with a default `null` value, which you can overwrite with the `emptyValue` prop) on top of the options, and makes the value nullable:

```jsx
<SelectInput source="category" allowEmpty choices={[
<SelectInput source="category" allowEmpty emptyValue="" choices={[
{ id: 'programming', name: 'Programming' },
{ id: 'lifestyle', name: 'Lifestyle' },
{ id: 'photography', name: 'Photography' },
Expand Down
24 changes: 18 additions & 6 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -709,31 +709,43 @@

<li class="chapter {% if page.path == 'Actions.md' %}active{% endif %}">
<a href="./Actions.html">
<b>14.</b> Writing Actions
<b>14.</b> Querying the API
</a>
<ul class="articles" {% if page.path !='Actions.md' %}style="display:none" {% endif %}>
<li class="chapter">
<a href="#the-simple-way">The Simple Way</a>
<a href="#the-basic-way-using-fetch">The Basic Way: Using <code>fetch</code></a>
</li>
<li class="chapter">
<a href="#using-a-data-provider-instead-of-fetch">Using a Data Provider</a>
<a href="#using-the-data-provider-instead-of-fetch">Using the <code>dataProvider</code></a>
</li>
<li class="chapter">
<a href="#using-a-custom-action-creator">Using a Custom Action Creator</a>
<a href="#using-the-withdataprovider-decorator">Using <code>withDataProvider</code></a>
</li>
<li class="chapter">
<a href="#handling-side-effects">Handling Side Effects</a>
</li>
<li class="chapter">
<a href="#success-and-failure-side-effects">Success and Failure Side Effects</a>
<a href="#optimistic-rendering-and-undo">Optimistic Rendering and Undo</a>
</li>
<li class="chapter">
<a href="#optimistic-rendering-and-undo">Optimistic Rendering and Undo</a>
<a href="#query-and-mutation-components"><code>&lt;Query&gt;</code> and <code>&lt;Mutation&gt;</code></a>
</li>
<li class="chapter">
<a href="#using-a-custom-action-creator">Using a Custom Action Creator</a>
</li>
<li class="chapter">
<a href="#adding-side-effects-to-actions">Adding Side Effects to Actions</a>
</li>
<li class="chapter">
<a href="#making-an-action-undoable">Undoable Action</a>
</li>
<li class="chapter">
<a href="#altering-the-form-values-before-submitting">Altering the Form Values before
Submitting</a>
</li>
<li class="chapter">
<a href="#custom-side-effects">Custom Side Effects</a>
</li>
<li class="chapter">
<a href="#custom-sagas">Custom Sagas</a>
</li>
Expand Down
224 changes: 115 additions & 109 deletions examples/demo/src/dashboard/Dashboard.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { Component } from 'react';
import { GET_LIST, GET_MANY, Responsive } from 'react-admin';
import { GET_LIST, GET_MANY, Responsive, withDataProvider } from 'react-admin';
import compose from 'recompose/compose';
import { connect } from 'react-redux';

import Welcome from './Welcome';
import MonthlyRevenue from './MonthlyRevenue';
import NbNewOrders from './NbNewOrders';
import PendingOrders from './PendingOrders';
import PendingReviews from './PendingReviews';
import NewCustomers from './NewCustomers';
import dataProviderFactory from '../dataProvider';

const styles = {
flex: { display: 'flex' },
Expand All @@ -21,118 +22,116 @@ class Dashboard extends Component {
state = {};

componentDidMount() {
this.fetchData();
}

componentDidUpdate(prevProps) {
// handle refresh
if (this.props.version !== prevProps.version) {
this.fetchData();
}
}

fetchData() {
this.fetchOrders();
this.fetchReviews();
this.fetchCustomers();
}

async fetchOrders() {
const { dataProvider } = this.props;
const aMonthAgo = new Date();
aMonthAgo.setDate(aMonthAgo.getDate() - 30);
const { data: recentOrders } = await dataProvider(
GET_LIST,
'commands',
{
filter: { date_gte: aMonthAgo.toISOString() },
sort: { field: 'date', order: 'DESC' },
pagination: { page: 1, perPage: 50 },
}
);
const aggregations = recentOrders
.filter(order => order.status !== 'cancelled')
.reduce(
(stats, order) => {
if (order.status !== 'cancelled') {
stats.revenue += order.total;
stats.nbNewOrders++;
}
if (order.status === 'ordered') {
stats.pendingOrders.push(order);
}
return stats;
},
{
revenue: 0,
nbNewOrders: 0,
pendingOrders: [],
}
);
this.setState({
revenue: aggregations.revenue.toLocaleString(undefined, {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}),
nbNewOrders: aggregations.nbNewOrders,
pendingOrders: aggregations.pendingOrders,
});
const { data: customers } = await dataProvider(GET_MANY, 'customers', {
ids: aggregations.pendingOrders.map(order => order.customer_id),
});
this.setState({
pendingOrdersCustomers: customers.reduce((prev, customer) => {
prev[customer.id] = customer; // eslint-disable-line no-param-reassign
return prev;
}, {}),
});
}

dataProviderFactory(process.env.REACT_APP_DATA_PROVIDER).then(
dataProvider => {
dataProvider(GET_LIST, 'commands', {
filter: { date_gte: aMonthAgo.toISOString() },
sort: { field: 'date', order: 'DESC' },
pagination: { page: 1, perPage: 50 },
})
.then(response =>
response.data
.filter(order => order.status !== 'cancelled')
.reduce(
(stats, order) => {
if (order.status !== 'cancelled') {
stats.revenue += order.total;
stats.nbNewOrders++;
}
if (order.status === 'ordered') {
stats.pendingOrders.push(order);
}
return stats;
},
{
revenue: 0,
nbNewOrders: 0,
pendingOrders: [],
}
)
)
.then(({ revenue, nbNewOrders, pendingOrders }) => {
this.setState({
revenue: revenue.toLocaleString(undefined, {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}),
nbNewOrders,
pendingOrders,
});
return pendingOrders;
})
.then(pendingOrders =>
pendingOrders.map(order => order.customer_id)
)
.then(customerIds =>
dataProvider(GET_MANY, 'customers', {
ids: customerIds,
})
)
.then(response => response.data)
.then(customers =>
customers.reduce((prev, customer) => {
prev[customer.id] = customer; // eslint-disable-line no-param-reassign
return prev;
}, {})
)
.then(customers =>
this.setState({ pendingOrdersCustomers: customers })
);

dataProvider(GET_LIST, 'reviews', {
filter: { status: 'pending' },
sort: { field: 'date', order: 'DESC' },
pagination: { page: 1, perPage: 100 },
})
.then(response => response.data)
.then(reviews => {
const nbPendingReviews = reviews.reduce(nb => ++nb, 0);
const pendingReviews = reviews.slice(
0,
Math.min(10, reviews.length)
);
this.setState({ pendingReviews, nbPendingReviews });
return pendingReviews;
})
.then(reviews => reviews.map(review => review.customer_id))
.then(customerIds =>
dataProvider(GET_MANY, 'customers', {
ids: customerIds,
})
)
.then(response => response.data)
.then(customers =>
customers.reduce((prev, customer) => {
prev[customer.id] = customer; // eslint-disable-line no-param-reassign
return prev;
}, {})
)
.then(customers =>
this.setState({ pendingReviewsCustomers: customers })
);
async fetchReviews() {
const { dataProvider } = this.props;
const { data: reviews } = await dataProvider(GET_LIST, 'reviews', {
filter: { status: 'pending' },
sort: { field: 'date', order: 'DESC' },
pagination: { page: 1, perPage: 100 },
});
const nbPendingReviews = reviews.reduce(nb => ++nb, 0);
const pendingReviews = reviews.slice(0, Math.min(10, reviews.length));
this.setState({ pendingReviews, nbPendingReviews });
const { data: customers } = await dataProvider(GET_MANY, 'customers', {
ids: pendingReviews.map(review => review.customer_id),
});
this.setState({
pendingReviewsCustomers: customers.reduce((prev, customer) => {
prev[customer.id] = customer; // eslint-disable-line no-param-reassign
return prev;
}, {}),
});
}

dataProvider(GET_LIST, 'customers', {
filter: {
has_ordered: true,
first_seen_gte: aMonthAgo.toISOString(),
},
sort: { field: 'first_seen', order: 'DESC' },
pagination: { page: 1, perPage: 100 },
})
.then(response => response.data)
.then(newCustomers => {
this.setState({ newCustomers });
this.setState({
nbNewCustomers: newCustomers.reduce(nb => ++nb, 0),
});
});
async fetchCustomers() {
const { dataProvider } = this.props;
const aMonthAgo = new Date();
aMonthAgo.setDate(aMonthAgo.getDate() - 30);
const { data: newCustomers } = await dataProvider(
GET_LIST,
'customers',
{
filter: {
has_ordered: true,
first_seen_gte: aMonthAgo.toISOString(),
},
sort: { field: 'first_seen', order: 'DESC' },
pagination: { page: 1, perPage: 100 },
}
);
this.setState({
newCustomers,
nbNewCustomers: newCustomers.reduce(nb => ++nb, 0),
});
}

render() {
Expand Down Expand Up @@ -222,4 +221,11 @@ class Dashboard extends Component {
}
}

export default Dashboard;
const mapStateToProps = state => ({
version: state.admin.ui.viewVersion,
});

export default compose(
connect(mapStateToProps),
withDataProvider
)(Dashboard);
Loading