diff --git a/docs/List.md b/docs/List.md index abc885f95f2..2ea8abb4b78 100644 --- a/docs/List.md +++ b/docs/List.md @@ -548,6 +548,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 4d11f70e564..f453a99776e 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 e13ce87f320..2747a14e14b 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 1a530c54e1e..16030cb2eb1 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 fde215dd410..bebcafc8a0a 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.func, 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 59025e6bb37..8a04fcb2440 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; @@ -25,6 +27,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(