Files
glchat/web/src/stores/unsaved.ts
T

106 lines
4.1 KiB
TypeScript
Raw Normal View History

import { create } from 'zustand';
import { platform } from '@/lib/platform';
/**
* Единый реестр несохранённых изменений (docs/client-tauri.md §5).
*
* Формы с локальным черновиком регистрируют здесь запись, пока в них есть
* правки. Реестр нужен по двум причинам: платформа должна знать, перехватывать
* ли закрытие окна (один флаг на всё приложение), а диалог закрытия — уметь
* сохранить все формы по очереди, не зная о них ничего.
*/
/** Сохранение формы: та же операция, что и кнопка «Сохранить». */
export type SaveHandler = () => Promise<void>;
export interface UnsavedEntry {
id: string;
save: SaveHandler;
}
interface UnsavedState {
entries: UnsavedEntry[];
/** Обёртка попросила закрыть окно, а в реестре есть правки. */
closePrompt: boolean;
/**
* Регистрирует запись и возвращает функцию снятия. Повторная регистрация с
* тем же id заменяет обработчик: форма может пересобрать `save`.
*/
register: (id: string, save: SaveHandler) => () => void;
unregister: (id: string) => void;
/** Показать диалог закрытия (сообщение `closeRequested` от обёртки). */
requestClose: () => void;
/** Закрыть диалог без выхода: пользователь выбрал «Отмена». */
dismissClose: () => void;
/**
* Сохранить все формы по очереди. Первая же ошибка останавливает процесс и
* уходит наверх: диалог покажет её текст, а окно останется открытым.
*/
saveAll: () => Promise<void>;
/** Сброс для тестов и выхода из аккаунта. */
reset: () => void;
}
const defaults = {
entries: [] as UnsavedEntry[],
closePrompt: false,
};
export const useUnsavedStore = create<UnsavedState>((set, get) => ({
...defaults,
register: (id, save) => {
const entries = get().entries;
const index = entries.findIndex((entry) => entry.id === id);
if (index === -1) {
set({ entries: [...entries, { id, save }] });
} else {
const next = [...entries];
next[index] = { id, save };
set({ entries: next });
}
return () => get().unregister(id);
},
unregister: (id) => {
const entries = get().entries;
if (!entries.some((entry) => entry.id === id)) {
return;
}
set({ entries: entries.filter((entry) => entry.id !== id) });
},
requestClose: () => set({ closePrompt: true }),
dismissClose: () => set({ closePrompt: false }),
saveAll: async () => {
// Копия списка: `save` может изменить реестр (например, форма сбросит
// черновик и снимет регистрацию), а обход должен остаться устойчивым.
for (const entry of [...get().entries]) {
await entry.save();
}
},
reset: () => set({ ...defaults }),
}));
/** Есть ли в реестре хотя бы одна несохранённая форма. */
export function hasUnsavedChanges(state: UnsavedState): boolean {
return state.entries.length > 0;
}
/**
* Платформа узнаёт о переходе «есть правки / нет правок» ровно один раз:
* сравниваем агрегат, а не длину списка. Иначе сохранение одной из двух форм
* мигало бы `setDirty(false)` → `setDirty(true)`, и обёртка успела бы решить,
* что закрывать окно можно.
*/
useUnsavedStore.subscribe((state, previous) => {
const dirty = hasUnsavedChanges(state);
if (dirty !== hasUnsavedChanges(previous)) {
platform.setDirty(dirty);
}
});