Browse Source

Added a persistent loading toast with spinner for backup exports that include print archives:

  Changes to ToastContext.tsx:
  - Added 'loading' toast type with a spinning Loader2 icon
  - Added showPersistentToast(id, message, type) function for toasts that don't auto-dismiss
  - Exposed dismissToast(id) function to allow programmatic dismissal
  - Added green-themed styling for loading toasts

  Changes to SettingsPage.tsx:
  - When exporting a backup that includes archives, shows a persistent "Preparing backup..." toast with spinner
  - The toast is automatically dismissed when the download starts or if an error occurs
  - For non-archive backups (which are fast), no loading toast is shown

  The user will now see clear feedback when creating a backup with print archives - a toast with a spinner appears immediately after clicking export and stays visible
  until the download dialog appears.
maziggy 5 months ago
parent
commit
b6b83a6360
2 changed files with 40 additions and 6 deletions
  1. 19 3
      frontend/src/contexts/ToastContext.tsx
  2. 21 3
      frontend/src/pages/SettingsPage.tsx

+ 19 - 3
frontend/src/contexts/ToastContext.tsx

@@ -1,16 +1,19 @@
 import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
-import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react';
+import { CheckCircle, XCircle, AlertCircle, Info, X, Loader2 } from 'lucide-react';
 
-type ToastType = 'success' | 'error' | 'warning' | 'info';
+type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading';
 
 interface Toast {
   id: string;
   message: string;
   type: ToastType;
+  persistent?: boolean;
 }
 
 interface ToastContextType {
   showToast: (message: string, type?: ToastType) => void;
+  showPersistentToast: (id: string, message: string, type?: ToastType) => void;
+  dismissToast: (id: string) => void;
 }
 
 const ToastContext = createContext<ToastContextType | undefined>(undefined);
@@ -28,6 +31,7 @@ const icons = {
   error: <XCircle className="w-5 h-5 text-red-400" />,
   warning: <AlertCircle className="w-5 h-5 text-yellow-400" />,
   info: <Info className="w-5 h-5 text-blue-400" />,
+  loading: <Loader2 className="w-5 h-5 text-bambu-green animate-spin" />,
 };
 
 const bgColors = {
@@ -35,6 +39,7 @@ const bgColors = {
   error: 'bg-red-500/10 border-red-500/30',
   warning: 'bg-yellow-500/10 border-yellow-500/30',
   info: 'bg-blue-500/10 border-blue-500/30',
+  loading: 'bg-bambu-green/10 border-bambu-green/30',
 };
 
 export function ToastProvider({ children }: { children: ReactNode }) {
@@ -50,12 +55,23 @@ export function ToastProvider({ children }: { children: ReactNode }) {
     }, 3000);
   }, []);
 
+  const showPersistentToast = useCallback((id: string, message: string, type: ToastType = 'info') => {
+    setToasts((prev) => {
+      // Update existing toast if same id, otherwise add new one
+      const exists = prev.find((t) => t.id === id);
+      if (exists) {
+        return prev.map((t) => (t.id === id ? { ...t, message, type, persistent: true } : t));
+      }
+      return [...prev, { id, message, type, persistent: true }];
+    });
+  }, []);
+
   const dismissToast = useCallback((id: string) => {
     setToasts((prev) => prev.filter((t) => t.id !== id));
   }, []);
 
   return (
-    <ToastContext.Provider value={{ showToast }}>
+    <ToastContext.Provider value={{ showToast, showPersistentToast, dismissToast }}>
       {children}
 
       {/* Toast Container */}

+ 21 - 3
frontend/src/pages/SettingsPage.tsx

@@ -24,7 +24,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
 export function SettingsPage() {
   const queryClient = useQueryClient();
   const { t, i18n } = useTranslation();
-  const { showToast } = useToast();
+  const { showToast, showPersistentToast, dismissToast } = useToast();
   const [localSettings, setLocalSettings] = useState<AppSettings | null>(null);
   const [showPlugModal, setShowPlugModal] = useState(false);
   const [editingPlug, setEditingPlug] = useState<SmartPlug | null>(null);
@@ -1836,17 +1836,35 @@ export function SettingsPage() {
           onClose={() => setShowBackupModal(false)}
           onExport={async (categories) => {
             setShowBackupModal(false);
+            const toastId = 'backup-progress';
+            const includesArchives = categories.archives;
+
+            // Show persistent loading toast for archive backups (can be large)
+            if (includesArchives) {
+              showPersistentToast(toastId, t('backup.preparing', { defaultValue: 'Preparing backup...' }), 'loading');
+            }
+
             try {
               const { blob, filename } = await api.exportBackup(categories);
+
+              // Dismiss loading toast before download starts
+              if (includesArchives) {
+                dismissToast(toastId);
+              }
+
               const url = URL.createObjectURL(blob);
               const a = document.createElement('a');
               a.href = url;
               a.download = filename;
               a.click();
               URL.revokeObjectURL(url);
-              showToast('Backup downloaded', 'success');
+              showToast(t('backup.downloaded', { defaultValue: 'Backup downloaded' }), 'success');
             } catch (err) {
-              showToast('Failed to create backup', 'error');
+              // Dismiss loading toast on error
+              if (includesArchives) {
+                dismissToast(toastId);
+              }
+              showToast(t('backup.failed', { defaultValue: 'Failed to create backup' }), 'error');
             }
           }}
         />