From 409c8233d1cc8fccde501a48b40be8068e2be149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Tue, 7 Feb 2023 22:55:33 +0100 Subject: [PATCH 1/3] [Doc] Add StackedFilters chapter --- docs/Features.md | 138 ++++---- docs/FilteringTutorial.md | 262 +++++++++------ docs/ListTutorial.md | 10 +- docs/NumberInput.md | 4 +- docs/Realtime.md | 7 +- docs/SavedQueriesList.md | 8 +- docs/SimpleList.md | 8 +- docs/StackedFilters.md | 299 ++++++++++++++++++ docs/navigation.html | 1 + docs/useStore.md | 4 +- packages/ra-core/src/store/useStore.ts | 4 +- .../src/button/BulkDeleteButton.tsx | 4 +- .../src/button/BulkExportButton.tsx | 4 +- .../src/button/BulkUpdateButton.tsx | 4 +- packages/ra-ui-materialui/src/list/List.tsx | 4 +- .../src/list/SimpleList/SimpleList.tsx | 4 +- .../src/list/datagrid/Datagrid.tsx | 4 +- .../src/list/filter/Filter.tsx | 4 +- 18 files changed, 576 insertions(+), 197 deletions(-) create mode 100644 docs/StackedFilters.md diff --git a/docs/Features.md b/docs/Features.md index b615198c6a6..867347a5476 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -291,94 +291,105 @@ Check the following components to learn more about guessers: In most admin and B2B apps, the most common task is to look for a record. React-admin includes many features to help you **build a user experience that streamlines the search workflow**. -The most basic search feature is [the Filter Button/Form combo](./FilteringTutorial.md#the-filter-buttonform-combo): an inline form displayed on top of the list. Users also see a dropdown button allowing them to add more inputs to that form. - -![List Filters](./img/list_filter.gif) - -This functionality relies on the `` prop: + + + + + + + + + +
+ + Filter Button/Form Combo + + + <FilterList> Sidebar +
+ + + + <StackedFilters> Dialog + + + Global <Search> +
+ +These features rely on powerful components with an intuitive API. For instance, you can set the Filter Button/Form Combo with the `` prop, using the same input components as in edition forms: ```jsx -import { TextInput } from 'react-admin'; +import { List, TextInput } from 'react-admin'; const postFilters = [ , , ]; -export const PostList = (props) => ( - - ... +export const PostList = () => ( + + {/* ... */} ); ``` -An alternative UI to the Filter Button/Form Combo is [the FilterList Sidebar](./FilteringTutorial.md#the-filterlist-sidebar). Similar to what users usually see on e-commerce websites, it's **a panel with many simple filters that can be enabled and combined** using the mouse. The user experience is better than the Button/Form Combo, because the filter values are explicit, and it doesn't require typing anything in a form. But it's a bit less powerful, as only filters with a finite set of values (or intervals) can be used in the ``. +Check the following chapters to learn more about each search and filtering component: -![Filter Sidebar](./img/filter-sidebar.gif) +- [The Filter Button/Form Combo](./FilteringTutorial.md#the-filter-buttonform-combo) +- [``](./FilterList.md) +- [``](./StackedFilters.md) +- [``](./Search.md) -Here is an example FilterList sidebar: +Users often apply the same filters over and over again. Saved Queries **let users save a combination of filters** and sort parameters into a new, personal filter, that persists between sessions. + +[![Saved Queries in FilterList](./img/SavedQueriesList.gif)](./img/SavedQueriesList.gif) + +Here is an example `` sidebar with saved queries: -{% raw %} ```jsx -import { SavedQueriesList, FilterLiveSearch, FilterList, FilterListItem } from 'react-admin'; +import { FilterList, FilterListItem, List, Datagrid } from 'react-admin'; import { Card, CardContent } from '@mui/material'; -import MailIcon from '@mui/icons-material/MailOutline'; -import CategoryIcon from '@mui/icons-material/LocalOffer'; -export const PostFilterSidebar = () => ( - +import { SavedQueriesList } from 'react-admin'; + +const SongFilterSidebar = () => ( + - - }> - - + }> + ... - }> - - - - + }> + ... ); -``` -{% endraw %} -React-admin also offers a **site-wide, full-text search input**, designed to be displayed on every page. +const SongList = () => ( + }> + + ... + + +); +``` -![ra-search basic](https://marmelab.com/ra-enterprise/modules/assets/ra-search-overview.gif) +Check [the Saved Queries Tutorial](./FilteringTutorial.md##saved-queries-let-users-save-filter-and-sort) to learn more. -Use [the `` component](./Search.md) to add a global search input to your app: +Finally, react-admin offers low-level components and hooks to **build your own search UI**: -{% raw %} -```jsx -// in src/MyAppBar.jsx -import { AppBar } from "react-admin"; -import { Typography } from "@mui/material"; -import { Search } from "@react-admin/ra-search"; - -export const MyAppbar = (props) => ( - - - - -); -``` -{% endraw %} +- [``](./FilterButton.md) +- [``](./FilteringTutorial.md#searchinput) +- [``](./FilterLiveSearch.md) +- [``](./SavedQueriesList.md) +- [`useListContext()`](./useListContext.md) +- [`useList()`](./useList.md) -Finally, react-admin offers low-level hooks and components to build your own search UI. Check the [Building A Custom Filter Tutorial](./FilteringTutorial.md#building-a-custom-filter) to learn more. +Check the [Building A Custom Filter Tutorial](./FilteringTutorial.md#building-a-custom-filter) to learn more. ## Forms & Validation @@ -876,7 +887,6 @@ For instance, replace `` with `` to have a list refreshing autom ```diff import { - List, - ListProps, Datagrid, TextField, NumberField, @@ -884,9 +894,9 @@ import { } from 'react-admin'; +import { ListLive } from '@react-admin/ra-realtime'; -const PostList = (props: ListProps) => ( -- -+ +const PostList = () => ( +- ++ @@ -1023,8 +1033,8 @@ const SongFilterSidebar = () => ( ); -const SongList = props => ( - }> +const SongList = () => ( + }> ... diff --git a/docs/FilteringTutorial.md b/docs/FilteringTutorial.md index 542204b8ecb..78ee48d20ff 100644 --- a/docs/FilteringTutorial.md +++ b/docs/FilteringTutorial.md @@ -5,82 +5,39 @@ title: "Filtering the List" # Filtering the List - - - -
- - - -
- -One of the most important features of the List page is the ability to filter the results. React-admin does its best to offer a powerful filter functionality, and to get out of the way when you want to go further. - -The next sections explain how to use the filter functionality. And first, a few explanations about the inner workings of filters: - -- [Filter Query Parameter](#filter-query-parameter) -- [Linking To A Pre-Filtered List](#linking-to-a-pre-filtered-list) - -React-admin proposes several UI components to let users see and modify filters, and gives you the tools to build custom ones. - -- The Filter Button/Form Combo - - [Usage](#the-filter-buttonform-combo) - - [Full-Text Search](#searchinput) - - [Quick Filters](#quick-filters) -- [The `` Sidebar](#the-filterlist-sidebar) -- [Saved Queries: Let Users Save Filter And Sort](#saved-queries-let-users-save-filter-and-sort) -- [Building A Custom Filter](#building-a-custom-filter) - -## Filter Query Parameter - -React-admin uses the `filter` query parameter from the URL to determine the filters to apply to the list. To change the filters, react-admin simply changes this `filter` query parameter, and the `` components fetches `dataProvider.getList()` again with the new filters. - -Here is a typical List URL: - -> https://myadmin.dev/#/posts?displayedFilters=%7B%22commentable%22%3Atrue%7D&filter=%7B%22commentable%22%3Atrue%2C%22q%22%3A%22lorem%20%22%7D&order=DESC&page=1&perPage=10&sort=published_at - -Once decoded, the `filter` query parameter reveals as a JSON value: - -``` -filter={"commentable":true,"q":"lorem "} -``` - -You can change the filters by updating the query parameter, e.g. using the `` component or the `useNavigate()` hook from `react-router-dom`. - -**Tip**: Once a user sets a filter, react-admin persists the filter value in the application state, so that when the user comes back to the list, they should see the filtered list. That's a design choice. - -## Linking To A Pre-Filtered List - -As the filter values are taken from the URL, you can link to a pre-filtered list by setting the `filter` query parameter. - -For instance, if you have a list of tags, you can display a button for each category to link to the list of posts filtered by that tag: - -{% raw %} -```jsx -import { useTranslate, useRecordContext } from 'react-admin'; -import Button from '@mui/material/Button'; -import { Link } from 'react-router-dom'; - -const LinkToRelatedProducts = () => { - const record = useRecordContext(); - const translate = useTranslate(); - return record ? ( - - ) : null; -}; -``` -{% endraw %} - -You can use this button e.g. as a child of ``. You can also create a custom Menu button with that technique to link to the unfiltered list by setting the filter value to `{}`. +One of the most important features of the List page is the ability to filter the results. React-admin offers powerful filter components, and gets out of the way when you want to go further. + +## Overview + + + + + + + + + + +
+ + The Filter Button/Form Combo + + + The <FilterList> Sidebar +
+ + + + The <StackedFilters> Dialog + + + The Global <Search> +
+ +React-admin offers 4 different ways to filter the list. Depending on the type of data you're displaying, the type and number of filters you have to display, and the device your users are using, you may want to use one or the other. ## The Filter Button/Form Combo @@ -96,8 +53,8 @@ const postFilters = [ , ]; -export const PostList = (props) => ( - +export const PostList = () => ( + ... ); @@ -217,6 +174,133 @@ Finally, a filter sidebar is the ideal place to display the user's favorite filt ![Filter Sidebar With SavedQueriesList](./img/SavedQueriesList.gif) +## The `` Component + + + +Another alternative filter UI is the Stacked Filters dialog, an [Enterprise Edition](https://marmelab.com/ra-enterprise) exclusive. It lets users build complex filters by combining a field, an operator, and a value. It's more powerful than the Filter Button/Form Combo, but requires more setup on the data provider. + +Here is an example StackedFilters configuration: + +```jsx +import { + List, + Datagrid, + TextField, + NumberField, + ReferenceArrayField + BooleanField, +} from 'react-admin'; +import { + textFilter, + dateFilter, + booleanFilter + referenceFilter, + StackedFilters, +} from '@react-admin/ra-form-layout'; + +const postListFilters = { + id: textFilter({ operators: ['eq', 'neq'] }), + title: textFilter(), + published_at: dateFilter(), + is_public: booleanFilter(), + tags: referenceFilter({ reference: 'tags' }), +}; + +const PostList = () => ( + }> + + + + + + + +) +``` + +Check the [`` documentation](./StackedFilters.md) for more information. + +## Global Search + +![ra-search basic](https://marmelab.com/ra-enterprise/modules/assets/ra-search-overview.gif) + +Although list filters allow to make precise queries using per-field criteria, users often prefer simpler interfaces like full-text search. After all, that's what they use every day on search engines, email clients, and in their file explorer. + +If you want to display a full-text search allowing to look for any record in the admin using a single form input, check out [the `` component](./Search.md), an [Enterprise Edition](https://marmelab.com/ra-enterprise) exclusive. + +`` can plug to any existing search engine (ElasticSearch, Lucene, or custom search engine), and lets you customize the search results to provide quick navigation to related items, turning the search engine into an "Omnibox": + +![ra-search demo](https://marmelab.com/ra-enterprise/modules/assets/ra-search-demo.gif) + +For mode details about the global search, check the [`` documentation](./Search.md). + +## Filter Query Parameter + +React-admin uses the `filter` query parameter from the URL to determine the filters to apply to the list. + +Here is a typical List page URL in a react-admin application: + +> https://myadmin.dev/#/posts?displayedFilters=%7B%22commentable%22%3Atrue%7D&filter=%7B%22commentable%22%3Atrue%2C%22q%22%3A%22lorem%20%22%7D&order=DESC&page=1&perPage=10&sort=published_at + +Once decoded, the `filter` query parameter reveals as a JSON value: + +``` +filter={"commentable":true,"q":"lorem "} +``` + +This leads to the following data provider call: + +```js +dataProvider.getList('posts', { + filter: { commentable: true, q: 'lorem ' }, + pagination: { page: 1, perPage: 10 }, + sort: { field: 'published_at', order: 'DESC' }, +}); +``` + +When a user adds or remove a filter, react-admin changes the `filter` query parameter in the URL, and the `` components fetches `dataProvider.getList()` again with the new filters. + +**Tip**: Once a user sets a filter, react-admin persists the filter value in the application state, so that when the user comes back to the list, they should see the filtered list. That's a design choice. + +**Tip**: You can change the filters programmatically by updating the query parameter, e.g. using the `` component or the `useNavigate()` hook from `react-router-dom`. + +## Linking To A Pre-Filtered List + +As the filter values are taken from the URL, you can link to a pre-filtered list by setting the `filter` query parameter. + +For instance, if you have a list of tags, you can display a button for each category to link to the list of posts filtered by that tag: + +{% raw %} +```jsx +import { useTranslate, useRecordContext } from 'react-admin'; +import Button from '@mui/material/Button'; +import { Link } from 'react-router-dom'; + +const LinkToRelatedProducts = () => { + const record = useRecordContext(); + const translate = useTranslate(); + return record ? ( + + ) : null; +}; +``` +{% endraw %} + +You can use this button e.g. as a child of ``. You can also create a custom Menu button with that technique to link to the unfiltered list by setting the filter value to `{}`. + ## Filter Operators The internal format for storing filters and sending them to the dataProvider is an object, e.g.: @@ -301,8 +385,8 @@ const SongFilterSidebar = () => ( ); -const SongList = props => ( - }> +const SongList = () => ( + }> ... @@ -310,20 +394,6 @@ const SongList = props => ( ); ``` -## Global Search - -Although list filters allow to make precise queries using per-field criteria, users often prefer simpler interfaces like full-text search. After all, that's what they use every day on search engines, email clients, and in their file explorer. - -If you want to display a full-text search allowing to look for any record in the admin using a single form input, check out [the `` component](./Search.md), an [Enterprise Edition](https://marmelab.com/ra-enterprise) exclusive. - -![ra-search basic](https://marmelab.com/ra-enterprise/modules/assets/ra-search-overview.gif) - -`` can plug to any existing search engine (ElasticSearch, Lucene, or custom search engine), and lets you customize the search results to provide quick navigation to related items, turning the search engine into an "Omnibox": - -![ra-search demo](https://marmelab.com/ra-enterprise/modules/assets/ra-search-demo.gif) - -For mode details about the global search, check the [`ra-search` module](https://marmelab.com/ra-enterprise/modules/ra-search) in React-Admin Enterprise Edition. - ## Building a Custom Filter ![Filters with submit button](./img/filter_with_submit.gif) @@ -475,8 +545,8 @@ const ListActions = () => ( ); -export const PostList = (props) => ( - }> +export const PostList = () => ( + }> ... ); diff --git a/docs/ListTutorial.md b/docs/ListTutorial.md index e7d688d77a7..2a2aa51f5f7 100644 --- a/docs/ListTutorial.md +++ b/docs/ListTutorial.md @@ -784,8 +784,8 @@ const PostPagination = () => { ); } -export const PostList = (props) => ( - }> +export const PostList = () => ( + }> ... ); @@ -810,10 +810,10 @@ export const PaginationActions = props => ( /> ); -export const Pagination = props => ; +export const Pagination = () => ; -export const UserList = props => ( - } > +export const UserList = () => ( + } > //... ); diff --git a/docs/NumberInput.md b/docs/NumberInput.md index 5564b3eb859..77f32f882b3 100644 --- a/docs/NumberInput.md +++ b/docs/NumberInput.md @@ -59,8 +59,8 @@ const productFilters = [ , ]; -export const ProductList = (props) => ( - +export const ProductList = () => ( + ... ); diff --git a/docs/Realtime.md b/docs/Realtime.md index 75388b4cab3..66372d3db48 100644 --- a/docs/Realtime.md +++ b/docs/Realtime.md @@ -59,7 +59,6 @@ For instance, replace `` with `` to have a list refreshing autom ```diff import { - List, - ListProps, Datagrid, TextField, NumberField, @@ -67,9 +66,9 @@ import { } from 'react-admin'; +import { ListLive } from '@react-admin/ra-realtime'; -const PostList = (props: ListProps) => ( -- -+ +const PostList = () => ( +- ++ diff --git a/docs/SavedQueriesList.md b/docs/SavedQueriesList.md index 56d5f5a9f4e..696a13374e7 100644 --- a/docs/SavedQueriesList.md +++ b/docs/SavedQueriesList.md @@ -35,8 +35,8 @@ const SongFilterSidebar = () => ( ); -const SongList = props => ( - }> +const SongList = () => ( + }> ... @@ -72,8 +72,8 @@ const SongFilterSidebar = () => ( ); -const SongList = props => ( - }> +const SongList = () => ( + }> ... diff --git a/docs/SimpleList.md b/docs/SimpleList.md index 541a62bfdda..b34391b8364 100644 --- a/docs/SimpleList.md +++ b/docs/SimpleList.md @@ -164,8 +164,8 @@ const postRowStyle = (record, index) => ({ backgroundColor: record.nb_views >= 500 ? '#efe' : 'white', }); -export const PostList = (props) => ( - +export const PostList = () => ( + record.title} rowStyle={postRowStyle} /> ); @@ -187,10 +187,10 @@ To use `` on small screens and a `` on larger screens, use import { useMediaQuery } from '@mui/material'; import { List, SimpleList, Datagrid } from 'react-admin'; -export const PostList = props => { +export const PostList = () => { const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); return ( - + {isSmall ? ( record.title} diff --git a/docs/StackedFilters.md b/docs/StackedFilters.md new file mode 100644 index 00000000000..c57edea6183 --- /dev/null +++ b/docs/StackedFilters.md @@ -0,0 +1,299 @@ +--- +layout: default +title: "The StackedFilters Component" +--- + +# `` + +This [Enterprise Edition](https://marmelab.com/ra-enterprise) component provides an alternative filter UI for `` pages. It lets users build complex filters by combining a field, an operator, and a value. + + + +## Usage + +Create a filter configuration object by speficying the operators and UI for each source that can be used as a filter. Then, pass it to the `` component, and pass that component to the `filters` prop of the `` component. + +```jsx +import { Datagrid, List, TextField, NumberField, BooleanField, ReferenceArrayField } from 'react-admin'; +import { StackedFilters, textFilter, dateFilter, referenceFilter, booleanFilter } from '@react-admin/ra-form-layout'; + +const postListFilters = { + id: textFilter({ operators: ['eq', 'neq'] }), + title: textFilter(), + published_at: dateFilter(), + is_public: booleanFilter(), + tags: referenceFilter({ reference: 'tags' }), +}; + +const PostList = () => ( + }> + + + + + + + +) +``` + +The `config` prop accepts an object with the following structure: + +```js +{ + [source]: { + operators: [operator], + input: ReactElement, + }, +} +``` + +Check the [built-in filters](#built-in-filters) section for more information about helpers for building filters. + +## Data Provider Integration + +Upon user interaction, `` sets the list `filter` with keys concatenating the selected source with the selected operator, separated by an underscore (`_`). In the screencast above, the user selection triggers a call to the `dataProvider` with the following filter: + +```js +dataProvider.getList('posts', { + filter: { + title_contains: 'volup', + is_public_eq: true, + }, + sort: { field: 'id', order: 'DESC' }, + pagination: { page: 1, perPage: 10 }, +}); +``` + +It's your responsibility to handle these compound keys in your data provider. For instance, to handle the `title_contains` filter, you can use the following code: + +```js +// in dataProvider.js +const dataProvider = { + getList: (resource, params) => { + const { page, perPage } = params.pagination; + const { field, order } = params.sort; + let filter = params.filter; + if (resource === 'posts') { + if (filter.title_contains) { + filter = { ...filter, title: { ilike: `%${filter.title_contains}%` } }; + delete filter.title_contains; + } + if (filter.is_public_eq) { + filter = { ...filter, is_public: filter.is_public_eq }; + delete filter.is_public_eq; + } + } + const query = { + sort: JSON.stringify([field, order]), + range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]), + filter: JSON.stringify(params.filter), + }; + const url = `${apiUrl}/${resource}?${stringify(query)}`; + return httpClient(url).then(({ headers, json }) => ({ + data: json, + total: parseInt(headers.get('content-range').split('/').pop(), 10), + })); + }, +}; +``` + +**Tip**: Build a generic solution to handle compound keys for all resources in your data provider, or add support for compound keys in your API directly. + +## `config` + +`` requires a filter configuration object defining the possible fields and operators. + +For convenience, react-admin provides a set of [built-in filters](#built-in-filters) for common field types (text, number, date, reference, etc). + +```jsx +import { StackedFilters, textFilter, dateFilter, referenceFilter, booleanFilter } from '@react-admin/ra-form-layout'; + +const postListFilters = { + id: textFilter({ operators: ['eq', 'neq'] }), + title: textFilter(), + published_at: dateFilter(), + is_public: booleanFilter(), + tags: referenceFilter({ reference: 'tags' }), +}; + +const PostList = () => ( + }> + {/* ... */} + +); +``` + +You can also define your own filters. Each filter is an object that defines the operators and UI for a given source. For instance, the following filter configuration defines a filter for the `views` field that uses a `NumberInput` for the `eq` and `neq` operators, and a custom `MyNumberRangeInput` for the `between` operator. + +```jsx +import { NumberInput } from 'react-admin'; +import { textFilter, referenceFilter, booleanFilter } from '@react-admin/ra-form-layout'; + +import { MyNumberRangeInput } from './MyNumberRangeInput'; + +const postListFilters = { + title: textFilter(), + views: { + operators: [ + { value: 'eq', label: 'Equals', input: ({ source }) => , }, + { value: 'neq', label: 'Not Equals', input: ({ source }) => , }, + { value: 'between', label: 'Between', input: ({ source }) => }, + ], + }, + tag_ids: referenceFilter({ reference: 'tags' }), + published: booleanFilter(), +}; +``` + +An operator is an object with the shape `{ label, value, input }`. + +- `label`: Operator input label. Can be a translation key. +- `value`: used as a suffix to the `source` when creating the list filters. +- `input`: a component accepting a `source` prop. React-admin will pass the source of the filter to the input component ('views' in the above example) + +For instance, with the source `views`, the operator `eq` and value set to `0`, the dataProvider will receive the following `filter` parameter: + +```js +{ views_eq: 0 } +``` + +**Tip**: You can define a default input for all operators of a filter by using the `input` property of the filter object: + +```jsx +import { NumberInput } from 'react-admin'; +import { textFilter, referenceFilter, booleanFilter } from '@react-admin/ra-form-layout'; + +const postListFilters = { + title: textFilter(), + views: { + input: ({ source }) => , + operators: [ + { value: 'eq', label: 'Equals' }, + { value: 'neq', label: 'Not Equals' }, + { value: 'between', label: 'Between', input: ({ source }) => }, + ], + }, + tag_ids: referenceFilter({ reference: 'tags' }), + published: booleanFilter(), +}; +``` + +## Built-In Filters + +To facilitate the creation of filter configurations, react-admin provides some helpers for common filter types. Each of them has predefined operators and inputs. They accept an array of operators if you want to remove some of them. + +- `textFilter`: A filter for text fields. Defines the following operator: `eq`, `neq` and `q`. +- `numberFilter`: A filter for number fields. Defines the following operator: `eq`, `neq`, `lt` and `gt`. +- `dateFilter`: A filter for date fields. Defines the following operator: `eq`, `neq`, `lt` and `gt`. +- `booleanFilter`: A filter for boolean fields. Defines the following operator: `eq`. +- `choicesFilter`: A filter for fields that accept a value from a list of choices. Defines the following operator: `eq`, `neq`, `eq_any` and `neq_any`. +- `choicesArrayFilter`: A filter for array fields. Defines the following operator: `inc`, `inc_any` and `ninc_any`. +- `referenceFilter`: A filter for reference fields. Defines the following operator: `eq`, `neq`, `eq_any` and `neq_any`. + +The operators are defined as follows: + +- `eq`: Equals +- `neq`: Not Equals +- `q`: Full-text search +- `lt`: Less than +- `gt`: Greater than +- `eq_any`: Equals any +- `neq_any`: Not Equals any +- `inc`: Includes +- `inc_any`: Includes any +- `ninc_any`: Not Includes any + +Use these built-in helpers to build your filter configuration: + +```jsx +import { textFilter, numberFilter, referenceFilter, booleanFilter } from '@react-admin/ra-form-layout'; + +const postListFilters = { + title: textFilter(), + views: numberFilter(), + tag_ids: referenceFilter({ reference: 'tags' }), + published: booleanFilter(), +}; +``` + +With the configuration above, the possible filter keys are: + +- `title_eq` +- `title_neq` +- `title_q` +- `views_eq` +- `views_neq` +- `views_lt` +- `views_gt` +- `tag_ids_eq` +- `tag_ids_neq` +- `tag_ids_eq_any` +- `tag_ids_neq_any` +- `published_eq` + +## Internationalization + +React-admin uses the keys of the filter configuration to display the field names. Each key goes through the standard [resource and field name translation system](./Translation.md#translating-resource-and-field-names), so you can customize them using a translation file. + +```js +// in i18n/en.js +export default { + resources: { + posts: { + name: 'Post |||| Posts', + fields: { + title: 'Title', + views: '# views', + tag_ids: 'Tags', + published: 'Published', + } + }, + }, +}; +``` + +`` also uses internationalization for operators. To leverage it, pass a translation key as the operator label: + +```jsx +import DateRangeInput from './DateRangeInput'; + +const MyFilterConfig = { + published_at: { + operators: [ + { + value: 'between', + label: 'resources.posts.filters.operators.between', + }, + { + value: 'nbetween', + label: 'resources.posts.filters.operators.nbetween', + }, + ], + input: ({ source }) => , + }, +}; +``` + +Then translate the operators in the translation messages: + +```js +// in i18n/en.js +export default { + resources: { + posts: { + name: 'Post |||| Posts', + filters: { + operators: { + between: 'Between', + nbetween: 'Not Between', + }, + }, + }, + }, +}; +``` + diff --git a/docs/navigation.html b/docs/navigation.html index b7ea8df2271..db19c0e76f1 100644 --- a/docs/navigation.html +++ b/docs/navigation.html @@ -74,6 +74,7 @@
  • <FilterList>
  • <FilterLiveSearch>
  • <SavedQueriesList>
  • +
  • <StackedFilters>
  • <Pagination>
  • <SortButton>
  • <SelectColumnsButton>
  • diff --git a/docs/useStore.md b/docs/useStore.md index 955b4745e25..f831e5b6b11 100644 --- a/docs/useStore.md +++ b/docs/useStore.md @@ -35,11 +35,11 @@ When one component calls `setValue` on a key, all the components that read the s ```jsx import { List, Datagrid } from 'react-admin'; -const PostList = props => { +const PostList = () => { const [density] = useStore('posts.list.density', 'small'); return ( - + ... diff --git a/packages/ra-core/src/store/useStore.ts b/packages/ra-core/src/store/useStore.ts index cdc78615d10..ed39a8b8241 100644 --- a/packages/ra-core/src/store/useStore.ts +++ b/packages/ra-core/src/store/useStore.ts @@ -18,11 +18,11 @@ import { useStoreContext } from './useStoreContext'; * @example * import { useStore } from 'react-admin'; * - * const PostList = props => { + * const PostList = () => { * const [density] = useStore('posts.list.density', 'small'); * * return ( - * + * * * ... * diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx index 3efa8aa0f88..ba98abbdaed 100644 --- a/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx @@ -27,8 +27,8 @@ import { MutationMode } from 'ra-core'; * * ); * - * export const PostList = (props) => ( - * }> + * export const PostList = () => ( + * }> * ... * * ); diff --git a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx index e7e8827cb25..5a00d2b2d73 100644 --- a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx @@ -30,8 +30,8 @@ import { Button, ButtonProps } from './Button'; * * ); * - * export const PostList = (props) => ( - * }> + * export const PostList = () => ( + * }> * ... * * ); diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx index 7d78963bf20..73855299681 100644 --- a/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx @@ -27,8 +27,8 @@ import { MutationMode } from 'ra-core'; * * ); * - * export const PostList = (props) => ( - * }> + * export const PostList = () => ( + * }> * ... * * ); diff --git a/packages/ra-ui-materialui/src/list/List.tsx b/packages/ra-ui-materialui/src/list/List.tsx index 8cad727b1ef..cd366ff589b 100644 --- a/packages/ra-ui-materialui/src/list/List.tsx +++ b/packages/ra-ui-materialui/src/list/List.tsx @@ -42,8 +42,8 @@ import { ListView, ListViewProps } from './ListView'; * , * * ]; - * export const PostList = (props) => ( - * ( + * ({ * backgroundColor: record.views >= 500 ? '#efe' : 'white', * }); - * export const PostList = (props) => ( - * + * export const PostList = () => ( + * * record.title} * secondaryText={record => `${record.views} views`} diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index 37b8bad5de0..b99f0029590 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -59,8 +59,8 @@ const defaultBulkActionButtons = ; * const postRowStyle = (record, index) => ({ * backgroundColor: record.nb_views >= 500 ? '#efe' : 'white', * }); - * export const PostList = (props) => ( - * + * export const PostList = () => ( + * * * * diff --git a/packages/ra-ui-materialui/src/list/filter/Filter.tsx b/packages/ra-ui-materialui/src/list/filter/Filter.tsx index 678a030ffe6..0642e89ac38 100644 --- a/packages/ra-ui-materialui/src/list/filter/Filter.tsx +++ b/packages/ra-ui-materialui/src/list/filter/Filter.tsx @@ -18,8 +18,8 @@ import { FilterContext } from '../FilterContext'; * * ); * - * export const PostList = (props) => ( - * }> + * export const PostList = () => ( + * }> * ... * * ); From 8c70825d37f98f015784dffa3525c26866e00636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Tue, 7 Feb 2023 22:59:44 +0100 Subject: [PATCH 2/3] Add new chapter to index --- docs/Reference.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Reference.md b/docs/Reference.md index 81bf0171e7e..4b3aba570ec 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -162,6 +162,7 @@ title: "Index" * [``](./SimpleShowLayout.md) * [``](./SingleFieldList.md) * [``](./SortButton.md) +* [``](./StackedFilters.md) **- T -** * `` From d1aa81bd0800fa7ed5b5def307cad4aa96dd31cc Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 8 Feb 2023 09:59:47 +0100 Subject: [PATCH 3/3] Proofreading --- docs/StackedFilters.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/StackedFilters.md b/docs/StackedFilters.md index c57edea6183..9b5a3043093 100644 --- a/docs/StackedFilters.md +++ b/docs/StackedFilters.md @@ -14,7 +14,7 @@ This [Enterprise Edition](https://marmelab.com/ra-enterprise)` component, and pass that component to the `filters` prop of the `` component. +Create a filter configuration object by specifying the operators and UI for each source that can be used as a filter. Then, pass it to the `` component, and pass that component to the `filters` prop of the `` component. ```jsx import { Datagrid, List, TextField, NumberField, BooleanField, ReferenceArrayField } from 'react-admin'; @@ -42,7 +42,7 @@ const PostList = () => ( The `config` prop accepts an object with the following structure: -```js +```jsx { [source]: { operators: [operator], @@ -57,7 +57,7 @@ Check the [built-in filters](#built-in-filters) section for more information abo Upon user interaction, `` sets the list `filter` with keys concatenating the selected source with the selected operator, separated by an underscore (`_`). In the screencast above, the user selection triggers a call to the `dataProvider` with the following filter: -```js +```jsx dataProvider.getList('posts', { filter: { title_contains: 'volup', @@ -70,7 +70,7 @@ dataProvider.getList('posts', { It's your responsibility to handle these compound keys in your data provider. For instance, to handle the `title_contains` filter, you can use the following code: -```js +```jsx // in dataProvider.js const dataProvider = { getList: (resource, params) => { @@ -155,9 +155,9 @@ An operator is an object with the shape `{ label, value, input }`. - `value`: used as a suffix to the `source` when creating the list filters. - `input`: a component accepting a `source` prop. React-admin will pass the source of the filter to the input component ('views' in the above example) -For instance, with the source `views`, the operator `eq` and value set to `0`, the dataProvider will receive the following `filter` parameter: +For instance, with the source `views`, the operator `eq`, and value set to `0`, the dataProvider will receive the following `filter` parameter: -```js +```jsx { views_eq: 0 } ``` @@ -186,13 +186,13 @@ const postListFilters = { To facilitate the creation of filter configurations, react-admin provides some helpers for common filter types. Each of them has predefined operators and inputs. They accept an array of operators if you want to remove some of them. -- `textFilter`: A filter for text fields. Defines the following operator: `eq`, `neq` and `q`. -- `numberFilter`: A filter for number fields. Defines the following operator: `eq`, `neq`, `lt` and `gt`. -- `dateFilter`: A filter for date fields. Defines the following operator: `eq`, `neq`, `lt` and `gt`. +- `textFilter`: A filter for text fields. Defines the following operator: `eq`, `neq`, and `q`. +- `numberFilter`: A filter for number fields. Defines the following operator: `eq`, `neq`, `lt`, and `gt`. +- `dateFilter`: A filter for date fields. Defines the following operator: `eq`, `neq`, `lt`, and `gt`. - `booleanFilter`: A filter for boolean fields. Defines the following operator: `eq`. -- `choicesFilter`: A filter for fields that accept a value from a list of choices. Defines the following operator: `eq`, `neq`, `eq_any` and `neq_any`. -- `choicesArrayFilter`: A filter for array fields. Defines the following operator: `inc`, `inc_any` and `ninc_any`. -- `referenceFilter`: A filter for reference fields. Defines the following operator: `eq`, `neq`, `eq_any` and `neq_any`. +- `choicesFilter`: A filter for fields that accept a value from a list of choices. Defines the following operator: `eq`, `neq`, `eq_any`, and `neq_any`. +- `choicesArrayFilter`: A filter for array fields. Defines the following operator: `inc`, `inc_any`, and `ninc_any`. +- `referenceFilter`: A filter for reference fields. Defines the following operator: `eq`, `neq`, `eq_any`, and `neq_any`. The operators are defined as follows: @@ -239,7 +239,7 @@ With the configuration above, the possible filter keys are: React-admin uses the keys of the filter configuration to display the field names. Each key goes through the standard [resource and field name translation system](./Translation.md#translating-resource-and-field-names), so you can customize them using a translation file. -```js +```jsx // in i18n/en.js export default { resources: { @@ -280,7 +280,7 @@ const MyFilterConfig = { Then translate the operators in the translation messages: -```js +```jsx // in i18n/en.js export default { resources: { @@ -295,5 +295,4 @@ export default { }, }, }; -``` - +``` \ No newline at end of file