Разработка личного кабинета клиента на React: фильтры и поиск

Продолжаем разработку кабинета пользователя с использованием React Admin. Ранее мы уже реализовали запросы к API для отображения пользователей, разбирались как настраивать вывод колонок с информацией используя типы полей, настраивали стили отображения и обрабатывали взаимосвязи. Так же добавляли функционал создания и редактирования записей, аутентификации пользователей.

Теперь давайте реализуем другой, не менее важный, функционал для любой панели клиента (админа). Это поиск и фильтрация отображаемых данных.

Добавляем функцию поиска в панель клиента React Admin

Ранее мы реализовали несколько таблиц с данными, в т.ч. и для вывода публикаций. У нас есть одна проблема. На практике, если количество записей будет увеличиваться, нам будет сложно искать нужные, а значит нам необходима функция поиска.

В React Admin можно использовать компонент Input для создания поискового поля. Для начала создаём компонент <Filter>. Затем добавляем его в список List, используя filters prop:

// in src/posts.js
//...
import { List, Datagrid, TextField, ReferenceField, EditButton, Edit, SimpleForm, TextInput, ReferenceInput, SelectInput, Create, Filter } from 'react-admin';

const PostFilter = (props) => (
  <Filter {...props}>
    <TextInput label='Search' source='q' alwaysOn />
    <ReferenceInput label='User' source='userId' reference='users' allowEmpty>
      <SelectInput optionText='name' />
    </ReferenceInput>
  </Filter>
);

export const PostList = props => (
  <List filters={<PostFilter />} {...props}>
//   ...
  </List>
);

Первый фильтр, ‘q’, предлагает полнотекстовый поиск. Он alwaysOn, т.е. всегда появляется на экране. Пользователь может добавить поле фильтра по userId, используя кнопку “ADD FILTER”. Так как применен <ReferenceInput>, то фильтр предлагает выбор уже существующих пользователей.

Фильтры являются “search-as-you-type”, т.е. по мере ввода информации в поле происходит немедленное обновление данных через API запросы.

Обратите внимание! Свойство label может быть применено к любому полю для настройки отображаемой подписи.

Настройка значков меню

В боковом меню отображается одинаковый значок как для сообщений, так и для пользователей. Настройка значка меню – это просто передача атрибута значка каждому <Resource>:

// in src/App.js
import PostIcon from '@material-ui/icons/Book';
import UserIcon from '@material-ui/icons/Group';

const App = () => (
  <Admin dataProvider={dataProvider} authProvider={authProvider}>
    <Resource name='posts' list={PostList} edit={PostEdit} create={PostCreate} icon={PostIcon} />
    <Resource name='users' list={UserList} edit={UserEdit} create={UserCreate} icon={UserIcon} />
  </Admin>
);

Установка домашней страницы

По умолчанию, домашней страницей панели будет первый элемент Resource. Если это необходимо изменить, то это можно сделать путем передачи свойства dashboard в компонент <Admin>:

// in src/Dashboard.js
import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';

export default () => (
  <Card>
    <CardHeader title='Welcome to Admin' />
    <CardContent>Usefull content will appear here later!</CardContent>
  </Card>
);
// in src/App.js
import Dashboard from './Dashboard';

const App = () => (
  <Admin dashboard={Dashboard} dataProvider={dataProvider} authProvider={authProvider}>
    // ...
  </Admin>
);

Поддержка на мобильных устройствах

React-Admin сам по себе выполнен с использованием responsive дизайна. Если вы измените ширину браузера, то колонки будут автоматически адаптироваться под ширину, а боковое меню схлопнется при достижении ширины менее 600 пикселей.

Но адаптивного макета недостаточно для создания адаптивного приложения. Компоненты Datagrid хорошо работают на настольных ПК, но абсолютно не адаптированы для мобильных устройств. Если ваша админ панель должна использоваться на мобильных устройствах, вам придется предоставить альтернативный компонент для небольших экранов.

Во-первых, вы должны знать, что вам не стоит использовать компонент <Datagrid> как дочерний элемент <List>. Вы можете использовать любой другой компонент, который вам нравится. Например, компонент <SimpleList>:

// in src/posts.js
import React from 'react';
import { List, SimpleList } from 'react-admin';

export const PostList = (props) => (
  <List {...props}>
    <SimpleList
      primaryText={record => record.title}
      secondaryText={record => `${record.views} views`}
      tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
    />
  </List>
);

Компонент <SimpleList> использует material-ui компоненты <List> и <ListItem> и принимает свойства primaryTextsecondaryText иtertiaryText.

Такой вариант удобен на мобильных устройствах, но пользователи настольных компьютеров останутся недовольны. Лучшим компромиссом будет использование <SimpleList> на маленьких экранах и <Datagrid> на более крупных. для это необходимо применить хук useMediaQuery следующим образом:

// in src/posts.js
import { useMediaQuery } from '@material-ui/core';

export const PostList = (props) => {
  const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm'));
  return (
    <List {...props}>
      {isSmall ? (
        <SimpleList
          primaryText={record => record.title}
          secondaryText={record => `${record.views} views`}
          tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
        />
      ) : (
        <Datagrid>
          <TextField source='id' />
          <ReferenceField label='User' source='userId' reference='users'>
            <TextField source='name' />
          </ReferenceField>
          <TextField source='title' />
          <TextField source='body' />
          <EditButton />
        </Datagrid>
      )}
    </List>
  );
};

Это работает именно так, как вы ожидаете. Таким образом, наша задача заключается в правильном использовании useMediaQuery() на странице, а react-admin побеспокоится об изменении разметки страницы.

