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
},
});
const { user, refreshUser } = useAuth();
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true);
const [isSyncing, setIsSyncing] = useState<boolean>(false);
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
const [isAutoSavingSchedule, setIsAutoSavingSchedule] = 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 autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveGitHubTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveGiteaTimeoutRef = useRef<NodeJS.Timeout | null>(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() {
<div className="flex gap-x-4">
<Button
onClick={handleImportGitHubData}
disabled={isSyncing || !isConfigSaved}
disabled={isSyncing || !isConfigFormValid()}
title={
!isConfigSaved
? 'Save configuration first'
!isConfigFormValid()
? 'Please fill all required GitHub and Gitea fields'
: isSyncing
? 'Import in progress'
: 'Import GitHub Data'
@@ -422,17 +491,6 @@ export function ConfigTabs() {
</>
)}
</Button>
<Button
onClick={handleSaveConfig}
disabled={!isConfigFormValid()}
title={
!isConfigFormValid()
? 'Please fill all required fields'
: 'Save Configuration'
}
>
Save Configuration
</Button>
</div>
</div>
@@ -450,6 +508,8 @@ export function ConfigTabs() {
: update,
}))
}
onAutoSave={autoSaveGitHubConfig}
isAutoSaving={isAutoSavingGitHub}
/>
<GiteaConfigForm
config={config.giteaConfig}
@@ -462,6 +522,8 @@ export function ConfigTabs() {
: update,
}))
}
onAutoSave={autoSaveGiteaConfig}
isAutoSaving={isAutoSavingGitea}
/>
</div>
<div className="flex gap-x-4">

View File

@@ -20,9 +20,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
interface GitHubConfigFormProps {
config: 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 handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -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 () => {

View File

@@ -21,19 +21,27 @@ import { toast } from "sonner";
interface GiteaConfigFormProps {
config: 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 handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
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 () => {