import { useEffect, useState, useCallback, useRef } from 'react'; import { GitHubConfigForm } from './GitHubConfigForm'; import { GiteaConfigForm } from './GiteaConfigForm'; import { AutomationSettings } from './AutomationSettings'; import { SSOSettings } from './SSOSettings'; import type { ConfigApiResponse, GiteaConfig, GitHubConfig, SaveConfigApiRequest, SaveConfigApiResponse, ScheduleConfig, DatabaseCleanupConfig, MirrorOptions, AdvancedOptions, } from '@/types/config'; import { Button } from '../ui/button'; import { useAuth } from '@/hooks/useAuth'; import { apiRequest, showErrorToast } from '@/lib/utils'; import { RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { Skeleton } from '@/components/ui/skeleton'; import { invalidateConfigCache } from '@/hooks/useConfigStatus'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; type ConfigState = { githubConfig: GitHubConfig; giteaConfig: GiteaConfig; scheduleConfig: ScheduleConfig; cleanupConfig: DatabaseCleanupConfig; mirrorOptions: MirrorOptions; advancedOptions: AdvancedOptions; }; export function ConfigTabs() { const [config, setConfig] = useState({ githubConfig: { username: '', token: '', privateRepositories: false, mirrorStarred: false, }, giteaConfig: { url: '', username: '', token: '', organization: 'github-mirrors', visibility: 'public', starredReposOrg: 'github', preserveOrgStructure: false, }, scheduleConfig: { enabled: false, interval: 3600, }, cleanupConfig: { enabled: false, retentionDays: 604800, // 7 days in seconds }, mirrorOptions: { mirrorReleases: false, mirrorMetadata: false, metadataComponents: { issues: false, pullRequests: false, labels: false, milestones: false, wiki: false, }, }, advancedOptions: { skipForks: false, skipStarredIssues: false, }, }); const { user } = useAuth(); const [isLoading, setIsLoading] = useState(true); const [isSyncing, setIsSyncing] = 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; const isGitHubValid = !!( githubConfig.username.trim() && githubConfig.token.trim() ); const isGiteaValid = !!( giteaConfig.url.trim() && giteaConfig.username.trim() && giteaConfig.token.trim() ); return isGitHubValid && isGiteaValid; }; const isGitHubConfigValid = (): boolean => { const { githubConfig } = config; return !!(githubConfig.username.trim() && githubConfig.token.trim()); }; // Removed the problematic useEffect that was causing circular dependencies // The lastRun and nextRun should be managed by the backend and fetched via API const handleImportGitHubData = async () => { if (!user?.id) return; setIsSyncing(true); try { const result = await apiRequest<{ success: boolean; message?: string }>( `/sync?userId=${user.id}`, { method: 'POST' }, ); result.success ? toast.success( 'GitHub data imported successfully! Head to the Repositories page to start mirroring.', ) : toast.error( `Failed to import GitHub data: ${ result.message || 'Unknown error' }`, ); } catch (error) { toast.error( `Error importing GitHub data: ${ error instanceof Error ? error.message : String(error) }`, ); } finally { setIsSyncing(false); } }; // Auto-save function specifically for schedule config changes const autoSaveScheduleConfig = useCallback(async (scheduleConfig: ScheduleConfig) => { if (!user?.id) return; // Clear any existing timeout if (autoSaveScheduleTimeoutRef.current) { clearTimeout(autoSaveScheduleTimeoutRef.current); } // Debounce the auto-save to prevent excessive API calls autoSaveScheduleTimeoutRef.current = setTimeout(async () => { setIsAutoSavingSchedule(true); const reqPayload: SaveConfigApiRequest = { userId: user.id!, githubConfig: config.githubConfig, giteaConfig: config.giteaConfig, scheduleConfig: scheduleConfig, cleanupConfig: config.cleanupConfig, mirrorOptions: config.mirrorOptions, advancedOptions: config.advancedOptions, }; 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 // Removed refreshUser() call to prevent page reload // Invalidate config cache so other components get fresh data invalidateConfigCache(); // Fetch updated config to get the recalculated nextRun time try { const updatedResponse = await apiRequest( `/config?userId=${user.id}`, { method: 'GET' }, ); if (updatedResponse && !updatedResponse.error) { setConfig(prev => ({ ...prev, scheduleConfig: updatedResponse.scheduleConfig || prev.scheduleConfig, })); } } catch (fetchError) { console.warn('Failed to fetch updated config after auto-save:', fetchError); } } else { showErrorToast( `Auto-save failed: ${result.message || 'Unknown error'}`, toast ); } } catch (error) { showErrorToast(error, toast); } finally { setIsAutoSavingSchedule(false); } }, 500); // 500ms debounce }, [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) return; // Clear any existing timeout if (autoSaveCleanupTimeoutRef.current) { clearTimeout(autoSaveCleanupTimeoutRef.current); } // Debounce the auto-save to prevent excessive API calls autoSaveCleanupTimeoutRef.current = setTimeout(async () => { setIsAutoSavingCleanup(true); const reqPayload: SaveConfigApiRequest = { userId: user.id!, githubConfig: config.githubConfig, giteaConfig: config.giteaConfig, scheduleConfig: config.scheduleConfig, cleanupConfig: cleanupConfig, mirrorOptions: config.mirrorOptions, advancedOptions: config.advancedOptions, }; 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(); // Fetch updated config to get the recalculated nextRun time try { const updatedResponse = await apiRequest( `/config?userId=${user.id}`, { method: 'GET' }, ); if (updatedResponse && !updatedResponse.error) { setConfig(prev => ({ ...prev, cleanupConfig: updatedResponse.cleanupConfig || prev.cleanupConfig, })); } } catch (fetchError) { console.warn('Failed to fetch updated config after auto-save:', fetchError); } } else { showErrorToast( `Auto-save failed: ${result.message || 'Unknown error'}`, toast ); } } catch (error) { showErrorToast(error, toast); } finally { setIsAutoSavingCleanup(false); } }, 500); // 500ms debounce }, [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, mirrorOptions: config.mirrorOptions, advancedOptions: config.advancedOptions, }; 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, mirrorOptions: config.mirrorOptions, advancedOptions: config.advancedOptions, }; 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]); // Auto-save function for mirror options (handled within GitHub config) const autoSaveMirrorOptions = useCallback(async (mirrorOptions: MirrorOptions) => { if (!user?.id) return; const reqPayload: SaveConfigApiRequest = { userId: user.id!, githubConfig: config.githubConfig, giteaConfig: config.giteaConfig, scheduleConfig: config.scheduleConfig, cleanupConfig: config.cleanupConfig, mirrorOptions: mirrorOptions, advancedOptions: config.advancedOptions, }; 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) { invalidateConfigCache(); } else { showErrorToast( `Auto-save failed: ${result.message || 'Unknown error'}`, toast ); } } catch (error) { showErrorToast(error, toast); } }, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.advancedOptions]); // Auto-save function for advanced options (handled within GitHub config) const autoSaveAdvancedOptions = useCallback(async (advancedOptions: AdvancedOptions) => { if (!user?.id) return; const reqPayload: SaveConfigApiRequest = { userId: user.id!, githubConfig: config.githubConfig, giteaConfig: config.giteaConfig, scheduleConfig: config.scheduleConfig, cleanupConfig: config.cleanupConfig, mirrorOptions: config.mirrorOptions, advancedOptions: advancedOptions, }; 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) { invalidateConfigCache(); } else { showErrorToast( `Auto-save failed: ${result.message || 'Unknown error'}`, toast ); } } catch (error) { showErrorToast(error, toast); } }, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.mirrorOptions]); // Cleanup timeouts on unmount useEffect(() => { return () => { if (autoSaveScheduleTimeoutRef.current) { clearTimeout(autoSaveScheduleTimeoutRef.current); } if (autoSaveCleanupTimeoutRef.current) { clearTimeout(autoSaveCleanupTimeoutRef.current); } if (autoSaveGitHubTimeoutRef.current) { clearTimeout(autoSaveGitHubTimeoutRef.current); } if (autoSaveGiteaTimeoutRef.current) { clearTimeout(autoSaveGiteaTimeoutRef.current); } }; }, []); useEffect(() => { if (!user?.id) return; const fetchConfig = async () => { setIsLoading(true); try { const response = await apiRequest( `/config?userId=${user.id}`, { method: 'GET' }, ); if (response && !response.error) { setConfig({ githubConfig: response.githubConfig || config.githubConfig, giteaConfig: response.giteaConfig || config.giteaConfig, scheduleConfig: response.scheduleConfig || config.scheduleConfig, cleanupConfig: response.cleanupConfig || config.cleanupConfig, mirrorOptions: response.mirrorOptions || config.mirrorOptions, advancedOptions: response.advancedOptions || config.advancedOptions, }); } } catch (error) { console.warn( 'Could not fetch configuration, using defaults:', error, ); } setIsLoading(false); }; fetchConfig(); }, [user?.id]); // Only depend on user.id, not the entire user object function ConfigCardSkeleton() { return (
{/* Header section */}
{/* Content section - Grid layout */}
{/* GitHub & Gitea connections - Side by side */}
{/* Automation & Maintenance - Full width */}
); } return isLoading ? (
) : (
{/* Header section */}

Configuration

Configure your GitHub and Gitea connections, and set up automatic mirroring.

{/* Content section - Tabs layout */} Connections Automation Authentication
setConfig(prev => ({ ...prev, githubConfig: typeof update === 'function' ? update(prev.githubConfig) : update, })) } mirrorOptions={config.mirrorOptions} setMirrorOptions={update => setConfig(prev => ({ ...prev, mirrorOptions: typeof update === 'function' ? update(prev.mirrorOptions) : update, })) } advancedOptions={config.advancedOptions} setAdvancedOptions={update => setConfig(prev => ({ ...prev, advancedOptions: typeof update === 'function' ? update(prev.advancedOptions) : update, })) } onAutoSave={autoSaveGitHubConfig} onMirrorOptionsAutoSave={autoSaveMirrorOptions} onAdvancedOptionsAutoSave={autoSaveAdvancedOptions} isAutoSaving={isAutoSavingGitHub} /> setConfig(prev => ({ ...prev, giteaConfig: typeof update === 'function' ? update(prev.giteaConfig) : update, })) } onAutoSave={autoSaveGiteaConfig} isAutoSaving={isAutoSavingGitea} githubUsername={config.githubConfig.username} />
{ setConfig(prev => ({ ...prev, scheduleConfig: newConfig })); autoSaveScheduleConfig(newConfig); }} onCleanupChange={(newConfig) => { setConfig(prev => ({ ...prev, cleanupConfig: newConfig })); autoSaveCleanupConfig(newConfig); }} isAutoSavingSchedule={isAutoSavingSchedule} isAutoSavingCleanup={isAutoSavingCleanup} />
); }