Поддержка вложения ресурсов - PullRequest
0 голосов
/ 18 января 2019

Мне интересно, возможно ли настроить DataProvider / Resource / List для поддержки URL-адресов REST вроде api/users/1/roles?

Для RESTful API это очень распространенный вариант использования для получения дочерних элементов определенной родительской сущности, но я не могу понять, как настроить React Admin и добиться этого. Я использую пользовательскую сборку DataProvider на базе данных OData.

Я понимаю, что я могу получить роли определенного пользователя по отфильтрованному запросу на api/roles?filter={userId: 1} или что-то в этом роде, но моя проблема в том, что мои пользователи и роли находятся во многих отношениях, поэтому ссылки на отношения хранятся в сводной таблице. , Другими словами, у меня нет ссылки на пользователя в таблице ролей, поэтому я не могу отфильтровать их.

Я наблюдаю за чем-то или есть какой-то подход, который я просто не вижу?

EDIT: REST API встроен в спецификацию OData и поддерживает отношения «многие ко многим» с классической сводной (или промежуточной) таблицей. Эта таблица не предоставляется в API, но используется в URL-адресах, подобных приведенному выше. Поэтому я не могу получить к нему прямой доступ как к ресурсу.

Схема для пользователя - Ролевые отношения тоже выглядят довольно стандартно.

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

Ответы [ 3 ]

0 голосов
/ 22 января 2019

Ваш вопрос уже отвечал здесь , но я хотел бы рассказать вам о моем обходном пути для работы React-Admin с отношениями "многие ко многим".

Как сказано в упомянутом ответе, вы должны расширить DataProvider, чтобы он мог извлекать ресурсы отношения «многие ко многим». Однако вам нужно использовать новый глагол REST, давайте предположим, что GET_MANY_MANY_REFERENCE где-то в вашем приложении. Поскольку разные службы / API REST могут иметь разные форматы маршрутов для извлечения связанных ресурсов, я не пытался создать новый DataProvider, я знаю, что это не лучшее решение, но для коротких сроков это довольно просто.

Мое решение вдохновлялось <ReferenceManyField> и созданием нового компонента <ReferenceManyManyField> для отношений «многие ко многим». Этот компонент извлекает связанные записи в componentDidMount, используя fetch API . При ответе использует данные ответа для построения на объектах, один из которых представляет собой объект с ключами, являющимися идентификаторами записей, и оценивает соответствующий объект записи, а также массив идентификаторов с идентификаторами записей. Это передается дочерним элементам вместе с другими переменными состояния, такими как 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. В других ссылочных полях выборка данных хранится в состоянии реакции-избыточности, в этой реализации это не так. Отношение не сохраняется нигде, кроме компонента, заставляющего приложение не работать в автономном режиме, поскольку не может получать данные, даже упорядочение невозможно.

0 голосов
/ 06 июля 2019

У меня был очень похожий вопрос. Мое решение было более хакерским, но немного проще в реализации, если все, что вам нужно, это включить 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. Это прозрачно для реакции-admin.

Не элегантно, но работает и, похоже, ничего не ломает в передней части.

0 голосов
/ 21 января 2019

TL; DR: По умолчанию React Admin не поддерживает вложенные ресурсы, вам нужно написать пользовательский поставщик данных .

На этот вопрос ответили в прошлом выпуске: maremelab / реагировать-администратор # 261

Подробный ответ

Поставщик данных по умолчанию в React Admin: ra-data-simple-rest.

Как объяснено в документации, эта библиотека не поддерживает вложенные ресурсы, поскольку она использует только имя ресурса и идентификатор ресурса для создания URL ресурса:

Simple REST Data Provider

Чтобы поддерживать вложенные ресурсы, вы должны написать свой собственный поставщик данных.

Поддержка вложенных ресурсов - это повторяющаяся функция запрос , но в то время основная команда не хочет обрабатывать эту нагрузку.

Я настоятельно рекомендую собрать свои силы и написать внешнего поставщика данных и опубликовать его как ra-data-odata поставщика. Это было бы отличным дополнением, и мы будем рады помочь вам с этим внешним пакетом.

...