В приведенном коде есть один нюанс, который можно заметить при переключении на SimpleList. Это Invalid Date и undefined views под каждой записью. Это произошло потому, что используемое фиктивное API JSONRestServer не поддерживает views и published_at поля для публикаций. Необходимо использовать реальное API, которое передает данные о просмотрах и дате публикации, чтобы данные отображались корректно.

Подключение к реальному API

В реальных проектах диалект вашего API (REST? GraphQL? Или что-то еще?) не будет соответствовать JSONPlaceholder. Написание Data Provider (провайдера данных) – это, вероятно, первое, что будет необходимо сделать, чтобы заставить работать react-admin. В зависимости от вашего API, это может потребовать нескольких часов дополнительной работы.

React-admin делегирует каждый запрос объекту Data Provider, который действует как адаптер между админ частью и API. Это делает react-admin совместимым с множеством API-диалектов, включая работу с конечными точками на различных доменах.

Для примера, представим что у нам есть конечная точка my.api.url REST API, которая ожидает следующие запросы:

ДействиеОжидаемый API запрос
Получить списокGET http://my.api.url/posts?sort=['title','ASC']&range=[0, 24]&filter={title:'bar'}
Получить одну записьGET http://my.api.url/posts/123
Получить несколько записейGET http://my.api.url/posts?filter={id:[123,456,789]}
Получить связанные записиGET http://my.api.url/posts?filter={author_id:345}
Создать записьPOST http://my.api.url/posts/123
Обновить записьPUT http://my.api.url/posts/123
Обновить записиPUT http://my.api.url/posts?filter={id:[123,124,125]}
Удалить записьDELETE http://my.api.url/posts/123
Удалить записиDELETE http://my.api.url/posts?filter={id:[123,124,125]}

React-admin вызывает Data Provider с одним из допустимых методов, ожидая в ответ Promise. Эти методы имеют название getList, getOne, getMany, getManyReference, create, update, updateMany, delete и deleteMany. Задача Data Provider’а запустить HTTP-запросы и трансформировать ответы в формат, понятный для react-admin.

С учетом данных из таблицы выше, код для провайдера данных API my.api.url будет выглядеть следующим образом:

import { fetchUtils } from 'react-admin';
import { stringify } from 'query-string';

const apiUrl = 'https://my.api.com/';
const httpClient = fetchUtils.fetchJson;

export default {
    getList: (resource, params) => {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        const query = {
            sort: JSON.stringify([field, order]),
            range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
            filter: JSON.stringify(params.filter),
        };
        const url = `${apiUrl}/${resource}?${stringify(query)}`;

        return httpClient(url).then(({ headers, json }) => ({
            data: json,
            total: parseInt(headers.get('content-range').split('/').pop(), 10),
        }));
    },

    getOne: (resource, params) =>
        httpClient(`${apiUrl}/${resource}/${params.id}`).then(({ json }) => ({
            data: json,
        })),

    getMany: (resource, params) => {
        const query = {
            filter: JSON.stringify({ id: params.ids }),
        };
        const url = `${apiUrl}/${resource}?${stringify(query)}`;
        return httpClient(url).then(({ json }) => ({ data: json }));
    },

    getManyReference: (resource, params) => {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        const query = {
            sort: JSON.stringify([field, order]),
            range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
            filter: JSON.stringify({
                ...params.filter,
                [params.target]: params.id,
            }),
        };
        const url = `${apiUrl}/${resource}?${stringify(query)}`;

        return httpClient(url).then(({ headers, json }) => ({
            data: json,
            total: parseInt(headers.get('content-range').split('/').pop(), 10),
        }));
    },

    update: (resource, params) =>
        httpClient(`${apiUrl}/${resource}/${params.id}`, {
            method: 'PUT',
            body: JSON.stringify(params.data),
        }).then(({ json }) => ({ data: json })),

    updateMany: (resource, params) => {
        const query = {
            filter: JSON.stringify({ id: params.ids}),
        };
        return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
            method: 'PUT',
            body: JSON.stringify(params.data),
        }).then(({ json }) => ({ data: json }));
    },

    create: (resource, params) =>
        httpClient(`${apiUrl}/${resource}`, {
            method: 'POST',
            body: JSON.stringify(params.data),
        }).then(({ json }) => ({
            data: { ...params.data, id: json.id },
        })),

    delete: (resource, params) =>
        httpClient(`${apiUrl}/${resource}/${params.id}`, {
            method: 'DELETE',
        }).then(({ json }) => ({ data: json })),

    deleteMany: (resource, params) => {
        const query = {
            filter: JSON.stringify({ id: params.ids}),
        };
        return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
            method: 'DELETE',
            body: JSON.stringify(params.data),
        }).then(({ json }) => ({ data: json }));
    }
};

Здесь fetchUtils.fetchJson() это только ярлык для fetch().then(r => r.json()) дающий контроль над кодами HTTP-ответов и возможность обработки ошибок 4xx или 5xx вызовом HTTPError. Вы можете использовать fetch()  напрямую, если это необходимо.

Переключение на использование данного провайдера вместо предыдущего решается всего лишь лишь заменой dataProvider:

// in src/App.js
import dataProvider from './dataProvider';

const App = () => (
    <Admin dataProvider={dataProvider}>
        // ...
    </Admin>
);

Заключение

На этом мы заканчиваем рассматривать основные функции и возможности React-admin. Стоит помнить, что React Admin был создан с учетом возможностей для индивидуальной настройки. Вы можете заменить любой компонент своим собственным. Для продолжения изучения возможностей React Admin стоит обратиться к документации React Admin. Так же загляните в документацию по компонентам Material-UI.

Предыдущие части материала: Разработка личного кабинета клиента на React и Авторизация и стайлинг личного кабинета клиента на React.

Добавить комментарий

Ваш адрес email не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.