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
45 lines
2.0 KiB
TypeScript
45 lines
2.0 KiB
TypeScript
import { coerceStored } from './useLocalStorageState';
|
|
|
|
describe('coerceStored', () => {
|
|
const initial = { name: '', count: 0, flag: false, tags: [] as string[], nested: { a: '' } };
|
|
|
|
it('подставляет дефолт для полей, добавленных после сохранения', () => {
|
|
expect(coerceStored(initial, { name: 'x' })).toEqual({ ...initial, name: 'x' });
|
|
});
|
|
|
|
it('отбрасывает ключи, которых в форме больше нет', () => {
|
|
expect(coerceStored(initial, { name: 'x', removed: 1 })).not.toHaveProperty('removed');
|
|
});
|
|
|
|
it('заменяет значение другого типа дефолтом', () => {
|
|
expect(coerceStored(initial, { name: 5, tags: { a: 1 }, flag: 'yes', nested: 'oops' }))
|
|
.toEqual(initial);
|
|
});
|
|
|
|
it('принимает массив нужного типа и мёржит вложенный объект', () => {
|
|
expect(coerceStored(initial, { tags: ['a'], nested: {} }))
|
|
.toEqual({ ...initial, tags: ['a'] });
|
|
});
|
|
|
|
it('не падает на null и примитиве вместо объекта', () => {
|
|
expect(coerceStored(initial, null)).toEqual(initial);
|
|
expect(coerceStored(initial, 'x')).toEqual(initial);
|
|
});
|
|
|
|
it('для примитивного initial проверяет тип', () => {
|
|
expect(coerceStored('', 5)).toBe('');
|
|
expect(coerceStored('', 'abc')).toBe('abc');
|
|
expect(coerceStored<number[]>([], [1, 2])).toEqual([1, 2]);
|
|
});
|
|
|
|
it('для boolean-флага отбрасывает значение другого типа', () => {
|
|
expect(coerceStored(false, true)).toBe(true);
|
|
expect(coerceStored(false, 'true')).toBe(false);
|
|
});
|
|
|
|
it('при initial === undefined принимает сохранённое как есть', () => {
|
|
expect(coerceStored<string | undefined>(undefined, 'a')).toBe('a');
|
|
expect(coerceStored({ x: undefined as string | undefined }, { x: 'a' })).toEqual({ x: 'a' });
|
|
});
|
|
});
|