Admin-gui development guide
Создание страниц
В директории pages
могут лежать только файлики, которые формируют URL страниц. Почитайте об этой особенности внимательно в документации Basic Features: Pages | Next.js
Для того, чтобы рядом с файлом страницы можно было располагать другие файлы: компоненты, скрипты, типы, что-то еще, что относится только к этой странице, нужно
- в директории
./views
создать страницу - в
./pages
создать link-file типа
export {default, getServerSideProps} from '@views/home'
Это позволит нам иметь любую структуру в категории ./views/Home, например. Будет удобно искать файлы, соответствующие конкретной странице
- Все страницы в
./pages
должны представлять из себя “link-file” к./views/page
Установка зависимостей
Обязательно указывать exact версию пакета yarn add package -E
или yarn add package -DE
Это сделано для того, чтобы при развороте на другой машине не подтянулась последняя версия какого-нибудь пакета. Особенно актуально, если разработчики этого пакета не придерживаются принципов семантического версионирования.
Добавление переменных окружения
Раздел о работе переменных окружения в next есть в официальной документации Basic Features: Environment Variables | Next.js. Стоит отметить, что переменные окружения нельзя использовать в клиентском коде, потому что он намертво вшивается при сборке (можно только в очень исключительных случаях, когда точно понимаешь, что делаешь). Если возникла необходимость, лучше посоветуйся с коллегами. Возможно, что-то делаешь не так.
Вывод сообщений об ошибках, информационных сообщений
В проекте реализованы кастомные алерты (выплывающие справа модальные окна). Их следует использовать для информирования пользователя об ошибках или успехе при обновлении данных.
Для удобства подготовлены хуки useError
, useSuccess
с предустановленными темами модалки, тянутся они из @context/modals
либо можно использовать метод appendModal
из хука useModalsContext
.
import { useError,useSuccess } from '@context/modal';
const Page = () => {
const { appendModal } = useModalsContext();
const { data: menuData, isLoading, error } = useMenu();
const addFile = useOrdersAddFile();
// обработаем ошибку метода получения меню
useError(error);
// обработаем ошибку метода добавления файла
useError(addFile.error);
// покажем, что файл добавлен успешно
useSuccess(addFile.status === 'success' ? ModalMessages.SUCCESS_UPDATE : '');
return <>
<Button onClick={() =>
// просто на клик по кнопке выведем модальное окно-предупреждение
appendModal({ message: 'просто уведомление, theme: 'warning' })
}>Текст</Button>
</>
}
Учтите, для каждой ошибки и успеха нужен свой собственный хук.
Их нельзя комбинировать вот так:
💩
useError(addFile.error || error);
useSuccess(addFile.status === 'success' ? ModalMessages.SUCCESS_UPDATE: '')
Работа с фильтрами
Все фильтры и табы (первого уровня вложенности) должны сохранять get-параметр в URL. Для этого предусмотрен хук useFilterHelper для фильтров и useTabs для табов.
Пример использования хука useFilterHelper
Строго говоря initialValues, возвращамые хуком, - это не обязательно начальные значения. При отправке формы фильтров изменится url, и изменятся выходные значения из хука useFilterHelper, поэтому initialValues можно использовать в качестве values в местах, где нужно получить значения формы.
import { useFilterHelper } from '@scripts/hooks';
const emptyInitialValues = {
number_like: '',
created_at: '',
status: [],
payment_method: [],
price_from: '',
price_to: '',
seller_id: [],
store_id: [],
delivery_type: [],
delivery_service: [],
is_canceled: '',
is_problem: '',
is_require_check: '',
manager_comment_like: '',
};
const Page = () => {
const { initialValues, URLHelper, filtersActive } = useFilterHelper(emptyInitialValues);
return (<>
{filtersActive ? 'Фильтры активны' : 'Ни один фильтр не выбран'}
<Form initialValues={initialValues} onSubmit={URLHelper}>
/** children */
</Form>
</>)
}
Пример использования хука useTabs
import { useTabs } from '@scripts/hooks';
const Page = () => {
const { getTabsProps } = useTabs();
return (
<Tabs {...getTabsProps()}>
<Tabs.List>
<Tabs.Tab>Информация</Tabs.Tab>
<Tabs.Tab>Адреса</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<Information />
</Tabs.Panel>
<Tabs.Panel>
<Addresses />
</Tabs.Panel>
</Tabs>)
}
Внимание. Для того, чтобы query-параметры из урла попадали в хук useFiltersHelper или useTabs ,при первом рендере страницы с фильтрами или табами, нужно описать ф-цию getServerSideProps. Без нее объект query из useRouter будет пустым. Подробнее об этой ф-ции читай в документации NextJS. Не забудьте ее экспортировать на уровне страницы.
export async function getServerSideProps() {
return {
props: {},
};
}
Адаптивность интерфейсов
На момент написания этого руководства адаптивность проработана для общих элементов интерфейса страницы: шапки, меню.
Если при создании страницы/компонента имеется макет адаптива, то учитывать его.
Если макета адаптива нет, то стараться перестраивать интерфейс самостоятельно, учитывая следующее:
- интерфейсом должно быть возможно пользоваться с мобильных устройств,
- страница не должна зумиться,
- для таблиц допустимо использовать горизонтальный скролл,
- поля формы просто перестраивать друг под друга.
Доступность интерфейсов
Разработку интерфейсов вести с "accessibility in mind". Держать в голове, как минимум, управление с клавиатуры и восприятие разметки скринридерами, для поддержки которых должна соблюдаться семантичность разметки и проставляться нейминги.
Ключевые моменты для семантики:
- использование
landmarks
- разметки блоков через теги main, aside, nav, article и пр. вместо нейтральных div - использование каскадной структуры заголовков без пропусков уровней - скринридеры строят меню из этих заголовков
- соблюдение нормативов, определённых спецификацией, при построении базовых элементов, в том числе это включает добавление aria-атрибутов
Нейминги
Нейминги важны, т.к. скринридеру нужен текст, чтобы озвучить его пользователю. По этой причине всем изображениям прописывается атрибут alt
, для svg, используемых внутри кнопок, присваивается title
, инпуты связываются с label
и т.д.
Имена могут указываться напрямую вложением текста внутрь тега, через атрибуты title
и alt
(для изображений), а также через aria-label
и aria-labelledby
. Проверить присваемое элементу имя можно через инструменты разработчика в Chrome: F12 -> Elements -> Accessibility
.
VisuallyHidden
Бывает необходимо скрыть текст визуально, когда он и так понятен из контекста, но оставить его доступным для AT. Или предоставить пользователям AT дополнительную информацию, которая для этого должна присутствовать в разметке, но визуально показываться не должна. Для этих целей служит компонент <VisuallyHidden />
из GDS, в который нужно оборачивать такие элементы вместо display: none
или visibility: hidden
.
Контроль качества
A11Y включает в себя множества правил, которым необходимо следовать при разработке и проектировании интерфейсов. В этом документе затронута лишь часть, наиболее важная для фронтенда. Эти правила не взяты с неба, а строго определяются спецификацией. Для их контроля существуют чеклисты, наглядно их перечисляющие: WCAG Checklist.
Для компонентов вне Storybook, например целых страниц, можно для тех же целей использовать браузерные расширения. Тот же axe
может быть встроен в качестве chrome extension.
Написание тестов
В настоящее время написание тестов не является обязательным, но возможность такая есть. К проекту подлючены связка Jest + React Testing Library, так что при желании разработчик может покрыть тестами свой код.
Покрытие тестами админ-панели есть в планах развития платформы.
Управление состоянием (state management)
В admin-gui нет стейт-менеджера в привычном виде. На проекте не использует Redux, MobX или какой-то другой стейт-менеджер.
Вместо этого используется другой подход, лишенный boilerplate-кода, простой в освоении и удобный в использовании React Query. Ему посвящен отдельный раздел.
Однако, несмотря на все преимущества ReactQuery иногда возникает необходимость хранить какие-то данные глобально. На нашем проекте это в основном состояния открыт/закрыт для глобальных попапов. Для этого используется обычный react-контекст и провайдер c говорящим названием CommonProvider
.