diff --git a/docs/List.md b/docs/List.md
index 40f466d7b70..a20e6fd667b 100644
--- a/docs/List.md
+++ b/docs/List.md
@@ -553,6 +553,33 @@ export const PostList = (props) => (
```
{% endraw %}
+### Specify Sort Order
+
+By default, when the user clicks on a column header, the list becomes sorted in the ascending order. You change this behavior by setting the `sortByOrder` prop to `"DESC"`:
+
+```jsx
+// in src/posts.js
+import React from 'react';
+import { List, Datagrid, TextField } from 'react-admin';
+
+export const PostList = (props) => (
+
+
+
+
+
+ `${record.author.first_name} ${record.author.last_name}`}
+ />
+
+
+
+);
+```
+
### Permanent Filter
You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the REST client in the `filter` props:
diff --git a/examples/simple/src/posts/PostList.js b/examples/simple/src/posts/PostList.js
index fa73cc50071..12b0af22a32 100644
--- a/examples/simple/src/posts/PostList.js
+++ b/examples/simple/src/posts/PostList.js
@@ -143,6 +143,7 @@ const PostList = props => {
@@ -151,7 +152,7 @@ const PostList = props => {
label="resources.posts.fields.commentable_short"
sortable={false}
/>
-
+
{
setFilters: (filters: any, displayedFilters: any) => void;
setPage: (page: number) => void;
setPerPage: (page: number) => void;
- setSort: (sort: string) => void;
+ setSort: (sort: string, order?: string) => void;
showFilter: (filterName: string, defaultValue: any) => void;
total: number;
version: number;
diff --git a/packages/ra-core/src/controller/useListParams.ts b/packages/ra-core/src/controller/useListParams.ts
index e8592514b46..4f000f5f901 100644
--- a/packages/ra-core/src/controller/useListParams.ts
+++ b/packages/ra-core/src/controller/useListParams.ts
@@ -40,7 +40,7 @@ interface Modifiers {
changeParams: (action: any) => void;
setPage: (page: number) => void;
setPerPage: (pageSize: number) => void;
- setSort: (sort: string) => void;
+ setSort: (sort: string, order?: string) => void;
setFilters: (filters: any, displayedFilters: any) => void;
hideFilter: (filterName: string) => void;
showFilter: (filterName: string, defaultValue: any) => void;
@@ -156,8 +156,11 @@ const useListParams = ({
}, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps
const setSort = useCallback(
- (newSort: string) =>
- changeParams({ type: SET_SORT, payload: { sort: newSort } }),
+ (sort: string, order?: string) =>
+ changeParams({
+ type: SET_SORT,
+ payload: { sort, order },
+ }),
requestSignature // eslint-disable-line react-hooks/exhaustive-deps
);
diff --git a/packages/ra-core/src/controller/useSortState.ts b/packages/ra-core/src/controller/useSortState.ts
index a921ff06f77..b55c01e996c 100644
--- a/packages/ra-core/src/controller/useSortState.ts
+++ b/packages/ra-core/src/controller/useSortState.ts
@@ -9,7 +9,7 @@ import { Sort } from '../types';
export interface SortProps {
setSortField: (field: string) => void;
setSortOrder: (order: string) => void;
- setSort: (sort: Sort) => void;
+ setSort: (sort: Sort, order?: string) => void;
sort: Sort;
}
@@ -116,7 +116,8 @@ export default (initialSort: Sort = defaultSort): SortProps => {
return {
setSort: useCallback(
- (sort: Sort) => dispatch({ type: 'SET_SORT', payload: { sort } }),
+ (sort: Sort, order?: string) =>
+ dispatch({ type: 'SET_SORT', payload: { sort, order } }),
[dispatch]
),
setSortField: useCallback(
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
index 3b86007d40e..290c3d117d8 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
@@ -83,6 +83,7 @@ ReferenceArrayField.propTypes = {
reference: PropTypes.string.isRequired,
resource: PropTypes.string,
sortBy: PropTypes.string,
+ sortByOrder: fieldPropTypes.sortByOrder,
source: PropTypes.string.isRequired,
};
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
index ddbb316fae5..7fdfac02aea 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
@@ -15,7 +15,7 @@ import LinearProgress from '../layout/LinearProgress';
import Link from '../Link';
import sanitizeRestProps from './sanitizeRestProps';
import { ClassNameMap } from '@material-ui/styles';
-import { FieldProps, InjectedFieldProps } from './types';
+import { FieldProps, fieldPropTypes, InjectedFieldProps } from './types';
interface ReferenceFieldProps extends FieldProps, InjectedFieldProps {
children: ReactElement;
@@ -118,6 +118,7 @@ ReferenceField.propTypes = {
reference: PropTypes.string.isRequired,
resource: PropTypes.string,
sortBy: PropTypes.string,
+ sortByOrder: fieldPropTypes.sortByOrder,
source: PropTypes.string.isRequired,
translateChoice: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
linkType: PropTypes.oneOfType([
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
index 7c3e2e37e31..c6a8a176292 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
@@ -16,7 +16,7 @@ import {
PaginationProps,
SortProps,
} from 'ra-core';
-import { FieldProps, InjectedFieldProps } from './types';
+import { FieldProps, fieldPropTypes, InjectedFieldProps } from './types';
/**
* Render related records to the current one.
@@ -138,6 +138,7 @@ ReferenceManyField.propTypes = {
reference: PropTypes.string.isRequired,
resource: PropTypes.string,
sortBy: PropTypes.string,
+ sortByOrder: fieldPropTypes.sortByOrder,
source: PropTypes.string.isRequired,
sort: PropTypes.exact({
field: PropTypes.string,
diff --git a/packages/ra-ui-materialui/src/field/sanitizeRestProps.ts b/packages/ra-ui-materialui/src/field/sanitizeRestProps.ts
index 394b46ab5e9..d163631562c 100644
--- a/packages/ra-ui-materialui/src/field/sanitizeRestProps.ts
+++ b/packages/ra-ui-materialui/src/field/sanitizeRestProps.ts
@@ -18,6 +18,7 @@ export default (props: object): object =>
'resource',
'sortable',
'sortBy',
+ 'sortByOrder',
'source',
'textAlign',
'translateChoice',
diff --git a/packages/ra-ui-materialui/src/field/types.ts b/packages/ra-ui-materialui/src/field/types.ts
index a91c40538e7..f7787ea2f91 100644
--- a/packages/ra-ui-materialui/src/field/types.ts
+++ b/packages/ra-ui-materialui/src/field/types.ts
@@ -2,9 +2,11 @@ import { Record } from 'ra-core';
import PropTypes from 'prop-types';
type TextAlign = 'right' | 'left';
+type SortOrder = 'ASC' | 'DESC';
export interface FieldProps {
addLabel?: boolean;
sortBy?: string;
+ sortByOrder?: SortOrder;
source?: string;
label?: string;
sortable?: boolean;
@@ -24,6 +26,7 @@ export interface InjectedFieldProps {
export const fieldPropTypes = {
addLabel: PropTypes.bool,
sortBy: PropTypes.string,
+ sortByOrder: PropTypes.oneOf(['ASC', 'DESC']),
source: PropTypes.string,
label: PropTypes.string,
sortable: PropTypes.bool,
diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
index 1fc31b786ab..0207efddad0 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
@@ -203,7 +203,7 @@ interface ReferenceInputViewProps {
resource: string;
setFilter: (v: string) => void;
setPagination: (pagination: Pagination) => void;
- setSort: (sort: Sort) => void;
+ setSort: (sort: Sort, order?: string) => void;
source: string;
warning?: string;
}
diff --git a/packages/ra-ui-materialui/src/list/Datagrid.js b/packages/ra-ui-materialui/src/list/Datagrid.js
index 970c32efd29..693d03a0e9c 100644
--- a/packages/ra-ui-materialui/src/list/Datagrid.js
+++ b/packages/ra-ui-materialui/src/list/Datagrid.js
@@ -127,7 +127,10 @@ const Datagrid = props => {
const updateSort = useCallback(
event => {
event.stopPropagation();
- setSort(event.currentTarget.dataset.sort);
+ setSort(
+ event.currentTarget.dataset.sort,
+ event.currentTarget.dataset.order
+ );
},
[setSort]
);
diff --git a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js
index e5b3a16d7f6..c7d5c83b0a3 100644
--- a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js
+++ b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.js
@@ -61,6 +61,7 @@ export const DatagridHeaderCell = props => {
}
direction={currentSort.order === 'ASC' ? 'asc' : 'desc'}
data-sort={field.props.sortBy || field.props.source}
+ data-order={field.props.sortByOrder || 'ASC'}
onClick={updateSort}
classes={classes}
>
diff --git a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js
index c056d13f6f0..a89dc98f813 100644
--- a/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js
+++ b/packages/ra-ui-materialui/src/list/DatagridHeaderCell.spec.js
@@ -48,6 +48,42 @@ describe('', () => {
expect(getByTitle('ra.action.sort').dataset.sort).toBe('title');
});
+ it('should be change order when field has a sortByOrder props', () => {
+ const { getByTitle } = render(
+
+
+
+
+ }
+ updateSort={() => true}
+ />
+
+
+
+ );
+ expect(getByTitle('ra.action.sort').dataset.order).toBe('DESC');
+ });
+
+ it('should be keep ASC order when field has not sortByOrder props', () => {
+ const { getByTitle } = render(
+
+
+
+ }
+ updateSort={() => true}
+ />
+
+
+
+ );
+ expect(getByTitle('ra.action.sort').dataset.order).toBe('ASC');
+ });
+
it('should be disabled when field has no sortby and no source', () => {
const { queryAllByTitle } = render(