mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 12:36:44 +03:00
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:
@@ -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">
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user