如何不更改 url on list filtering with react-admin?
How to not change the url on list filtering with react-admin?
假设我有一条路线 /customers,其中 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 个 帮助主题 。
当我开始在侧抽屉中的筛选器中键入内容时,url 会发生变化。这是根据 react-admin 的列表过滤器的工作方式。
问题是客户列表注意到路线更改。实际上,它根据与 帮助主题 相关的搜索词开始查询和重新加载 customers。当然没有找到客户。
我希望客户列表不会注意到我正在过滤帮助主题。我的目标解决方案是侧抽屉中的列表过滤器在我输入帮助主题搜索词时不会更改 url。
我如何配置或自定义侧抽屉中的过滤器,以便在键入时不更改 url,而是将当前过滤器值存储在组件状态之类的东西中?
实际上,由于过滤器以一种形式存在(通过 react-final-form),它保持自己的状态,所以我可以接受这样的解决方案。但是当然 publishToUrl
不是 Filter 的可用道具。
const MyFilter = props => (
<Filter {...props} publishToUrl={false} >
<TextInput source="title" />
</Filter>
);
相关:
setFilters() 属性 从父 List 传给 Filter 组件。
因此您需要使用 removed/wrapped 和条件注释行来实现您自己的 useListParams 挂钩:
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
然后你必须实现 useListController 并调用你的钩子而不是 react-admin 的钩子。
const [query, queryModifiers] = useListParams({
resource,
location,
filterDefaultValues,
sort,
perPage,
debounce,
});
最后,您实现了 List 组件并传递了您的新酷 useListController。过滤值不会反映在查询字符串以及分页和排序中。
另一种更简单的方法是拦截 Filter 组件中的 setFilters 调用并执行
dispatch(changeListParams(resource, newParams));
具有新的过滤器值,但没有实现一堆挂钩和组件。
感谢 d0berm4n 提供的指导,我能够编译一个工作解决方案(用于 react-admin 3.x)。它在创建的查询方面非常有限(只是过滤),但这正是我需要的。
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;
更新:此解决方案不完整。同一页面上的其他过滤器现在开始查询 MyFilter 所在的列表。我怀疑这是一个我可以隔离的问题。稍后会详细介绍...
我尝试了不同的解决方案,也许对某人有帮助:
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>
);
};
现在,由于某种原因它发送了两次查询,所以我也将 useListController 复制到 useMyListController,现在它只发送一次。缺点是版本升级需要维护
此外,Christiaan Westerbeek 解决方案与 useMyListController 的组合对我来说效果最好。
我有类似的问题。我有一个包含三个选项卡的路线。在每个选项卡上,我都呈现了不同的列表。一旦我在 tab1 上选择了过滤器,它们就会传播到 url 并应用于 tab2 列表和 tab3 列表。
我找到了解决方法:
我已经分析了 react-admin 源代码。它正在使用 useListParams.ts 中的方法 "changeParams"。此方法使用 'react-router-dom' 中的 useHistory();
,并将过滤器参数推送到 url:
history.push({
search: `?${stringify({
...newParams,
filter: JSON.stringify(newParams.filter),
displayedFilters: JSON.stringify(newParams.displayedFilters),
})}`,
});
所以我的解决方案是在选项卡上更改我做了 history.push({ search: '' });
(当然你必须先安装 react-router-dom,导入 useHistory,然后将历史作为 const const history = useHistory();
)。
此解决方案在选项卡更改时清除 url 参数,因此搜索参数(过滤器、排序和范围)不再应用于其他选项卡(和列表)。
以下代码将删除 List 组件与 Redux Store 和 Location 的附件,它可用于在单个页面上显示多个列表
使用这个列表而不是 React 管理员的列表
创建自己的列表组件
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;
对于像我一样正在寻找可靠解决方案并找到此主题的人。
https://marmelab.com/react-admin/List.html#synchronize-with-url
对于没有同步的页面上的多个列表,所有列表都在相同的过滤器和分页上。
使用 ResourceContextProvider > 列表(带或不带 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>
假设我有一条路线 /customers,其中 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 个 帮助主题 。
当我开始在侧抽屉中的筛选器中键入内容时,url 会发生变化。这是根据 react-admin 的列表过滤器的工作方式。
问题是客户列表注意到路线更改。实际上,它根据与 帮助主题 相关的搜索词开始查询和重新加载 customers。当然没有找到客户。
我希望客户列表不会注意到我正在过滤帮助主题。我的目标解决方案是侧抽屉中的列表过滤器在我输入帮助主题搜索词时不会更改 url。
我如何配置或自定义侧抽屉中的过滤器,以便在键入时不更改 url,而是将当前过滤器值存储在组件状态之类的东西中?
实际上,由于过滤器以一种形式存在(通过 react-final-form),它保持自己的状态,所以我可以接受这样的解决方案。但是当然 publishToUrl
不是 Filter 的可用道具。
const MyFilter = props => (
<Filter {...props} publishToUrl={false} >
<TextInput source="title" />
</Filter>
);
相关:
setFilters() 属性 从父 List 传给 Filter 组件。
因此您需要使用 removed/wrapped 和条件注释行来实现您自己的 useListParams 挂钩:
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
然后你必须实现 useListController 并调用你的钩子而不是 react-admin 的钩子。
const [query, queryModifiers] = useListParams({
resource,
location,
filterDefaultValues,
sort,
perPage,
debounce,
});
最后,您实现了 List 组件并传递了您的新酷 useListController。过滤值不会反映在查询字符串以及分页和排序中。
另一种更简单的方法是拦截 Filter 组件中的 setFilters 调用并执行
dispatch(changeListParams(resource, newParams));
具有新的过滤器值,但没有实现一堆挂钩和组件。
感谢 d0berm4n 提供的指导,我能够编译一个工作解决方案(用于 react-admin 3.x)。它在创建的查询方面非常有限(只是过滤),但这正是我需要的。
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;
更新:此解决方案不完整。同一页面上的其他过滤器现在开始查询 MyFilter 所在的列表。我怀疑这是一个我可以隔离的问题。稍后会详细介绍...
我尝试了不同的解决方案,也许对某人有帮助:
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>
);
};
现在,由于某种原因它发送了两次查询,所以我也将 useListController 复制到 useMyListController,现在它只发送一次。缺点是版本升级需要维护
此外,Christiaan Westerbeek 解决方案与 useMyListController 的组合对我来说效果最好。
我有类似的问题。我有一个包含三个选项卡的路线。在每个选项卡上,我都呈现了不同的列表。一旦我在 tab1 上选择了过滤器,它们就会传播到 url 并应用于 tab2 列表和 tab3 列表。
我找到了解决方法:
我已经分析了 react-admin 源代码。它正在使用 useListParams.ts 中的方法 "changeParams"。此方法使用 'react-router-dom' 中的 useHistory();
,并将过滤器参数推送到 url:
history.push({
search: `?${stringify({
...newParams,
filter: JSON.stringify(newParams.filter),
displayedFilters: JSON.stringify(newParams.displayedFilters),
})}`,
});
所以我的解决方案是在选项卡上更改我做了 history.push({ search: '' });
(当然你必须先安装 react-router-dom,导入 useHistory,然后将历史作为 const const history = useHistory();
)。
此解决方案在选项卡更改时清除 url 参数,因此搜索参数(过滤器、排序和范围)不再应用于其他选项卡(和列表)。
以下代码将删除 List 组件与 Redux Store 和 Location 的附件,它可用于在单个页面上显示多个列表
使用这个列表而不是 React 管理员的列表
创建自己的列表组件
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;
对于像我一样正在寻找可靠解决方案并找到此主题的人。
https://marmelab.com/react-admin/List.html#synchronize-with-url
对于没有同步的页面上的多个列表,所有列表都在相同的过滤器和分页上。
使用 ResourceContextProvider > 列表(带或不带 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>