feat: implement auto-save functionality for schedule config changes and enhance UI with loading indicator

This commit is contained in:
Arunavo Ray
2025-05-24 11:31:40 +05:30
parent a1da82a718
commit 0b568a3b37
2 changed files with 83 additions and 11 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback, useRef } from 'react';
import { GitHubConfigForm } from './GitHubConfigForm'; import { GitHubConfigForm } from './GitHubConfigForm';
import { GiteaConfigForm } from './GiteaConfigForm'; import { GiteaConfigForm } from './GiteaConfigForm';
import { ScheduleConfigForm } from './ScheduleConfigForm'; import { ScheduleConfigForm } from './ScheduleConfigForm';
@@ -52,6 +52,8 @@ export function ConfigTabs() {
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 [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
const [isAutoSaving, setIsAutoSaving] = useState<boolean>(false);
const autoSaveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isConfigFormValid = (): boolean => { const isConfigFormValid = (): boolean => {
const { githubConfig, giteaConfig } = config; const { githubConfig, giteaConfig } = config;
@@ -150,6 +152,65 @@ export function ConfigTabs() {
} }
}; };
// 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
// Clear any existing timeout
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
// Debounce the auto-save to prevent excessive API calls
autoSaveTimeoutRef.current = setTimeout(async () => {
setIsAutoSaving(true);
const reqPayload: SaveConfigApiRequest = {
userId: user.id!,
githubConfig: config.githubConfig,
giteaConfig: config.giteaConfig,
scheduleConfig: scheduleConfig,
};
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
await refreshUser();
} else {
toast.error(
`Auto-save failed: ${result.message || 'Unknown error'}`,
{ duration: 3000 }
);
}
} catch (error) {
toast.error(
`Auto-save error: ${
error instanceof Error ? error.message : String(error)
}`,
{ duration: 3000 }
);
} finally {
setIsAutoSaving(false);
}
}, 500); // 500ms debounce
}, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig, refreshUser]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
};
}, []);
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
@@ -331,6 +392,8 @@ export function ConfigTabs() {
: update, : update,
})) }))
} }
onAutoSave={autoSaveScheduleConfig}
isAutoSaving={isAutoSaving}
/> />
</div> </div>
</div> </div>

View File

@@ -9,33 +9,36 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "../ui/select"; } from "../ui/select";
import { RefreshCw } from "lucide-react";
interface ScheduleConfigFormProps { interface ScheduleConfigFormProps {
config: ScheduleConfig; config: ScheduleConfig;
setConfig: React.Dispatch<React.SetStateAction<ScheduleConfig>>; setConfig: React.Dispatch<React.SetStateAction<ScheduleConfig>>;
onAutoSave?: (config: ScheduleConfig) => void;
isAutoSaving?: boolean;
} }
export function ScheduleConfigForm({ export function ScheduleConfigForm({
config, config,
setConfig, setConfig,
onAutoSave,
isAutoSaving = false,
}: ScheduleConfigFormProps) { }: ScheduleConfigFormProps) {
const handleChange = ( const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => { ) => {
const { name, value, type } = e.target; const { name, value, type } = e.target;
setConfig({ const newConfig = {
...config, ...config,
[name]: [name]:
type === "checkbox" ? (e.target as HTMLInputElement).checked : value, type === "checkbox" ? (e.target as HTMLInputElement).checked : value,
}); };
}; setConfig(newConfig);
// Convert seconds to human-readable format // Trigger auto-save for schedule config changes
const formatInterval = (seconds: number): string => { if (onAutoSave) {
if (seconds < 60) return `${seconds} seconds`; onAutoSave(newConfig);
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`; }
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`;
return `${Math.floor(seconds / 86400)} days`;
}; };
// Predefined intervals // Predefined intervals
@@ -55,7 +58,13 @@ export function ScheduleConfigForm({
return ( return (
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6 relative">
{isAutoSaving && (
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
<span className="text-xs">Auto-saving...</span>
</div>
)}
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<div className="flex items-center"> <div className="flex items-center">
<Checkbox <Checkbox