Files
tnsu-approval-route/frontend/src/webparts/approvalAdmin/hooks/useFormDraft.ts
T
gorbenko.pavel f3b0f0ad48
Tests / frontend-test (push) Successful in 1m39s
Tests / backend-test (push) Successful in 9m27s
Персист модалок: черновики, восстановление после F5, предупреждение при выходе (#30)
Модалки по жёлтым кнопкам теряли введённое при случайном клике мимо окна.

**Формы создания** (типы заявок, доступ к заявкам, маршруты, шаги маршрута)
- Черновик полей хранится в 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
2026-10-19 04:46:16 +00:00

59 lines
2.6 KiB
TypeScript

import * as React from 'react';
import { readLocalStorage } from './useLocalStorageState';
export interface IFormDraft<T> {
value: T;
setValue: React.Dispatch<React.SetStateAction<T>>;
/** Форма отличается от initial — есть что сбрасывать. */
isDirty: boolean;
/** Вернуть initial и забыть черновик (кнопка «Очистить», успешное сохранение). */
reset: () => void;
}
function removeDraft(key: string): void {
try {
window.localStorage.removeItem(key);
} catch {
// приватный режим — черновика и не было
}
}
/**
* Состояние формы создания, которое переживает закрытие модалки (случайный клик
* по подложке, «Отмена», F5): черновик лежит в localStorage под key, пока его
* не сбросят явно — reset() после успешного сохранения или по кнопке «Очистить».
*
* key === undefined — обычный useState без персиста: так форма редактирования
* существующей записи всегда открывается с актуальными данными с сервера, а не
* с устаревшей правкой.
*/
export function useFormDraft<T>(key: string | undefined, initial: T): IFormDraft<T> {
const [value, setValue] = React.useState<T>(() => (key ? readLocalStorage(key, initial) : initial));
// initial у вызывающих — литерал, пересоздаётся каждый рендер; сравниваем по содержимому.
const initialJson = JSON.stringify(initial);
const isDirty = JSON.stringify(value) !== initialJson;
React.useEffect(() => {
if (!key) return;
if (!isDirty) {
removeDraft(key);
return;
}
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch {
// приватный режим / переполненная квота — не критично, просто не персистим
}
}, [key, value, isDirty]);
// Ключ удаляем синхронно, а не через эффект: после успешного сохранения модалка
// тут же размонтируется, и эффект на новое значение уже не отработает.
const reset = (): void => {
if (key) removeDraft(key);
setValue(JSON.parse(initialJson) as T);
};
return { value, setValue, isDirty, reset };
}