В качестве frontend используется React 17 (typescript), Recoil, Material-UI 5
Репозиторий: https://github.com/levtrilev/surecrm - проект является открытым. Вы можете свободно копировать и использовать его. Данное описание поможет разобраться в коде.
Соглашение об именовании объектов: переменные и папки именуются, начиная с маленькой латинской буквы. Объекты, компоненты и типы данных именуются, начиная с большой латинской буквы, с использованием нотации camelCase. (исключения – объекты, одноименные с объектами БД, например, поля JSON-объектов, где_используется_кебаб_нотация.)
Структура папок React JS проекта
После создания проекта командой «yarn create react-app surecrm --template typescript» в выбранной папке создана папка проекта «surecrm», внутри которой в папке \\surecrm\src созданы папки проекта: «components», «shared», «state». Из стандартных файлов после создания проекта в корне проекта (то есть в папке \\surecrm\src), разработка происходит в файлах App.tsx, index.tsx и types.d.ts. В основном, интенсивно пополняется файл types.d.ts – здесь объявляются все новые типы данных, например, ProductType, OrderType.
Некоторые изменения внесены в \\surecrm\public\index.html. Остальные разработки происходят в папках \\surecrm\src\components, \\surecrm\src\shared и \\surecrm\src\state (далее упоминаю их как components, shared и state).
В папке shared добавляются объекты, общие для всех бизнес-компонентов. Например объект (подпапка) navigation, файл appConsts.ts – общие для всего приложения константы..
Для каждой бизнес-сущности создается папка в \\surecrm\src\components, например: components\customer или components\customerCategory.
Работа с данными - подпапка data. Внутри папки бизнес-компонента находится подпапка data, где располагаются два файла: имяDao.ts и имяState.ts, например, productDao.ts – здесь собраны функции доступа к данным (Dao – data access objects) и productState.ts – здесь собраны объявления Recoil state объектов (atom, selector, – см getting started по Recoil https://recoiljs.org/docs/introduction/getting-started ) и их дефолтные значения.
Основные файлы-компоненты бизнес-объекта (на примере Product):
ProductGrig.tsx
Компонент – списочное представление товаров
ProductEdit.tsx
Компонент-обертка над формой редактирования карточки товара, содержащий основные бизнес-функции сохранения, удаления, обновления
ProductFormDialog.tsx
Компонент-форма редактирования карточки товара с техническими функциями «при изменении поля», debounce итд
ProductSelector.tsx
Компонент – окно для выбора и поиска товаров в случаях, когда в формах редактирования других сущностей (заказ) нужно выбрать товар из справочника
Объекты состояния – файл data\productState.ts
Замечание: Здесь подробно описывается реализация объектов для одной бизнес-сущности Product, но другие бизнес-сущности обслуживаются такими же объектами. Например, достаточно скопировать папку бизнес-сущности, аккуратно переименовать все переменные и имена файлов, типов и объектов, например, currentProductIdState на currentCustomerIdState и все заработает. Естественно с разницей на то, что у каждой бизнес-сущности есть свои (в соответствии со структурой БД их надо добавить в данные и в верстку формы) специфические поля кроме id и name.
Про Recoil: atomFamilyотличается от atom и selectorFamily отличается от selector тем, что в случае Family можно инициализировать одновременно несколько одноименных объектов (открыть несколько товаров с разными id, например). Поэтому для единичных объектов предусмотрены Family, а для списочных (список товаров) инициализировать одновременно два и более списков товаров не предусмотрено.
Атомы:
currentProductIdState – atomFamily – id товара, выбранного пользователем для открытия на редактирование
newProductState – atom, технический, для использования непосредственно в верстке, использующийся для создания нового или редактирования единичного объекта (товара в данном случае). Если создается новый объект, то используется дефолтное значение атома (см. newProductDefault). Если редактируется имеющийся объект в этот атом загружается имеющееся значение из селектора. Об открытии на редактирование подробнее см. ниже ProductEdit.
Селекторы:
productsQuery – selector – список товаров - возвращается результат запроса из одной таблицы, например такого: const response = await fetch(«https://surecrm.org/view_products», …);
productsFullQuery - selector – список товаров - возвращается результат запроса из группы таблиц, например такого: const response = await fetch(«https://surecrm.org/view_products?select=*,product_categories:view_product_categories(id,name)», …). Таблица товаров содержит «голый» id категории товара, а такой запрос возвращает не только id категории товара, но и название категории товара в случае, если в таблице товаров id категории товара определен как Foreign Key
productQuery – selectorFamily – возвращает данные товара, имеющего id, значение которого установлено в атоме currentProductIdState
Компонент основного представления списка – файл ProductGrig.tsx
Определен функциональный компонент ProductsGrid() – без пропсов.
Вёрстка:
Используется компонент DataGrid из библиотеки MaterialUI (см. https://mui.com/x/react-data-grid/getting-started/ ), используются общие компаненты (из папки shared) TopDocsButtons
и YesCancelDialog. Это простые компоненты. TopDocsButtons – кнопки над списком Обновить список, Создать новый (элемент), Удалить(выделенные элементы). YesCancelDialog – при удалении и при выходе без сохранения введенной информации запрашивается подтверждение.
Также здесь вызывается сложный (будет описан отдельно) компонент для создания/редактирования товара ProductEdit.
Основные функции:
editProductAction, copyProductAction, deleteProductAction – назнначение функций понятно по названию.
Прокомментирую функцию editProductAction (остальные проще). Функция вызывается по нажатию кнопки Редактировать в списке товаров. В этом случае в нее передается id товара. Либо по кнопке Создать над списком. В этом случае в нее передается id товара = 0.
const editProductAction= (id: number) => {
if(id=== 0) { // если создается новый товар
setNewProduct(newProductDefault); // создаем новый пустой товар
setCurrentProductId(0); // обнуляем текущий id товара, чтобы поле название товара тоже было пустым
setCurrentProdCategId(0); // обнуляем текущий id категории товара, чтобы поле название категории товара тоже было пустым
editmodeText= 'создание нового'; // в карточке товара пользователь видит в каком режиме он работает – редактирование или создание
} else {
editmodeText = 'редактирование';
setCurrentProductId(id); // чтобы по id «подтянуть» в селектор productQuery название товара
const product= products.find(x => x.id=== id) as ProductFullType; // в массиве товаров найти элемент с указанным id
setNewProduct(fullProductToProduct(product)); // найденный объект «загружается» в переменную, где он будет редактироваться
setCurrentProdCategId(product.category_id); // чтобы по id «подтянуть» в соответствующий селектор название категории товара
}
setOpenEditModal(true); // устанавливается «флаг», по которому открывается модальное окно редактирования
};
Компонент редактирования Товара – файл ProductEdit.tsx
Определен функциональный компонент ProductEdit со следующими пропсами:
interface Props {
modalState: boolean; // сейчас открывается только модально. Оставил для развития – если понадобится открывать несколько
setFromParrent: SetOpenModal; // Флаг открытия/закрытия окна
editmodeText: string; // строка для вывода пользователю о режиме работы редактирование/новый
outerEditContext: string; // контекст редактирования для селекторов и атомов – на случай, если будут открыты несколько аналогичных объектов
}
Вёрстка: вызывается компонент с формой редактирования ProductFormDialog и диалог подтверждения YesNoCancelDialog
Основные функции:
updateProduct – основное: в зависимости от режима создание/новый вызывается функция работы с данными , postNewProduct(newProduct) – для вставки новой записи в таблицу БД или putUpdatedProduct(newProduct) – для обновления записи
И дополнительно устанавливается setIsModified(false) в знак того, что сохранение не требуется. В этом случае при выходе из окна редактирования пользователю не будет задан вопрос о подтверждении выхода с потерей результатов ввода данных
useEffect – зависящая от currentProdCategId, – в результате выбора пользователя при вызове справочника категорий, устанавливает в карточке товара id категории товара;
useEffect – зависящая от yesNoCancel, – срабатывает по результату взаимодействия с пользователем при выводе диалога подтверждения удаления или выхода без сохранения введенных данных.
handleClose - обрабатывается действие закрытия(выхода). Если данные пользователем вводились/изменялись – выводится диалог подтверждения выхода без сохранения. Если не вводились – закрывается модальное окно.
Компонент - форма редактирования Товара – файл ProductFormDialog.tsx
Определен функциональный компонент ProductFormDialog. Назначение пропсов соответствует их названиям.
const isInitialMount = useRef(-2);
используется прием, когда в функциям UseEffect() отслеживается первичный mount – в этот момент значение isInitialMount инкрементируется. Таких функций здесь две. Когда значение достигает 0, значит это уже целевое срабатывание «эффекта».
Еще прокомментирую применение debounce – приёма, предотвращающего срабатывание обработки onchange при каждом вводе очередного символа в полях ввода. Конструкция получилась громоздкая, поэтому для каждого поля код заключен в
// #region onProductNameChange
// #endregion onProductNameChange
для возможности коллапса строк кода. Каждый такой фрагмент включает 1)собственно обработчик события ввода, который формирует переменную с введенным значением 2)функцию-вызов хука обновления целевого объекта (обернутого в debounce 1 раз в секунду), 3) useEffect для запуска хука по изменению вводимого значения. Собственную функцию debounce написал и оставил на всякий случай в shared. Но позже обнаружил эту функцию в библиотеке MUI и использую её.
Модальное окно реализовано как Druggable (можно перетаскивать мышкой). Я не уверен, что следует оставить этот вариант, поскольку пропала возможность в полях текстового ввода, например, мышкой выделить фрагмент текста, - вместо этого начинается перетаскивание окна. Для Druggable реализации служат объекты:
const paperComponentEnabledRef = useRef(PaperComponentEnabled);
const paperComponentDisabledRef = useRef(PaperComponentDisabled);
const paperComponentRef = useRef(PaperComponentEnabled);
const enableDruggableParent = () => {
paperComponentRef.current = paperComponentEnabledRef.current;
};
Остальные понятны из наименований.
Компонент – окно поиска и выбора Товара из справочника – файл ProductSelector.tsx
Определен функциональный компонент ProductSelector. Назначение пропсов соответствует их названиям.
Компонент является оберткой, т.е. готовит данные для пропсов и вызывает единый для всех бизнес-сущностей компонент SelectorBodySearch, находящийся в папке shared – его копию не надо реализовывать при создании объектов новой бизнес-сущности.
Вызывается также компонент ProductEdit в случае, если пользователь в процессе работы со справочником товаров нажмет кнопку Редактировать.
Напоминаю, что проект является открытым - Вы можете свободно копировать и использовать его. Данное описание поможет разобраться в коде. Продолжение следует