@mytec: iter1.1.1 ready for testing
This commit is contained in:
@@ -9,6 +9,8 @@ interface ShortcutHandlers {
|
||||
onCloseForm: () => void;
|
||||
onShowShortcuts?: () => void;
|
||||
onDeleteRequest?: (siteId: string, siteName: string) => void;
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
}
|
||||
|
||||
function isInputActive(): boolean {
|
||||
@@ -23,6 +25,8 @@ export function useKeyboardShortcuts({
|
||||
onCloseForm,
|
||||
onShowShortcuts,
|
||||
onDeleteRequest,
|
||||
onUndo,
|
||||
onRedo,
|
||||
}: ShortcutHandlers) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -36,6 +40,20 @@ export function useKeyboardShortcuts({
|
||||
return;
|
||||
}
|
||||
|
||||
// Undo: Ctrl+Z (or Cmd+Z on Mac)
|
||||
if (modKey && e.key === 'z' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onUndo?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
|
||||
if (modKey && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) {
|
||||
e.preventDefault();
|
||||
onRedo?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape always works
|
||||
if (e.key === 'Escape') {
|
||||
useSitesStore.getState().selectSite(null);
|
||||
@@ -118,5 +136,5 @@ export function useKeyboardShortcuts({
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest]);
|
||||
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest, onUndo, onRedo]);
|
||||
}
|
||||
|
||||
76
frontend/src/hooks/useUnsavedChanges.ts
Normal file
76
frontend/src/hooks/useUnsavedChanges.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useSitesStore } from '@/store/sites.ts';
|
||||
import { useCoverageStore } from '@/store/coverage.ts';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface DirtyState {
|
||||
isDirty: boolean;
|
||||
lastSavedSnapshot: string | null;
|
||||
markDirty: () => void;
|
||||
markClean: () => void;
|
||||
setSnapshot: (snapshot: string) => void;
|
||||
}
|
||||
|
||||
export const useDirtyStore = create<DirtyState>((set) => ({
|
||||
isDirty: false,
|
||||
lastSavedSnapshot: null,
|
||||
markDirty: () => set({ isDirty: true }),
|
||||
markClean: () => set({ isDirty: false }),
|
||||
setSnapshot: (snapshot: string) => set({ lastSavedSnapshot: snapshot, isDirty: false }),
|
||||
}));
|
||||
|
||||
function getCurrentSnapshot(): string {
|
||||
const sites = useSitesStore.getState().sites;
|
||||
const settings = useCoverageStore.getState().settings;
|
||||
return JSON.stringify({ sites: sites.map((s) => s.id), settings });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that tracks unsaved changes and warns on page unload.
|
||||
* Call markClean() after a successful save.
|
||||
*/
|
||||
export function useUnsavedChanges() {
|
||||
const isDirty = useDirtyStore((s) => s.isDirty);
|
||||
const prevSnapshot = useRef<string | null>(null);
|
||||
|
||||
// Take initial snapshot
|
||||
useEffect(() => {
|
||||
prevSnapshot.current = getCurrentSnapshot();
|
||||
}, []);
|
||||
|
||||
// Watch sites array for changes and mark dirty
|
||||
useEffect(() => {
|
||||
const unsub = useSitesStore.subscribe((state) => {
|
||||
const snap = JSON.stringify(state.sites.map((s) => s.id));
|
||||
if (prevSnapshot.current !== null && snap !== prevSnapshot.current) {
|
||||
useDirtyStore.getState().markDirty();
|
||||
}
|
||||
prevSnapshot.current = snap;
|
||||
});
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
// beforeunload warning
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (useDirtyStore.getState().isDirty) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
}, []);
|
||||
|
||||
const markClean = useCallback(() => {
|
||||
prevSnapshot.current = getCurrentSnapshot();
|
||||
useDirtyStore.getState().markClean();
|
||||
}, []);
|
||||
|
||||
const markDirty = useCallback(() => {
|
||||
useDirtyStore.getState().markDirty();
|
||||
}, []);
|
||||
|
||||
return { isDirty, markClean, markDirty };
|
||||
}
|
||||
Reference in New Issue
Block a user