From acc1d0626938fc40e87ed473873c0ee7a4115054 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Thu, 9 Apr 2026 08:45:22 +0200 Subject: [PATCH] feat: manual Save All button in TopBar with pending change tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes are now queued locally (yellow highlight) and only persisted to Supabase when the user clicks the Save button in the top-right corner. The button shows the count of pending changes and a spinner while saving. EditPanel closes immediately after queuing — no Supabase call per edit. Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 43 ++++++++++++++++++++++-------------- src/components/EditPanel.tsx | 32 ++++++--------------------- src/components/TopBar.tsx | 17 ++++++++++++-- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b134d48..9e0b4b4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,8 @@ export default function App() { const [isLoadingDefault, setIsLoadingDefault] = useState(true); const [defaultLoadError, setDefaultLoadError] = useState(null); const [rowStatuses, setRowStatuses] = useState>({}); + const [pendingRows, setPendingRows] = useState>({}); + const [isSavingAll, setIsSavingAll] = useState(false); useEffect(() => { const loadDefaultData = async () => { @@ -238,31 +240,37 @@ export default function App() { setAppState(prev => ({ ...prev, hasUnsavedChanges: false })); }; - const handleSaveRow = async (rowIndex: number, updatedRow: ExcelRow): Promise => { - // 1. Optimistically update UI + const handleSaveRow = (rowIndex: number, updatedRow: ExcelRow) => { + // Update local UI state only — no Supabase call here. + // Changes are queued in pendingRows and saved manually via handleSaveAll. setAppState(prev => { const newData = [...prev.data]; newData[rowIndex] = updatedRow; return { ...prev, data: newData, hasUnsavedChanges: true }; }); - const articleNo = String(updatedRow[COLUMNS.ARTICLE_NO]); + setPendingRows(prev => ({ ...prev, [articleNo]: updatedRow })); setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' })); + setEditingRowIndex(null); + }; - // 2. Persist to Supabase - const success = await saveRowToSupabase(articleNo, updatedRow); - - if (success) { - setRowStatuses(prev => ({ ...prev, [articleNo]: 'saved' })); - setAppState(prev => ({ ...prev, hasUnsavedChanges: false })); - setEditingRowIndex(null); // Close panel only after confirmed save - } else { - console.error(`Failed to save ${articleNo} to Supabase`); - setRowStatuses(prev => ({ ...prev, [articleNo]: 'error' })); - // Panel stays open so user can retry + const handleSaveAll = async () => { + const entries = Object.entries(pendingRows) as [string, ExcelRow][]; + if (entries.length === 0) return; + setIsSavingAll(true); + let allSuccess = true; + for (const [articleNo, rowData] of entries) { + const success = await saveRowToSupabase(articleNo, rowData); + if (success) { + setRowStatuses(prev => ({ ...prev, [articleNo]: 'saved' })); + setPendingRows(prev => { const n = { ...prev }; delete n[articleNo]; return n; }); + } else { + setRowStatuses(prev => ({ ...prev, [articleNo]: 'error' })); + allSuccess = false; + } } - - return success; + if (allSuccess) setAppState(prev => ({ ...prev, hasUnsavedChanges: false })); + setIsSavingAll(false); }; const captureState = (message: string) => { @@ -334,6 +342,9 @@ export default function App() { onUndo={handleUndo} undoMessage={undoHistory[0]?.message} undoSteps={undoHistory.length} + pendingCount={Object.keys(pendingRows).length} + onSaveAll={handleSaveAll} + isSavingAll={isSavingAll} />
diff --git a/src/components/EditPanel.tsx b/src/components/EditPanel.tsx index 18c20c1..6185856 100644 --- a/src/components/EditPanel.tsx +++ b/src/components/EditPanel.tsx @@ -8,7 +8,7 @@ import { ConfirmModal } from './ConfirmModal'; interface EditPanelProps { row: ExcelRow; rowIndex: number; - onSave: (rowIndex: number, updatedRow: ExcelRow) => Promise; + onSave: (rowIndex: number, updatedRow: ExcelRow) => void; onClose: () => void; onCaptureState: (message: string) => void; } @@ -33,8 +33,6 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed const [loadingField, setLoadingField] = useState(null); const [error, setError] = useState(null); - const [isSaving, setIsSaving] = useState(false); - const [saveError, setSaveError] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [pendingGeminiField, setPendingGeminiField] = useState(null); const [generatedFields, setGeneratedFields] = useState>(new Set()); @@ -136,11 +134,8 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed } }; - const handleSave = async () => { + const handleSave = () => { setIsConfirmOpen(false); - setSaveError(false); - setIsSaving(true); - const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData)); if (hasModifications) { onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`); @@ -160,13 +155,7 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed newRow[COLUMNS.MOQ] = formData.moq; newRow[COLUMNS.DETAILS_DE] = formData.detailsDe; newRow[COLUMNS.DETAILS_EN] = formData.detailsEn; - - const success = await onSave(rowIndex, newRow); - // If save failed, panel stays open — handleSaveRow in App.tsx won't call setEditingRowIndex(null) - if (!success) { - setSaveError(true); - } - setIsSaving(false); + onSave(rowIndex, newRow); }; const DimensionInput = ({ label, field, placeholder }: { label: string, field: keyof typeof formData, placeholder?: string }) => ( @@ -315,25 +304,18 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
- {saveError && ( - - Error saving — check your connection and retry. - - )}
void; undoMessage?: string; undoSteps: number; + pendingCount: number; + onSaveAll: () => Promise; + isSavingAll: boolean; } -export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps }: TopBarProps) { +export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps, pendingCount, onSaveAll, isSavingAll }: TopBarProps) { return (
@@ -56,6 +59,16 @@ export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail, )}
+ {pendingCount > 0 && ( + + )} {hasData && (