From 941f61830fc9f395fe626b87c72a70801fab4973 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Wed, 28 May 2025 13:17:48 +0530 Subject: [PATCH] feat: implement comprehensive auto-save for all config forms and remove manual save button - Add auto-save functionality to all GitHub config form fields (text inputs and checkboxes) - Add auto-save functionality to all Gitea config form fields (text inputs and select dropdown) - Extend existing auto-save pattern to cover text inputs with 500ms debounce - Remove Save Configuration button and related manual save logic - Update Import GitHub Data button to depend on form validation instead of saved state - Remove isConfigSaved dependency from all auto-save functions for immediate activation - Add proper cleanup for all auto-save timeouts on component unmount - Maintain silent auto-save operation without intrusive notifications All configuration changes now auto-save seamlessly, providing a better UX while maintaining data consistency and error handling. --- src/components/config/ConfigTabs.tsx | 174 ++++++++++++++------- src/components/config/GitHubConfigForm.tsx | 15 +- src/components/config/GiteaConfigForm.tsx | 14 +- 3 files changed, 141 insertions(+), 62 deletions(-) diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx index a29d92f..7c2051b 100644 --- a/src/components/config/ConfigTabs.tsx +++ b/src/components/config/ConfigTabs.tsx @@ -56,14 +56,18 @@ export function ConfigTabs() { retentionDays: 604800, // 7 days in seconds }, }); - const { user, refreshUser } = useAuth(); + const { user } = useAuth(); const [isLoading, setIsLoading] = useState(true); const [isSyncing, setIsSyncing] = useState(false); - const [isConfigSaved, setIsConfigSaved] = useState(false); + const [isAutoSavingSchedule, setIsAutoSavingSchedule] = useState(false); const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState(false); + const [isAutoSavingGitHub, setIsAutoSavingGitHub] = useState(false); + const [isAutoSavingGitea, setIsAutoSavingGitea] = useState(false); const autoSaveScheduleTimeoutRef = useRef(null); const autoSaveCleanupTimeoutRef = useRef(null); + const autoSaveGitHubTimeoutRef = useRef(null); + const autoSaveGiteaTimeoutRef = useRef(null); const isConfigFormValid = (): boolean => { const { githubConfig, giteaConfig } = config; @@ -109,44 +113,9 @@ export function ConfigTabs() { } }; - const handleSaveConfig = async () => { - if (!user?.id) return; - const reqPayload: SaveConfigApiRequest = { - userId: user.id, - githubConfig: config.githubConfig, - giteaConfig: config.giteaConfig, - scheduleConfig: config.scheduleConfig, - cleanupConfig: config.cleanupConfig, - }; - try { - const response = await fetch('/api/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(reqPayload), - }); - const result: SaveConfigApiResponse = await response.json(); - if (result.success) { - await refreshUser(); - setIsConfigSaved(true); - // Invalidate config cache so other components get fresh data - invalidateConfigCache(); - toast.success( - 'Configuration saved successfully! Now import your GitHub data to begin.', - ); - } else { - showErrorToast( - `Failed to save configuration: ${result.message || 'Unknown error'}`, - toast - ); - } - } catch (error) { - showErrorToast(error, toast); - } - }; - // Auto-save function specifically for schedule config changes const autoSaveScheduleConfig = useCallback(async (scheduleConfig: ScheduleConfig) => { - if (!user?.id || !isConfigSaved) return; // Only auto-save if config was previously saved + if (!user?.id) return; // Clear any existing timeout if (autoSaveScheduleTimeoutRef.current) { @@ -206,11 +175,11 @@ export function ConfigTabs() { setIsAutoSavingSchedule(false); } }, 500); // 500ms debounce - }, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig, config.cleanupConfig]); + }, [user?.id, config.githubConfig, config.giteaConfig, config.cleanupConfig]); // Auto-save function specifically for cleanup config changes const autoSaveCleanupConfig = useCallback(async (cleanupConfig: DatabaseCleanupConfig) => { - if (!user?.id || !isConfigSaved) return; // Only auto-save if config was previously saved + if (!user?.id) return; // Clear any existing timeout if (autoSaveCleanupTimeoutRef.current) { @@ -269,7 +238,101 @@ export function ConfigTabs() { setIsAutoSavingCleanup(false); } }, 500); // 500ms debounce - }, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig, config.scheduleConfig]); + }, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig]); + + // Auto-save function specifically for GitHub config changes + const autoSaveGitHubConfig = useCallback(async (githubConfig: GitHubConfig) => { + if (!user?.id) return; + + // Clear any existing timeout + if (autoSaveGitHubTimeoutRef.current) { + clearTimeout(autoSaveGitHubTimeoutRef.current); + } + + // Debounce the auto-save to prevent excessive API calls + autoSaveGitHubTimeoutRef.current = setTimeout(async () => { + setIsAutoSavingGitHub(true); + + const reqPayload: SaveConfigApiRequest = { + userId: user.id!, + githubConfig: githubConfig, + giteaConfig: config.giteaConfig, + scheduleConfig: config.scheduleConfig, + cleanupConfig: config.cleanupConfig, + }; + + try { + const response = await fetch('/api/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(reqPayload), + }); + const result: SaveConfigApiResponse = await response.json(); + + if (result.success) { + // Silent success - no toast for auto-save + // Invalidate config cache so other components get fresh data + invalidateConfigCache(); + } else { + showErrorToast( + `Auto-save failed: ${result.message || 'Unknown error'}`, + toast + ); + } + } catch (error) { + showErrorToast(error, toast); + } finally { + setIsAutoSavingGitHub(false); + } + }, 500); // 500ms debounce + }, [user?.id, config.giteaConfig, config.scheduleConfig, config.cleanupConfig]); + + // Auto-save function specifically for Gitea config changes + const autoSaveGiteaConfig = useCallback(async (giteaConfig: GiteaConfig) => { + if (!user?.id) return; + + // Clear any existing timeout + if (autoSaveGiteaTimeoutRef.current) { + clearTimeout(autoSaveGiteaTimeoutRef.current); + } + + // Debounce the auto-save to prevent excessive API calls + autoSaveGiteaTimeoutRef.current = setTimeout(async () => { + setIsAutoSavingGitea(true); + + const reqPayload: SaveConfigApiRequest = { + userId: user.id!, + githubConfig: config.githubConfig, + giteaConfig: giteaConfig, + scheduleConfig: config.scheduleConfig, + cleanupConfig: config.cleanupConfig, + }; + + try { + const response = await fetch('/api/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(reqPayload), + }); + const result: SaveConfigApiResponse = await response.json(); + + if (result.success) { + // Silent success - no toast for auto-save + // Invalidate config cache so other components get fresh data + invalidateConfigCache(); + } else { + showErrorToast( + `Auto-save failed: ${result.message || 'Unknown error'}`, + toast + ); + } + } catch (error) { + showErrorToast(error, toast); + } finally { + setIsAutoSavingGitea(false); + } + }, 500); // 500ms debounce + }, [user?.id, config.githubConfig, config.scheduleConfig, config.cleanupConfig]); // Cleanup timeouts on unmount useEffect(() => { @@ -280,6 +343,12 @@ export function ConfigTabs() { if (autoSaveCleanupTimeoutRef.current) { clearTimeout(autoSaveCleanupTimeoutRef.current); } + if (autoSaveGitHubTimeoutRef.current) { + clearTimeout(autoSaveGitHubTimeoutRef.current); + } + if (autoSaveGiteaTimeoutRef.current) { + clearTimeout(autoSaveGiteaTimeoutRef.current); + } }; }, []); @@ -304,7 +373,7 @@ export function ConfigTabs() { cleanupConfig: response.cleanupConfig || config.cleanupConfig, }); - if (response.id) setIsConfigSaved(true); + } } catch (error) { console.warn( @@ -401,10 +470,10 @@ export function ConfigTabs() {
-
@@ -450,6 +508,8 @@ export function ConfigTabs() { : update, })) } + onAutoSave={autoSaveGitHubConfig} + isAutoSaving={isAutoSavingGitHub} />
diff --git a/src/components/config/GitHubConfigForm.tsx b/src/components/config/GitHubConfigForm.tsx index e4b1801..3ef3474 100644 --- a/src/components/config/GitHubConfigForm.tsx +++ b/src/components/config/GitHubConfigForm.tsx @@ -20,9 +20,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; interface GitHubConfigFormProps { config: GitHubConfig; setConfig: React.Dispatch>; + onAutoSave?: (githubConfig: GitHubConfig) => Promise; + isAutoSaving?: boolean; } -export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) { +export function GitHubConfigForm({ config, setConfig, onAutoSave, isAutoSaving }: GitHubConfigFormProps) { const [isLoading, setIsLoading] = useState(false); const handleChange = (e: React.ChangeEvent) => { @@ -43,10 +45,17 @@ export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) { ); } - setConfig({ + const newConfig = { ...config, [name]: type === "checkbox" ? checked : value, - }); + }; + + setConfig(newConfig); + + // Auto-save for all field changes + if (onAutoSave) { + onAutoSave(newConfig); + } }; const testConnection = async () => { diff --git a/src/components/config/GiteaConfigForm.tsx b/src/components/config/GiteaConfigForm.tsx index beeb8b3..f86bead 100644 --- a/src/components/config/GiteaConfigForm.tsx +++ b/src/components/config/GiteaConfigForm.tsx @@ -21,19 +21,27 @@ import { toast } from "sonner"; interface GiteaConfigFormProps { config: GiteaConfig; setConfig: React.Dispatch>; + onAutoSave?: (giteaConfig: GiteaConfig) => Promise; + isAutoSaving?: boolean; } -export function GiteaConfigForm({ config, setConfig }: GiteaConfigFormProps) { +export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }: GiteaConfigFormProps) { const [isLoading, setIsLoading] = useState(false); const handleChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; - setConfig({ + const newConfig = { ...config, [name]: value, - }); + }; + setConfig(newConfig); + + // Auto-save for all field changes + if (onAutoSave) { + onAutoSave(newConfig); + } }; const testConnection = async () => {