Персист модалок: черновики, восстановление после F5, предупреждение при выходе (#30)
Tests / frontend-test (push) Successful in 1m39s
Tests / backend-test (push) Successful in 9m27s

Модалки по жёлтым кнопкам теряли введённое при случайном клике мимо окна.

**Формы создания** (типы заявок, доступ к заявкам, маршруты, шаги маршрута)
- Черновик полей хранится в localStorage (useFormDraft) и переживает закрытие окна, «Отмену» и F5.
- Окно, открытое в момент F5, открывается снова (usePersistedOpen); закрытие сбрасывает только признак, черновик остаётся.
- Черновик стирается после успешного сохранения или кнопкой «Очистить» в подвале.

**Формы редактирования**
- Не персистятся — открываются с данными сервера.
- При выходе с несохранёнными правками (крестик, «Отмена», клик мимо) — диалог «Закрыть без сохранения?» (isDirty в FormModal).

**Устойчивость к старым данным в localStorage**
- coerceStored приводит сохранённое значение к форме текущего дефолта по полям: новое поле — дефолт, удалённое отбрасывается, значение другого типа и null заменяются дефолтом. Покрыто тестами.

**Прочее**
- Modal и ConfirmDialog рендерятся порталом в body поверх шапки SharePoint — на живой странице верх высокого окна уезжал под шапку.
- Убраны Sass-предупреждения mixed-decls в Card.module.scss.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/30
This commit was merged in pull request #30.
This commit is contained in:
2026-10-19 04:46:16 +00:00
parent 0e6a7064d2
commit f3b0f0ad48
26 changed files with 474 additions and 109 deletions
@@ -11,6 +11,7 @@ import { useRequestTypes } from '../../hooks/useRequestTypes';
import { useTemplateRoutes } from '../../hooks/useTemplateRoutes';
import { usePortalDictionaries } from '../../hooks/usePortalDictionaries';
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
import { usePersistedOpen } from '../../hooks/usePersistedOpen';
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
import { replaceRoute } from '../../routing';
import {
@@ -39,6 +40,8 @@ export interface IRoutesScreenProps {
// Фильтр по типу заявки переживает перезагрузку: администратор обычно работает
// с одним типом заявок подряд, и сбрасывать выбор на каждый заход неудобно.
const ROUTES_FILTER_STORAGE_KEY = 'approvalAdmin.routesFilter';
/** Окно создания маршрута переживает F5 — поля подставляются из черновика (см. RouteFormModal). */
const CREATE_OPEN_STORAGE_KEY = 'approvalAdmin.draftOpen.route';
export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
api,
@@ -69,7 +72,13 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
const routes = useTemplateRoutes(api, filterRequestTypeId ? Number(filterRequestTypeId) : undefined);
const [editing, setEditing] = React.useState<TemplateRoute | undefined>();
// showForm — только редактирование; окно создания восстанавливается после F5 (usePersistedOpen).
const [showForm, setShowForm] = React.useState(false);
const createOpen = usePersistedOpen(CREATE_OPEN_STORAGE_KEY);
const closeForm = (): void => {
setShowForm(false);
createOpen.close();
};
const [copying, setCopying] = React.useState<TemplateRoute | undefined>();
const deleteTarget = useDeleteTarget<TemplateRoute>(item => api.templateRoutes.remove(item.id), routes.reload);
@@ -83,11 +92,13 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
const openCreate = (): void => {
setEditing(undefined);
setShowForm(true);
setShowForm(false);
createOpen.open();
};
const openEdit = (item: TemplateRoute): void => {
setEditing(item);
createOpen.close();
setShowForm(true);
};
@@ -159,7 +170,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
emptyMessage={routes.isLoading ? texts.common.loading : texts.routes.emptyList}
/>
{showForm && (
{(showForm || createOpen.isOpen) && (
<RouteFormModal
api={api}
editing={editing}
@@ -169,7 +180,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
departments={portalDictionaries.departments}
dictionaryLoading={portalDictionaries.isLoading}
onSaved={routes.reload}
onClose={() => setShowForm(false)}
onClose={closeForm}
/>
)}