f3b0f0ad48
Модалки по жёлтым кнопкам теряли введённое при случайном клике мимо окна. **Формы создания** (типы заявок, доступ к заявкам, маршруты, шаги маршрута) - Черновик полей хранится в 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
59 lines
2.6 KiB
TypeScript
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 };
|
|
}
|