Reputation: 11137
Assume I have a route /customers where a List of customers is being rendered. On that same route there's also a side drawer present. That drawer contains a List with a Filter of help topics.
When I start typing in the Filter in the side drawer, the url changes. That's according to how react-admin's list filter is working.
The problem is that the customers list is noticing the route changes. In effect it starts querying and reloading customers based on a search term that is related to help topics. No customers found of course.
I want the customer list to not notice I'm filtering on help topics. The solution I'm aiming for is that the list filter in the side drawer will not change the url while I input a help topic search term.
How can I configure or customise the filter in the side drawer to not change the url while typing, but store the current filter value in something like component state instead?
Actually, since the filter lives in a form (by react-final-form), which keeps its own state, I could live with a solution like this. But of course publishToUrl
isn't an available prop for Filter.
const MyFilter = props => (
<Filter {...props} publishToUrl={false} >
<TextInput source="title" />
</Filter>
);
Related:
Upvotes: 2
Views: 8403
Reputation: 36
For those who, like me, are looking for a reliable solution and find this topic.
https://marmelab.com/react-admin/List.html#synchronize-with-url
For multi list on page without sync all on same filters and pagination.
Use ResourceContextProvider > List (with or without syncWithLocation):
<ResourceContextProvider value="posts">
<List syncWithLocation basePath="/posts">
<Datagrid>
... Sync with url
</Datagrid>
</List>
</ResourceContextProvider>
<ResourceContextProvider value="users">
<List basePath="/users" >
<Datagrid>
... Not Sync with url
</Datagrid>
</List>
</ResourceContextProvider>
Upvotes: 1
Reputation: 49
Following code will remove attachment of List component with Redux Store and Location, it can be used to display multiple list on single page
use this List instead of react admin's List
Create Own List component
import * as React from 'react';
import PropTypes from 'prop-types';
import {
} from 'ra-core';
import {ListView} from 'ra-ui-materialui';
import {useListController} from '../../controller/useListController';
export const TitlePropType = PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]);
/**
* List page component
*
* The <List> component renders the list layout (title, buttons, filters, pagination),
* and fetches the list of records from the REST API.
* It then delegates the rendering of the list of records to its child component.
* Usually, it's a <Datagrid>, responsible for displaying a table with one row for each post.
*
* In Redux terms, <List> is a connected component, and <Datagrid> is a dumb component.
*
* The <List> component accepts the following props:
*
* - actions
* - aside
* - component
* - filter (the permanent filter to apply to the query)
* - filters (a React component used to display the filter form)
* - pagination
* - perPage
* - sort
* - title
*
* @example
*
* const PostFilter = (props) => (
* <Filter {...props}>
* <TextInput label="Search" source="q" alwaysOn />
* <TextInput label="Title" source="title" />
* </Filter>
* );
* export const PostList = (props) => (
* <List {...props}
* title="List of posts"
* sort={{ field: 'published_at' }}
* filter={{ is_published: true }}
* filters={PostFilter}
* >
* <Datagrid>
* <TextField source="id" />
* <TextField source="title" />
* <EditButton />
* </Datagrid>
* </List>
* );
*/
export const List = props => {
return <ListView {...props} {...useListController(props)} />;
}
List.propTypes = {
// the props you can change
actions: PropTypes.element,
aside: PropTypes.element,
bulkActionButtons: PropTypes.oneOfType([PropTypes.element, PropTypes.bool]),
children: PropTypes.node,
classes: PropTypes.object,
className: PropTypes.string,
filter: PropTypes.object,
filterDefaultValues: PropTypes.object,
filters: PropTypes.element,
pagination: PropTypes.element,
perPage: PropTypes.number.isRequired,
sort: PropTypes.shape({
field: PropTypes.string,
order: PropTypes.string,
}),
title: TitlePropType,
// the props managed by react-admin
authProvider: PropTypes.func,
hasCreate: PropTypes.bool.isRequired,
hasEdit: PropTypes.bool.isRequired,
hasList: PropTypes.bool.isRequired,
hasShow: PropTypes.bool.isRequired,
location: PropTypes.object,
match: PropTypes.object,
path: PropTypes.string,
resource: PropTypes.string.isRequired,
};
List.defaultProps = {
filter: {},
perPage: 10,
};
controller/useListController
import { isValidElement, ReactElement, useEffect, useMemo } from 'react';
import inflection from 'inflection';
import { useSelector } from 'react-redux';
import get from 'lodash/get';
import {useCheckMinimumRequiredProps, useTranslate,
useNotify, useGetList, CRUD_GET_LIST, useVersion, useRecordSelection } from 'react-admin';
import { ListParams } from 'ra-core';
import { Sort, RecordMap, Identifier, ReduxState, Record } from 'ra-core';
import {SORT_ASC} from 'ra-core/esm/reducer/admin/resource/list/queryReducer';
import useListParams from './useListParams';
export interface ListProps {
// the props you can change
filter?: object;
filters?: ReactElement<any>;
filterDefaultValues?: object;
pagination?: ReactElement<any>;
perPage?: number;
sort?: Sort;
// the props managed by react-admin
basePath: string;
debounce?: number;
hasCreate?: boolean;
hasEdit?: boolean;
hasList?: boolean;
hasShow?: boolean;
path?: string;
query: ListParams;
resource: string;
[key: string]: any;
}
const defaultSort = {
field: 'id',
order: SORT_ASC,
};
const defaultData = {};
export interface ListControllerProps<RecordType = Record> {
basePath: string;
currentSort: Sort;
data: RecordMap<RecordType>;
defaultTitle: string;
displayedFilters: any;
filterValues: any;
hasCreate: boolean;
hideFilter: (filterName: string) => void;
ids: Identifier[];
loading: boolean;
loaded: boolean;
onSelect: (ids: Identifier[]) => void;
onToggleItem: (id: Identifier) => void;
onUnselectItems: () => void;
page: number;
perPage: number;
resource: string;
selectedIds: Identifier[];
setFilters: (filters: any, displayedFilters: any) => void;
setPage: (page: number) => void;
setPerPage: (page: number) => void;
setSort: (sort: string) => void;
showFilter: (filterName: string, defaultValue: any) => void;
total: number;
version: number;
}
/**
* Prepare data for the List view
*
* @param {Object} props The props passed to the List component.
*
* @return {Object} controllerProps Fetched and computed data for the List view
*
* @example
*
* import { useListController } from 'react-admin';
* import ListView from './ListView';
*
* const MyList = props => {
* const controllerProps = useListController(props);
* return <ListView {...controllerProps} {...props} />;
* }
*/
export const useListController = <RecordType = Record>(
props: ListProps
): ListControllerProps<RecordType> => {
useCheckMinimumRequiredProps('List', ['basePath', 'resource'], props);
const {
basePath,
resource,
hasCreate,
filterDefaultValues,
sort = defaultSort,
perPage = 10,
filter,
debounce = 500,
} = props;
if (filter && isValidElement(filter)) {
throw new Error(
'<List> received a React element as `filter` props. If you intended to set the list filter elements, use the `filters` (with an s) prop instead. The `filter` prop is internal and should not be set by the developer.'
);
}
const translate = useTranslate();
const notify = useNotify();
const version = useVersion();
const [query, queryModifiers] = useListParams({
resource,
filterDefaultValues,
sort,
perPage,
debounce
});
const [selectedIds, selectionModifiers] = useRecordSelection(resource);
/**
* We want the list of ids to be always available for optimistic rendering,
* and therefore we need a custom action (CRUD_GET_LIST) that will be used.
*/
const { ids, total, loading, loaded } = useGetList<RecordType>(
resource,
{
page: query.page,
perPage: query.perPage,
},
{ field: query.sort, order: query.order },
{ ...query.filter, ...filter },
{
action: CRUD_GET_LIST,
onFailure: error =>
notify(
typeof error === 'string'
? error
: error.message || 'ra.notification.http_error',
'warning'
),
}
);
const data = useSelector(
(state: ReduxState): RecordMap<RecordType> =>
get(
state.admin.resources,
[resource, 'data'],
defaultData
) as RecordMap<RecordType>
);
// When the user changes the page/sort/filter, this controller runs the
// useGetList hook again. While the result of this new call is loading,
// the ids and total are empty. To avoid rendering an empty list at that
// moment, we override the ids and total with the latest loaded ones.
const defaultIds = [];
const defaultTotal = 0;
useEffect(() => {
if (
query.page <= 0 ||
(!loading && query.page > 1 && (ids || []).length === 0)
) {
// query for a page that doesn't exist, set page to 1
queryModifiers.setPage(1);
}
}, [loading, query.page, ids, queryModifiers]);
const currentSort = useMemo(
() => ({
field: query.sort,
order: query.order,
}),
[query.sort, query.order]
);
const resourceName = translate(`resources.${resource}.name`, {
smart_count: 2,
_: inflection.humanize(inflection.pluralize(resource)),
});
const defaultTitle = translate('ra.page.list', {
name: resourceName,
});
return {
basePath,
currentSort,
data,
defaultTitle,
displayedFilters: query.displayedFilters,
filterValues: query.filterValues,
hasCreate,
hideFilter: queryModifiers.hideFilter,
ids: typeof total === 'undefined' ? defaultIds : ids,
loaded: loaded || defaultIds.length > 0,
loading,
onSelect: selectionModifiers.select,
onToggleItem: selectionModifiers.toggle,
onUnselectItems: selectionModifiers.clearSelection,
page: query.page,
perPage: query.perPage,
resource,
selectedIds,
setFilters: queryModifiers.setFilters,
setPage: queryModifiers.setPage,
setPerPage: queryModifiers.setPerPage,
setSort: queryModifiers.setSort,
showFilter: queryModifiers.showFilter,
total: typeof total === 'undefined' ? defaultTotal : total,
version,
};
};
export const injectedProps = [
'basePath',
'currentSort',
'data',
'defaultTitle',
'displayedFilters',
'filterValues',
'hasCreate',
'hideFilter',
'ids',
'loading',
'loaded',
'onSelect',
'onToggleItem',
'onUnselectItems',
'page',
'perPage',
'refresh',
'resource',
'selectedIds',
'setFilters',
'setPage',
'setPerPage',
'setSort',
'showFilter',
'total',
'version',
];
export default useListController;
controller/useListParams
import {useCallback, useMemo, useState} from 'react';
import lodashDebounce from 'lodash/debounce';
import set from 'lodash/set';
import { ListParams } from 'ra-core';
import { Sort, removeKey, removeEmpty } from 'ra-core';
import queryReducer from 'ra-core/esm/reducer/admin/resource/list/queryReducer';
import {SORT_ASC, SET_SORT, SET_PAGE, SET_PER_PAGE, SET_FILTER} from 'ra-core/esm/reducer/admin/resource/list/queryReducer';
interface ListParamsOptions {
resource: string;
perPage?: number;
sort?: Sort;
filterDefaultValues?: object;
debounce?: number;
}
interface Parameters extends ListParams {
filterValues: object;
displayedFilters: {
[key: string]: boolean;
};
requestSignature: any[];
}
interface Modifiers {
changeParams: (action: any) => void;
setPage: (page: number) => void;
setPerPage: (pageSize: number) => void;
setSort: (sort: string) => void;
setFilters: (filters: any, displayedFilters: any) => void;
hideFilter: (filterName: string) => void;
showFilter: (filterName: string, defaultValue: any) => void;
}
const emptyObject = {};
const defaultSort = {
field: 'id',
order: SORT_ASC,
};
const defaultParams = {};
/**
* Get the list parameters (page, sort, filters) and modifiers.
*
* These parameters are merged from 3 sources:
* - the query string from the URL
* - the params stored in the state (from previous navigation)
* - the options passed to the hook (including the filter defaultValues)
*
* @returns {Array} A tuple [parameters, modifiers].
* Destructure as [
* { page, perPage, sort, order, filter, filterValues, displayedFilters, requestSignature },
* { setFilters, hideFilter, showFilter, setPage, setPerPage, setSort }
* ]
*
* @example
*
* const [listParams, listParamsActions] = useListParams({
* resource: 'posts',
* location: location // From react-router. Injected to your component by react-admin inside a List
* filterDefaultValues: {
* published: true
* },
* sort: {
* field: 'published_at',
* order: 'DESC'
* },
* perPage: 25
* });
*
* const {
* page,
* perPage,
* sort,
* order,
* filter,
* filterValues,
* displayedFilters,
* requestSignature
* } = listParams;
*
* const {
* setFilters,
* hideFilter,
* showFilter,
* setPage,
* setPerPage,
* setSort,
* } = listParamsActions;
*/
const useListParams = ({
resource,
filterDefaultValues,
sort = defaultSort,
perPage = 10,
debounce = 500,
}: ListParamsOptions): [Parameters, Modifiers] => {
const [params, setParams] = useState(defaultParams);
const requestSignature = [
resource,
params,
filterDefaultValues,
JSON.stringify(sort),
perPage
];
const query = useMemo(
() =>
getQuery({
params,
filterDefaultValues,
sort,
perPage
}),
requestSignature // eslint-disable-line react-hooks/exhaustive-deps
);
const changeParams = useCallback(action => {
const newParams = queryReducer(query, action);
setParams(newParams);
}, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps
const setSort = useCallback(
(newSort: string) =>
changeParams({ type: SET_SORT, payload: { sort: newSort } }),
requestSignature // eslint-disable-line react-hooks/exhaustive-deps
);
const setPage = useCallback(
(newPage: number) => changeParams({ type: SET_PAGE, payload: newPage }),
requestSignature // eslint-disable-line react-hooks/exhaustive-deps
);
const setPerPage = useCallback(
(newPerPage: number) =>
changeParams({ type: SET_PER_PAGE, payload: newPerPage }),
requestSignature // eslint-disable-line react-hooks/exhaustive-deps
);
const filterValues = query.filter || emptyObject;
const displayedFilterValues = query.displayedFilters || emptyObject;
const debouncedSetFilters = lodashDebounce(
(newFilters, newDisplayedFilters) => {
let payload = {
filter: removeEmpty(newFilters),
displayedFilters: undefined,
};
if (newDisplayedFilters) {
payload.displayedFilters = Object.keys(
newDisplayedFilters
).reduce((filters, filter) => {
return newDisplayedFilters[filter]
? { ...filters, [filter]: true }
: filters;
}, {});
}
changeParams({
type: SET_FILTER,
payload,
});
},
debounce
);
const setFilters = useCallback(
(filters, displayedFilters) =>
debouncedSetFilters(filters, displayedFilters),
requestSignature // eslint-disable-line react-hooks/exhaustive-deps
);
const hideFilter = useCallback((filterName: string) => {
const newFilters = removeKey(filterValues, filterName);
const newDisplayedFilters = {
...displayedFilterValues,
[filterName]: undefined,
};
setFilters(newFilters, newDisplayedFilters);
}, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps
const showFilter = useCallback((filterName: string, defaultValue: any) => {
const newFilters = set(filterValues, filterName, defaultValue);
const newDisplayedFilters = {
...displayedFilterValues,
[filterName]: true,
};
setFilters(newFilters, newDisplayedFilters);
}, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps
return [
{
displayedFilters: displayedFilterValues,
filterValues,
requestSignature,
...query,
},
{
changeParams,
setPage,
setPerPage,
setSort,
setFilters,
hideFilter,
showFilter,
},
];
};
/**
* Check if user has already set custom sort, page, or filters for this list
*
* User params come from the Redux store as the params props. By default,
* this object is:
*
* { filter: {}, order: null, page: 1, perPage: null, sort: null }
*
* To check if the user has custom params, we must compare the params
* to these initial values.
*
* @param {Object} params
*/
export const hasCustomParams = (params: ListParams) => {
return (
params &&
params.filter &&
(Object.keys(params.filter).length > 0 ||
params.order != null ||
params.page !== 1 ||
params.perPage != null ||
params.sort != null)
);
};
/**
* Merge list params from 3 different sources:
* - the query string
* - the params stored in the state (from previous navigation)
* - the props passed to the List component (including the filter defaultValues)
*/
export const getQuery = ({
filterDefaultValues,
params,
sort,
perPage,
}) => {
const query: Partial<ListParams> =
hasCustomParams(params)
? { ...params } : { filter: filterDefaultValues || {} };
if (!query.sort) {
query.sort = sort.field;
query.order = sort.order;
}
if (!query.perPage) {
query.perPage = perPage;
}
if (!query.page) {
query.page = 1;
}
return {
...query,
page: getNumberOrDefault(query.page, 1),
perPage: getNumberOrDefault(query.perPage, 10),
} as ListParams;
};
export const getNumberOrDefault = (
possibleNumber: string | number | undefined,
defaultValue: number
) =>
(typeof possibleNumber === 'string'
? parseInt(possibleNumber, 10)
: possibleNumber) || defaultValue;
export default useListParams;
Upvotes: 3
Reputation: 11
I had similar issue. I have one route with three tabs. On each tab I have different List rendered. Once I had selected filters on tab1, they were propagated to url and applied to tab2 list and tab3 list.
I found solution to this:
I've analyzed react-admin source code. It is using method "changeParams" in useListParams.ts. This method is using useHistory();
from 'react-router-dom', and pushes filter params to url:
history.push({
search: `?${stringify({
...newParams,
filter: JSON.stringify(newParams.filter),
displayedFilters: JSON.stringify(newParams.displayedFilters),
})}`,
});
So my solution is that on tabs change I did history.push({ search: '' });
(of course you have to first install react-router-dom, import useHistory, and then make history as a const const history = useHistory();
).
This solution cleans url params on tab change, so search params(filters, sort and range) are not applied anymore to other tabs (and lists).
Upvotes: 1
Reputation: 306
I have tried a different solution, perhaps it will be of help to someone:
const FunctionsFilter = ({resource, ...rest}) => {
const classes = useStyles();
const location = useLocation();
const [query, queryModifiers] = useMyListParams({
resource,
location,
filterDefaultValues: {},
sort: {
field: 'name',
order: 'asc',
},
perPage: 5,
debounce: 500,
});
return (
<Filter resource={resource} {...rest} setFilters={queryModifiers.setFilters}>
<TextInput className={classes.dialogformfilter} source="name" alwaysOn resettable/>
</Filter>
);
};
Now, for some reason it sends the query twice, so i also copied useListController to useMyListController, and now it only sends it once. The downside is that you have to maintain it on version upgrades.
Also, a combination of Christiaan Westerbeek solution with useMyListController, worked best for me.
Upvotes: 0
Reputation: 11137
Thanks to the directions provided by d0berm4n, I was able to compile a working solution (for react-admin 3.x). It's pretty limited in terms of the query that it creates (just filter), but it's just wat I need.
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import lodashDebounce from 'lodash/debounce';
import { Filter, TextInput, changeListParams } from 'react-admin';
const MyFilter = ({ resource, ...rest }) => {
const dispatch = useDispatch();
const debouncedSetFilters = lodashDebounce((filter = {}) => {
const query = { filter };
dispatch(changeListParams(resource, query));
}, 500);
const setFilters = useCallback(debouncedSetFilters, []);
return (
<Filter resource={resource} {...rest} setFilters={setFilters}>
<TextInput source="title" alwaysOn resettable />
</Filter>
);
};
MyFilter.propTypes = {
resource: PropTypes.string,
};
MyFilter.displayName = 'MyFilter';
export default MyFilter;
Update: This solution is incomplete. Other filters on the same page are now starting to query on the List that MyFilter is on. I suspect that's a problem that I can isolate. More on this later...
Upvotes: 0
Reputation: 71
setFilters() property is passed down to Filter component from his parent List.
So you need to implement your own useListParams hook with removed/wrapped with condition commented lines:
const changeParams = useCallback(action => {
const newQuery = getQuery({
location,
params,
filterDefaultValues,
sort,
perPage,
});
const newParams = queryReducer(newQuery, action);
// history.push({
// search: `?${stringify({
// ...newParams,
// filter: JSON.stringify(newParams.filter),
// })}`,
// });
dispatch(changeListParams(resource, newParams));
}, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps
Then you have to implement useListController and call your hook instead of react-admin's one.
const [query, queryModifiers] = useListParams({
resource,
location,
filterDefaultValues,
sort,
perPage,
debounce,
});
Finally you implement List component and pass your new cool useListController. Filter values will not be reflected in query string as well as paging and sorting.
Another and simpler way is to intercept setFilters call in your Filter component and do
dispatch(changeListParams(resource, newParams));
with new filter values without implementation bunch of hooks and components.
Upvotes: 1