5

我想知道,是否可以配置 DataProvider/Resource/List 以支持 REST url,例如api/users/1/roles

对于 RESTful API,获取某个父实体的子实体是非常常见的用例,但我不知道如何设置 React Admin 并实现这一点。我正在使用基于 OData 规范后端的自定义 DataProvider 构建。

我知道我可以通过过滤请求api/roles?filter={userId: 1}或类似的方式获取某些用户的角色,但我的问题是我的用户和角色处于多对多关系,因此关系引用存储在数据透视表中。换句话说,我没有角色表中的用户参考,所以我无法过滤它们。

我是在监督什么,还是有一些我根本看不到的方法?

编辑:REST API 内置于 OData 规范中,它支持与经典数据透视表(或中间表)的多对多关系。此表未在 API 中公开,但在上述 URL 中使用。所以我不能直接将它作为资源访问。

用户模式 ​​- 角色关系看起来也很标准。

|----------|    |-----------|     |--------|
| USER     |    | User_Role |     | Role   |
|----------|    |-----------|     |--------|
| Id       |-\  | Id        |   /-| Id     |
| Login    |  \-| UserId    |  /  | Name   |
| Password |    | RoleId    |-/   | Code   |
|----------|    |-----------|     |--------|
4

3 回答 3

8

TL;DR:默认情况下,React Admin 不支持嵌套资源,您必须编写自定义数据提供程序

这个问题在过去的问题上得到了回答:maremelab/react-admin#261

详细解答

React Admin 中的默认数据提供者是ra-data-simple-rest.

如其文档中所述,此库不支持嵌套资源,因为它仅使用资源名称和资源 ID 来构建资源 URL:

简单的 REST 数据提供者

为了支持嵌套资源,您必须编写自己的数据提供程序。

嵌套资源支持是一个经常性的 功能 请求,但当时,核心团队不想处理这种工作负载。

我强烈建议收集你的力量并编写一个外部数据提供者并像ra-data-odata提供者一样发布它。这将是一个很好的补充,我们将很荣幸为您提供该外部软件包。

于 2019-01-21T15:04:28.517 回答
4

你的问题已经在这里回答了,但我想告诉你我的解决方法,以便 React-Admin 处理多对多关系。

正如在提到的答案中所说,您必须扩展 DataProvider 才能获取多对多关系的资源。但是,您需要使用新的 REST 动词,让我们假设GET_MANY_MANY_REFERENCE您的应用程序的某个地方。由于不同的 REST 服务/API 可以具有不同的路由格式来获取相关资源,因此我没有费心尝试构建新的 DataProvider,我知道这不是一个很好的解决方案,但对于较短的截止日期来说是相当简单的。

我的解决方案是获取灵感并为多对多关系<ReferenceManyField>构建一个新组件。该组件使用fetch API<ReferenceManyManyField>获取相关记录。On response 使用响应数据构建对象,其中一个数据是一个对象,其键是记录 ID,值是相应的记录对象,以及一个具有记录 ID 的 ids 数组。这与其他状态变量(如 page、sort、perPage、total)一起传递给子项,以处理数据的分页和排序。请注意,更改 Datagrid 中数据的顺序意味着将向 API 发出新请求。这个组件分为一个控制器和一个视图,比如componentDidMount<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 状态中,在此实现中不是。除了使应用程序无法离线工作的组件之外,该关系没有保存在任何地方,因为无法获取数据,甚至无法进行排序。

于 2019-01-22T11:15:18.843 回答
3

有一个非常相似的问题。我的解决方案更像是一种 hack,但如果您只想启用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.jscase 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)}`;

target所以基本上我只是将参数重新映射到 url 以匹配硬编码正则表达式的特殊情况。

ReferenceManyField通常会导致 dataProvider 调用api/roles?filter[_nested_users_id]=1,而此修改会改为调用 dataProvider api/users/1/roles。它对 react-admin 是透明的。

不优雅,但它可以工作,并且似乎不会破坏前端的任何东西。

于 2019-07-06T02:29:02.523 回答