Files
ersatztv/web/src/mediaSources/useDirtyGuard.ts
T
timothyandOpenAI Codex 3481f98bcc
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m44s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
fix(spa): clear dirty guard before saved navigation
Fixes #344

Co-Authored-By: OpenAI Codex <codex@openai.com>
2026-07-13 23:47:43 +02:00

45 lines
2.0 KiB
TypeScript

// Shared dirty-guard wiring for the media-source editor screens (design §D.2). Mirrors the
// SchedulesScreen convention (registerNavigationGuard on the current dirty state + a `beforeunload`
// listener while dirty) so an editor with unsaved changes vetoes both in-app navigation (App's
// popstate owner consults canLeaveCurrentScreen) and full-page unloads (reload / close tab).
//
// The guard predicate reads the LATEST dirty value via a ref updated on every render, so App's
// synchronous popstate handler sees the current state without the effect having to re-register on
// each keystroke. A single hook keeps the three editors (connection / libraries / path-replacements)
// consistent instead of copy-pasting the guard block into each.
import { useCallback, useEffect, useRef } from 'react';
import { registerNavigationGuard } from '../navigationGuard';
export function useDirtyGuard(dirty: boolean, prompt: string): () => void {
const dirtyRef = useRef(dirty);
// A successful save may navigate in the same promise callback that queues the clean baseline.
// React has not rendered that baseline yet, so let the caller transfer the guard synchronously
// before dispatching the synthetic popstate. The normal render still owns the durable state.
const markClean = useCallback(() => {
dirtyRef.current = false;
}, []);
// Keep the ref current for the synchronous guard predicate. Updated in an effect (committed before
// any user pop/nav gesture can fire) rather than during render.
useEffect(() => {
dirtyRef.current = dirty;
}, [dirty]);
useEffect(() => registerNavigationGuard(() => !dirtyRef.current || window.confirm(prompt)), [prompt]);
useEffect(() => {
if (!dirty) {
return undefined;
}
const handler = (event: BeforeUnloadEvent) => {
event.preventDefault();
event.returnValue = '';
};
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [dirty]);
return markClean;
}