Files
rfcp/RFCP-Iteration-1.1.1-UX-Safety-Undo-Redo.md
2026-01-30 23:00:52 +02:00

6.9 KiB

RFCP Frontend - Iteration 1.1.1: UX Safety & Undo/Redo

Date: January 30, 2025
Type: Frontend Enhancement
Estimated: 2-3 hours
Location: /opt/rfcp/frontend/


🎯 Goal

Add safety confirmations for destructive actions and implement full Undo/Redo system.


📋 Pre-reading

  1. Review current state management in src/store/ (Zustand)
  2. Check existing toast implementation for delete actions

📊 Current State

  • Toast exists for delete actions (basic)
  • No unsaved changes detection
  • No confirmation dialogs
  • No undo/redo system

Tasks

1. Unsaved Changes Detection

Add dirty state tracking to store:

// src/store/projectStore.ts (or similar)

interface ProjectState {
  // ... existing
  isDirty: boolean;
  lastSavedState: string | null; // JSON snapshot
  
  markDirty: () => void;
  markClean: () => void;
  checkDirty: () => boolean;
}

// On any change to sites/settings:
markDirty()

// On save:
markClean()
lastSavedState = JSON.stringify(currentState)

Browser beforeunload warning:

// src/App.tsx or dedicated hook

useEffect(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (store.isDirty) {
      e.preventDefault();
      e.returnValue = ''; // Required for Chrome
    }
  };
  
  window.addEventListener('beforeunload', handleBeforeUnload);
  return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, []);

2. Confirmation Dialogs

Create reusable ConfirmDialog component:

// src/components/ui/ConfirmDialog.tsx

interface ConfirmDialogProps {
  isOpen: boolean;
  title: string;
  message: string;
  confirmText?: string;      // default: "Confirm"
  cancelText?: string;       // default: "Cancel"
  variant?: 'danger' | 'warning' | 'info';
  onConfirm: () => void;
  onCancel: () => void;
}

// Styling:
// - danger: red confirm button (for delete)
// - warning: yellow/orange (for discard changes)
// - info: blue (for info confirmations)

Apply to actions:

Action Dialog
Load project (when dirty) "Є незбережені зміни. Завантажити інший проект?"
Delete project "Видалити проект '{name}'? Цю дію не можна скасувати."
Delete site "Видалити станцію '{name}'?"
Delete sector "Видалити сектор '{name}'?"
New project (when dirty) "Є незбережені зміни. Створити новий проект?"
Page refresh (handled by beforeunload) Browser native dialog

3. Undo/Redo System

Create history store:

// src/store/historyStore.ts

interface HistoryState {
  past: ProjectSnapshot[];
  future: ProjectSnapshot[];
  maxHistory: number; // default: 50
  
  // Actions
  push: (snapshot: ProjectSnapshot) => void;
  undo: () => ProjectSnapshot | null;
  redo: () => ProjectSnapshot | null;
  clear: () => void;
  
  // Selectors
  canUndo: boolean;
  canRedo: boolean;
}

type ProjectSnapshot = {
  sites: Site[];
  settings: CoverageSettings;
  timestamp: number;
  action: string; // "add site", "delete sector", "move site", etc.
};

Integration points — push snapshot BEFORE these actions:

  • Add site
  • Delete site
  • Update site (position, params)
  • Add sector
  • Delete sector
  • Update sector
  • Update coverage settings
  • Import project
  • Clear all sites

Keyboard shortcuts:

// src/hooks/useKeyboardShortcuts.ts

useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    // Undo: Ctrl+Z (Windows/Linux) or Cmd+Z (Mac)
    if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
      e.preventDefault();
      handleUndo();
    }
    
    // Redo: Ctrl+Shift+Z or Ctrl+Y
    if ((e.ctrlKey || e.metaKey) && (
      (e.key === 'z' && e.shiftKey) || 
      e.key === 'y'
    )) {
      e.preventDefault();
      handleRedo();
    }
  };
  
  window.addEventListener('keydown', handleKeyDown);
  return () => window.removeEventListener('keydown', handleKeyDown);
}, []);

UI indicators:

// Toolbar buttons (disabled when can't undo/redo)
<button 
  onClick={handleUndo} 
  disabled={!canUndo}
  title="Undo (Ctrl+Z)"
>
  <UndoIcon />
</button>

<button 
  onClick={handleRedo} 
  disabled={!canRedo}
  title="Redo (Ctrl+Shift+Z)"
>
  <RedoIcon />
</button>

4. Enhanced Toast Notifications

Extend existing toast for all actions:

// Success toasts
"Проект збережено"
"Станцію додано"
"Станцію видалено"
"Зміни скасовано" (undo)
"Зміни повернено" (redo)

// Error toasts
"Помилка збереження"
"Помилка завантаження"

// Info toasts
"Проект завантажено"

Toast with undo action (optional enhancement):

// When deleting, show toast with undo button
toast({
  message: "Станцію видалено",
  action: {
    label: "Скасувати",
    onClick: () => handleUndo()
  },
  duration: 5000
});

📁 Files to Create/Modify

src/
├── components/ui/
│   └── ConfirmDialog.tsx      # NEW
├── store/
│   ├── historyStore.ts        # NEW
│   └── projectStore.ts        # MODIFY (add isDirty)
├── hooks/
│   ├── useUnsavedChanges.ts   # NEW
│   └── useKeyboardShortcuts.ts # NEW or MODIFY
└── App.tsx                     # MODIFY (add beforeunload)

Success Criteria

  • Refresh page with unsaved changes → browser warning
  • Load project with unsaved changes → confirmation dialog
  • Delete project → confirmation dialog (danger style)
  • Delete site/sector → confirmation dialog
  • Ctrl+Z undoes last action
  • Ctrl+Shift+Z redoes
  • Undo/Redo buttons in toolbar (with disabled states)
  • Toast shows on save/delete/undo/redo
  • History limited to 50 states (memory management)

🧪 Test Scenarios

  1. Unsaved changes:

    • Add site → refresh → browser warning appears
    • Add site → save → refresh → no warning
  2. Confirmations:

    • Add site → click Load → dialog appears → Cancel → site still there
    • Add site → click Load → dialog appears → Confirm → new project loads
  3. Undo/Redo:

    • Add 3 sites → Ctrl+Z → 2 sites remain
    • Ctrl+Z again → 1 site
    • Ctrl+Shift+Z → 2 sites back
    • Add new site after undo → redo history cleared
  4. Edge cases:

    • Undo when nothing to undo → button disabled, no action
    • 51 actions → oldest dropped from history

📝 Notes

  • Keep snapshots lightweight (only sites + settings, not UI state)
  • Debounce position changes (don't snapshot every pixel of drag)
  • Consider grouping rapid changes (e.g., typing in input)
  • Ukrainian UI text for all dialogs/toasts

Ready for Claude Code 🚀