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.
This commit is contained in:
maziggy
2025-12-12 16:12:30 +01:00
parent a14dcbc034
commit b6b83a6360
2 changed files with 40 additions and 6 deletions
+19 -3
View File
@@ -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
View File
@@ -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');
}
}}
/>