支持资源嵌套

Support for resource nesting

我想知道,是否可以配置 DataProvider/Resource/List 以支持像 api/users/1/roles 这样的 REST url?

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

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

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

编辑: REST API 内置于 OData 规范中,它支持与经典枢轴(或中间枢轴)table 的多对多关系。此 table 未在 API 中公开,但在上述 url 中使用。所以我无法直接将其作为资源访问。

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

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

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提供程序一样发布。这将是一个很好的补充,我们很荣幸能帮助您处理该外部包。

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

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

我的解决方案是从 <ReferenceManyField> 中汲取灵感,并为多对多关系构建一个新组件 <ReferenceManyManyField>。此组件使用 fetch APIcomponentDidMount 上获取相关记录。 On response 使用响应数据构建对象,一个数据是一个对象,其键是记录 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 状态中,在此实现中不是。除了组件之外,该关系不会保存在任何地方,这使得应用程序无法离线工作,因为无法获取数据,甚至无法订购。

有一个非常相似的问题。我的 更像是 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 的一个分支。 我把case GET_MANY_REFERENCE下的index.js改成了:

      // 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.

是透明的

不够优雅,但可以正常工作,而且似乎不会破坏前端的任何东西。