From e498325975fd1844b00f4b09f49a8e004467d0f5 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Sun, 29 Mar 2026 17:35:07 +0200 Subject: [PATCH] Implement Undo system for AI and bulk actions --- .gitignore | 2 + src/App.tsx | 27 +++++++++++++ src/components/DimensionsView.tsx | 4 +- src/components/EditPanel.tsx | 7 +++- src/components/UndoToast.tsx | 67 +++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/components/UndoToast.tsx diff --git a/.gitignore b/.gitignore index 5a86d2a..bf12ddb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ coverage/ *.log .env* !.env.example +.vercel +.env*.local diff --git a/src/App.tsx b/src/App.tsx index c4d9745..ef7c4ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { getAllSyncedRows, saveRowToSupabase } from './lib/supabase'; import { getStoredSession, signOut, type AuthSession } from './lib/auth'; import { LoginPage } from './components/LoginPage'; import { DimensionsView } from './components/DimensionsView'; +import { UndoToast } from './components/UndoToast'; export default function App() { const [session, setSession] = useState(() => getStoredSession()); @@ -31,6 +32,7 @@ export default function App() { hasUnsavedChanges: false }); const [activeModule, setActiveModule] = useState<'descriptions' | 'matrix' | 'dimensions'>('descriptions'); + const [undoState, setUndoState] = useState<{ data: ExcelRow[], message: string } | null>(null); const [editingRowIndex, setEditingRowIndex] = useState(null); const [isLoadingDefault, setIsLoadingDefault] = useState(true); const [defaultLoadError, setDefaultLoadError] = useState(null); @@ -245,6 +247,23 @@ export default function App() { } }; + const captureState = (message: string) => { + setUndoState({ + data: JSON.parse(JSON.stringify(appState.data)), // Deep copy + message + }); + }; + + const handleUndo = () => { + if (!undoState) return; + setAppState(prev => ({ + ...prev, + data: undoState.data, + hasUnsavedChanges: true + })); + setUndoState(null); + }; + const stats = useMemo(() => { if (appState.data.length === 0) return null; let missingDeLong = 0; @@ -329,6 +348,7 @@ export default function App() { headers={appState.headers} onEdit={(index) => setEditingRowIndex(index)} onSaveRow={handleSaveRow} + onCaptureState={captureState} /> )} @@ -336,12 +356,19 @@ export default function App() { + setUndoState(null)} + /> + {editingRowIndex !== null && ( setEditingRowIndex(null)} + onCaptureState={captureState} /> )} diff --git a/src/components/DimensionsView.tsx b/src/components/DimensionsView.tsx index 5c77b84..39d7b74 100644 --- a/src/components/DimensionsView.tsx +++ b/src/components/DimensionsView.tsx @@ -8,6 +8,7 @@ interface DimensionsViewProps { headers: string[]; onEdit: (index: number) => void; onSaveRow: (index: number, updatedRow: ExcelRow) => void; + onCaptureState: (message: string) => void; } interface DimensionGroup { @@ -22,7 +23,7 @@ interface DimensionGroup { }; } -export function DimensionsView({ data, headers, onEdit, onSaveRow }: DimensionsViewProps) { +export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureState }: DimensionsViewProps) { const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true); const [syncing, setSyncing] = useState(null); @@ -100,6 +101,7 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow }: DimensionsV return; } + onCaptureState(`Synced ${group.rows.length} products in ${group.innerDims} group`); setSyncing(group.key); try { const first = group.rows[0].row; diff --git a/src/components/EditPanel.tsx b/src/components/EditPanel.tsx index b76c401..290a42e 100644 --- a/src/components/EditPanel.tsx +++ b/src/components/EditPanel.tsx @@ -9,9 +9,10 @@ interface EditPanelProps { rowIndex: number; onSave: (rowIndex: number, updatedRow: ExcelRow) => void; onClose: () => void; + onCaptureState: (message: string) => void; } -export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) { +export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: EditPanelProps) { const [formData, setFormData] = useState({ longDe: row[COLUMNS.LONG_DE] || '', longEn: row[COLUMNS.LONG_EN] || '', @@ -100,6 +101,10 @@ export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) { }; const handleSave = () => { + const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData)); + if (hasModifications) { + onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`); + } const newRow = [...row]; newRow[COLUMNS.LONG_DE] = formData.longDe; newRow[COLUMNS.LONG_EN] = formData.longEn; diff --git a/src/components/UndoToast.tsx b/src/components/UndoToast.tsx new file mode 100644 index 0000000..3fca51b --- /dev/null +++ b/src/components/UndoToast.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react'; +import { Undo2, X, AlertCircle } from 'lucide-react'; +import { cn } from '../lib/utils'; +import { ExcelRow } from '../types'; + +interface UndoToastProps { + undoState: { data: ExcelRow[], message: string } | null; + onUndo: () => void; + onClose: () => void; +} + +export function UndoToast({ undoState, onUndo, onClose }: UndoToastProps) { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (undoState) { + setIsVisible(true); + const timer = setTimeout(() => { + setIsVisible(false); + setTimeout(onClose, 300); // Wait for transition + }, 8000); + return () => clearTimeout(timer); + } else { + setIsVisible(false); + } + }, [undoState, onClose]); + + if (!undoState && !isVisible) return null; + + return ( +
+
+
+ +
+ +
+
Action Completed
+
{undoState?.message}
+
+ +
+ + + +
+
+
+ ); +}