我想知道,是否可以将DataProvider / Resource / List配置为支持诸如api/users/1/roles
之类的REST网址?
对于RESTful API,获取某些父实体的子代是非常常见的用例,但是我无法弄清楚它如何设置React Admin并实现这一点。我正在OData规范后端上使用自定义DataProvider。
我了解可以通过api/roles?filter={userId: 1}
或类似的过滤请求来获取某些用户的角色,但是我的问题是我的用户和角色处于多对多关系,因此关系引用存储在数据透视表。换句话说,我在角色表中没有关于用户的引用,因此无法过滤它们。
我正在监督某些事情,还是有一些我根本看不到的方法?
编辑: REST API是基于OData规范构建的,它支持与经典数据透视表(或中间表)的多对多关系。该表未在API中公开,但在上面的网址中使用。因此,我无法直接将其作为资源访问。
用户模式-角色关系看起来也很标准。
|----------| |-----------| |--------|
| USER | | User_Role | | Role |
|----------| |-----------| |--------|
| Id |-\ | Id | /-| Id |
| Login | \-| UserId | / | Name |
| Password | | RoleId |-/ | Code |
|----------| |-----------| |--------|
答案 0 :(得分:5)
TL; DR:默认情况下,React Admin不支持嵌套资源,您必须write a custom data provider。
这个问题在过去的一个问题上得到了回答:maremelab/react-admin#261
详细答案
React Admin中的默认数据提供者为ra-data-simple-rest
。
如其文档所述,该库不支持嵌套资源,因为它仅使用资源名称和资源ID来构建资源URL:
为了支持嵌套资源,您必须编写自己的数据提供程序。
嵌套资源支持是recurrent feature request,但当时,核心团队不想处理这些工作。
我强烈建议您集结力量并编写外部数据提供者,并像ra-data-odata
提供者那样发布它。这将是一个很好的补充,我们很荣幸为您提供该外部软件包。
答案 1 :(得分:4)
您的问题已经回答here,但是我想向您介绍我的解决方法,以便React-Admin与多对多关系进行工作。
如上述答案中所述,您必须扩展DataProvider才能使其获取多对多关系的资源。但是,您需要使用新的REST动词,假设GET_MANY_MANY_REFERENCE
在您的应用程序中的某个位置。由于不同的REST服务/ API可以具有不同的路由格式来获取相关资源,因此我不必费心尝试构建新的DataProvider,因此我知道这不是一个很好的解决方案,但是对于较短的截止日期而言,这很简单。
我的解决方案是从<ReferenceManyField>
那里获得灵感,并为多对多关系建立了一个新的组件<ReferenceManyManyField>
。该组件使用fetch API获取componentDidMount
上的相关记录。在响应时,使用响应数据将对象构建为一个数据,该数据是一个对象,其键为记录id,并为各个记录对象赋值,并为一个具有记录id的ids数组。这与其他状态变量(如page,sort,perPage,total)一起传递给子代,以处理数据的分页和排序。请注意,更改Datagrid中数据的顺序意味着将向API发出新请求。该组件分为一个控制器和一个视图,如<ReferencemanyField>
,在该视图中,控制器获取数据,对其进行管理并将其传递给子级,并通过视图接收控制器数据并将其传递给子级以呈现其内容。即使有一定限制,这也使我能够在Datagrid上呈现多对多关系数据,这是聚合到我的项目中的组件,并且仅当需要将字段更改为以下内容时,才可以使用当前的API。但就目前而言,它可以正常使用,并且可以在我的应用中重复使用。
实施细节如下:
//ReferenceManyManyField
export const ReferenceManyManyField = ({children, ...prop}) => {
if(React.Children.count(children) !== 1) {
throw new Error( '<ReferenceManyField> only accepts a single child (like <Datagrid>)' )
}
return <ReferenceManyManyFieldController {...props}>
{controllerProps => (<ReferenceManyManyFieldView
{...props}
{...{children, ...controllerProps}} /> )}
</ReferenceManyManyFieldController>
//ReferenceManyManyFieldController
class ReferenceManyManyFieldController extends Component {
constructor(props){
super(props)
//State to manage sorting and pagination, <ReferecemanyField> uses some props from react-redux
//I discarded react-redux for simplicity/control however in the final solution react-redux might be incorporated
this.state = {
sort: props.sort,
page: 1,
perPage: props.perPage,
total: 0
}
}
componentWillMount() {
this.fetchRelated()
}
//This could be a call to your custom dataProvider with a new REST verb
fetchRelated({ record, resource, reference, showNotification, fetchStart, fetchEnd } = this.props){
//fetchStart and fetchEnd are methods that signal an operation is being made and make active/deactivate loading indicator, dataProvider or sagas should do this
fetchStart()
dataProvider(GET_LIST,`${resource}/${record.id}/${reference}`,{
sort: this.state.sort,
pagination: {
page: this.state.page,
perPage: this.state.perPage
}
})
.then(response => {
const ids = []
const data = response.data.reduce((acc, record) => {
ids.push(record.id)
return {...acc, [record.id]: record}
}, {})
this.setState({data, ids, total:response.total})
})
.catch(e => {
console.error(e)
showNotification('ra.notification.http_error')
})
.finally(fetchEnd)
}
//Set methods are here to manage pagination and ordering,
//again <ReferenceManyField> uses react-redux to manage this
setSort = field => {
const order =
this.state.sort.field === field &&
this.state.sort.order === 'ASC'
? 'DESC'
: 'ASC';
this.setState({ sort: { field, order } }, this.fetchRelated);
};
setPage = page => this.setState({ page }, this.fetchRelated);
setPerPage = perPage => this.setState({ perPage }, this.fetchRelated);
render(){
const { resource, reference, children, basePath } = this.props
const { page, perPage, total } = this.state;
//Changed basePath to be reference name so in children can nest other resources, not sure why the use of replace, maybe to maintain plurals, don't remember
const referenceBasePath = basePath.replace(resource, reference);
return children({
currentSort: this.state.sort,
data: this.state.data,
ids: this.state.ids,
isLoading: typeof this.state.ids === 'undefined',
page,
perPage,
referenceBasePath,
setPage: this.setPage,
setPerPage: this.setPerPage,
setSort: this.setSort,
total
})
}
}
ReferenceManyManyFieldController.defaultProps = {
perPage: 25,
sort: {field: 'id', order: 'DESC'}
}
//ReferenceManyManyFieldView
export const ReferenceManyManyFieldView = ({
children,
classes = {},
className,
currentSort,
data,
ids,
isLoading,
page,
pagination,
perPage,
reference,
referenceBasePath,
setPerPage,
setPage,
setSort,
total
}) => (
isLoading ?
<LinearProgress className={classes.progress} />
:
<Fragment>
{React.cloneElement(children, {
className,
resource: reference,
ids,
data,
basePath: referenceBasePath,
currentSort,
setSort,
total
})}
{pagination && React.cloneElement(pagination, {
page,
perPage,
setPage,
setPerPage,
total
})}
</Fragment>
);
//Assuming the question example, the presentation of many-to-many relationship would be something like
const UserShow = ({...props}) => (
<Show {...props}>
<TabbedShowLayout>
<Tab label='User Roles'>
<ReferenceManyManyField source='users' reference='roles' addLabel={false} pagination={<Pagination/>}>
<Datagrid>
<TextField source='name'/>
<TextField source='code'/>
</Datagrid>
</ReferenceManyManyField>
</Tab>
</TabbedShowLayout>
</Show>
)
//Used <TabbedShowLayout> because is what I use in my project, not sure if works under <Show> or <SimpleShowLayout>, but I think it work since I use it in other contexts
我认为可以改善实现并与React-Admin更好地兼容。在其他参考字段中,数据提取存储在react-redux状态下,在此实现中不是。该关系不会保存在组件之外的任何地方,从而使应用程序无法离线运行,因为无法获取数据,甚至无法订购。
答案 2 :(得分:0)
有一个非常相似的问题。我的solution有点像黑客,但是如果您想要启用ReferenceManyField
,则实现起来会更简单。仅dataProvider
需要修改:
我要在这里重复为当前问题修改的解决方案:
使用库存ReferenceManyField
:
<Show {...props}>
<TabbedShowLayout>
<Tab label="Roles">
<ReferenceManyField reference="roles" target="_nested_users_id" pagination={<Pagination/>} >
<Datagrid>
<TextField source="role" />
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
然后,我修改了我的dataProvider,它是ra-jsonapi-client的分支。
我从index.js
下更改了case GET_MANY_REFERENCE
:
// Add the reference id to the filter params.
query[`filter[${params.target}]`] = params.id;
url = `${apiUrl}/${resource}?${stringify(query)}`;
对此:
// Add the reference id to the filter params.
let refResource;
const match = /_nested_(.*)_id/g.exec(params.target);
if (match != null) {
refResource = `${match[1]}/${params.id}/${resource}`;
} else {
query[`filter[${params.target}]`] = params.id;
refResource = resource;
}
url = `${apiUrl}/${refResource}?${stringify(query)}`;
因此,基本上,我只是将特殊情况下的参数重新映射到url,其中target
与硬编码的正则表达式匹配。
ReferenceManyField
通常会导致dataProvider调用api/roles?filter[_nested_users_id]=1
,而此修改使dataProvider调用api/users/1/roles
。对react-admin来说是透明的。
不太优雅,但它可以正常工作,而且前端似乎没有任何损坏。