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.
This commit is contained in:
Arunavo Ray
2025-05-28 13:17:48 +05:30
parent 5b60cffaae
commit 941f61830f
3 changed files with 141 additions and 62 deletions

View File

@@ -56,14 +56,18 @@ export function ConfigTabs() {
retentionDays: 604800, // 7 days in seconds retentionDays: 604800, // 7 days in seconds
}, },
}); });
const { user, refreshUser } = useAuth(); const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSyncing, setIsSyncing] = useState<boolean>(false); const [isSyncing, setIsSyncing] = useState<boolean>(false);
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
const [isAutoSavingSchedule, setIsAutoSavingSchedule] = useState<boolean>(false); const [isAutoSavingSchedule, setIsAutoSavingSchedule] = useState<boolean>(false);
const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState<boolean>(false); const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState<boolean>(false);
const [isAutoSavingGitHub, setIsAutoSavingGitHub] = useState<boolean>(false);
const [isAutoSavingGitea, setIsAutoSavingGitea] = useState<boolean>(false);
const autoSaveScheduleTimeoutRef = useRef<NodeJS.Timeout | null>(null); const autoSaveScheduleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null); const autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveGitHubTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveGiteaTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isConfigFormValid = (): boolean => { const isConfigFormValid = (): boolean => {
const { githubConfig, giteaConfig } = config; 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 // Auto-save function specifically for schedule config changes
const autoSaveScheduleConfig = useCallback(async (scheduleConfig: ScheduleConfig) => { 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 // Clear any existing timeout
if (autoSaveScheduleTimeoutRef.current) { if (autoSaveScheduleTimeoutRef.current) {
@@ -206,11 +175,11 @@ export function ConfigTabs() {
setIsAutoSavingSchedule(false); setIsAutoSavingSchedule(false);
} }
}, 500); // 500ms debounce }, 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 // Auto-save function specifically for cleanup config changes
const autoSaveCleanupConfig = useCallback(async (cleanupConfig: DatabaseCleanupConfig) => { 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 // Clear any existing timeout
if (autoSaveCleanupTimeoutRef.current) { if (autoSaveCleanupTimeoutRef.current) {
@@ -269,7 +238,101 @@ export function ConfigTabs() {
setIsAutoSavingCleanup(false); setIsAutoSavingCleanup(false);
} }
}, 500); // 500ms debounce }, 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 // Cleanup timeouts on unmount
useEffect(() => { useEffect(() => {
@@ -280,6 +343,12 @@ export function ConfigTabs() {
if (autoSaveCleanupTimeoutRef.current) { if (autoSaveCleanupTimeoutRef.current) {
clearTimeout(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: cleanupConfig:
response.cleanupConfig || config.cleanupConfig, response.cleanupConfig || config.cleanupConfig,
}); });
if (response.id) setIsConfigSaved(true);
} }
} catch (error) { } catch (error) {
console.warn( console.warn(
@@ -401,10 +470,10 @@ export function ConfigTabs() {
<div className="flex gap-x-4"> <div className="flex gap-x-4">
<Button <Button
onClick={handleImportGitHubData} onClick={handleImportGitHubData}
disabled={isSyncing || !isConfigSaved} disabled={isSyncing || !isConfigFormValid()}
title={ title={
!isConfigSaved !isConfigFormValid()
? 'Save configuration first' ? 'Please fill all required GitHub and Gitea fields'
: isSyncing : isSyncing
? 'Import in progress' ? 'Import in progress'
: 'Import GitHub Data' : 'Import GitHub Data'
@@ -422,17 +491,6 @@ export function ConfigTabs() {
</> </>
)} )}
</Button> </Button>
<Button
onClick={handleSaveConfig}
disabled={!isConfigFormValid()}
title={
!isConfigFormValid()
? 'Please fill all required fields'
: 'Save Configuration'
}
>
Save Configuration
</Button>
</div> </div>
</div> </div>
@@ -450,6 +508,8 @@ export function ConfigTabs() {
: update, : update,
})) }))
} }
onAutoSave={autoSaveGitHubConfig}
isAutoSaving={isAutoSavingGitHub}
/> />
<GiteaConfigForm <GiteaConfigForm
config={config.giteaConfig} config={config.giteaConfig}
@@ -462,6 +522,8 @@ export function ConfigTabs() {
: update, : update,
})) }))
} }
onAutoSave={autoSaveGiteaConfig}
isAutoSaving={isAutoSavingGitea}
/> />
</div> </div>
<div className="flex gap-x-4"> <div className="flex gap-x-4">

View File

@@ -20,9 +20,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
interface GitHubConfigFormProps { interface GitHubConfigFormProps {
config: GitHubConfig; config: GitHubConfig;
setConfig: React.Dispatch<React.SetStateAction<GitHubConfig>>; setConfig: React.Dispatch<React.SetStateAction<GitHubConfig>>;
onAutoSave?: (githubConfig: GitHubConfig) => Promise<void>;
isAutoSaving?: boolean;
} }
export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) { export function GitHubConfigForm({ config, setConfig, onAutoSave, isAutoSaving }: GitHubConfigFormProps) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -43,10 +45,17 @@ export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) {
); );
} }
setConfig({ const newConfig = {
...config, ...config,
[name]: type === "checkbox" ? checked : value, [name]: type === "checkbox" ? checked : value,
}); };
setConfig(newConfig);
// Auto-save for all field changes
if (onAutoSave) {
onAutoSave(newConfig);
}
}; };
const testConnection = async () => { const testConnection = async () => {

View File

@@ -21,19 +21,27 @@ import { toast } from "sonner";
interface GiteaConfigFormProps { interface GiteaConfigFormProps {
config: GiteaConfig; config: GiteaConfig;
setConfig: React.Dispatch<React.SetStateAction<GiteaConfig>>; setConfig: React.Dispatch<React.SetStateAction<GiteaConfig>>;
onAutoSave?: (giteaConfig: GiteaConfig) => Promise<void>;
isAutoSaving?: boolean;
} }
export function GiteaConfigForm({ config, setConfig }: GiteaConfigFormProps) { export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }: GiteaConfigFormProps) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleChange = ( const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => { ) => {
const { name, value } = e.target; const { name, value } = e.target;
setConfig({ const newConfig = {
...config, ...config,
[name]: value, [name]: value,
}); };
setConfig(newConfig);
// Auto-save for all field changes
if (onAutoSave) {
onAutoSave(newConfig);
}
}; };
const testConnection = async () => { const testConnection = async () => {