mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 04:26:44 +03:00
feat: implement navigation context and enhance component loading states across the application
This commit is contained in:
@@ -26,6 +26,7 @@ import { useFilterParams } from '@/hooks/useFilterParams';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useLiveRefresh } from '@/hooks/useLiveRefresh';
|
import { useLiveRefresh } from '@/hooks/useLiveRefresh';
|
||||||
import { useConfigStatus } from '@/hooks/useConfigStatus';
|
import { useConfigStatus } from '@/hooks/useConfigStatus';
|
||||||
|
import { useNavigation } from '@/components/layout/MainLayout';
|
||||||
|
|
||||||
type MirrorJobWithKey = MirrorJob & { _rowKey: string };
|
type MirrorJobWithKey = MirrorJob & { _rowKey: string };
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export function ActivityLog() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { registerRefreshCallback } = useLiveRefresh();
|
const { registerRefreshCallback } = useLiveRefresh();
|
||||||
const { isFullyConfigured } = useConfigStatus();
|
const { isFullyConfigured } = useConfigStatus();
|
||||||
|
const { navigationKey } = useNavigation();
|
||||||
|
|
||||||
const [activities, setActivities] = useState<MirrorJobWithKey[]>([]);
|
const [activities, setActivities] = useState<MirrorJobWithKey[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -71,7 +73,7 @@ export function ActivityLog() {
|
|||||||
/* ------------------------- initial fetch --------------------------- */
|
/* ------------------------- initial fetch --------------------------- */
|
||||||
|
|
||||||
const fetchActivities = useCallback(async () => {
|
const fetchActivities = useCallback(async () => {
|
||||||
if (!user) return false;
|
if (!user?.id) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -101,11 +103,13 @@ export function ActivityLog() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user?.id]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Reset loading state when component becomes active
|
||||||
|
setIsLoading(true);
|
||||||
fetchActivities();
|
fetchActivities();
|
||||||
}, [fetchActivities]);
|
}, [fetchActivities, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
// Register with global live refresh system
|
// Register with global live refresh system
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { apiRequest } from '@/lib/utils';
|
|||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { invalidateConfigCache } from '@/hooks/useConfigStatus';
|
||||||
|
|
||||||
type ConfigState = {
|
type ConfigState = {
|
||||||
githubConfig: GitHubConfig;
|
githubConfig: GitHubConfig;
|
||||||
@@ -117,6 +118,8 @@ export function ConfigTabs() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
setIsConfigSaved(true);
|
setIsConfigSaved(true);
|
||||||
|
// Invalidate config cache so other components get fresh data
|
||||||
|
invalidateConfigCache();
|
||||||
toast.success(
|
toast.success(
|
||||||
'Configuration saved successfully! Now import your GitHub data to begin.',
|
'Configuration saved successfully! Now import your GitHub data to begin.',
|
||||||
);
|
);
|
||||||
@@ -165,6 +168,8 @@ export function ConfigTabs() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Silent success - no toast for auto-save
|
// Silent success - no toast for auto-save
|
||||||
// Removed refreshUser() call to prevent page reload
|
// Removed refreshUser() call to prevent page reload
|
||||||
|
// Invalidate config cache so other components get fresh data
|
||||||
|
invalidateConfigCache();
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||||
import { usePageVisibility } from "@/hooks/usePageVisibility";
|
import { usePageVisibility } from "@/hooks/usePageVisibility";
|
||||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||||
|
import { useNavigation } from "@/components/layout/MainLayout";
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { registerRefreshCallback } = useLiveRefresh();
|
const { registerRefreshCallback } = useLiveRefresh();
|
||||||
const isPageVisible = usePageVisibility();
|
const isPageVisible = usePageVisibility();
|
||||||
const { isFullyConfigured } = useConfigStatus();
|
const { isFullyConfigured } = useConfigStatus();
|
||||||
|
const { navigationKey } = useNavigation();
|
||||||
|
|
||||||
const [repositories, setRepositories] = useState<Repository[]>([]);
|
const [repositories, setRepositories] = useState<Repository[]>([]);
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
@@ -68,7 +70,7 @@ export function Dashboard() {
|
|||||||
// Extract fetchDashboardData as a stable callback
|
// Extract fetchDashboardData as a stable callback
|
||||||
const fetchDashboardData = useCallback(async (showToast = false) => {
|
const fetchDashboardData = useCallback(async (showToast = false) => {
|
||||||
try {
|
try {
|
||||||
if (!user || !user.id) {
|
if (!user?.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,12 +116,14 @@ export function Dashboard() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, isFullyConfigured]);
|
}, [user?.id, isFullyConfigured]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
// Initial data fetch
|
// Initial data fetch and reset loading state when component becomes active
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Reset loading state when component mounts or becomes active
|
||||||
|
setIsLoading(true);
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, [fetchDashboardData]);
|
}, [fetchDashboardData, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
// Setup dashboard auto-refresh (30 seconds) and register with live refresh
|
// Setup dashboard auto-refresh (30 seconds) and register with live refresh
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { useConfigStatus } from "@/hooks/useConfigStatus";
|
|||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log";
|
currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log";
|
||||||
|
onNavigate?: (page: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ currentPage }: HeaderProps) {
|
export function Header({ currentPage, onNavigate }: HeaderProps) {
|
||||||
const { user, logout, isLoading } = useAuth();
|
const { user, logout, isLoading } = useAuth();
|
||||||
const { isLiveEnabled, toggleLive } = useLiveRefresh();
|
const { isLiveEnabled, toggleLive } = useLiveRefresh();
|
||||||
const { isFullyConfigured, isLoading: configLoading } = useConfigStatus();
|
const { isFullyConfigured, isLoading: configLoading } = useConfigStatus();
|
||||||
@@ -49,10 +50,18 @@ export function Header({ currentPage }: HeaderProps) {
|
|||||||
return (
|
return (
|
||||||
<header className="border-b bg-background">
|
<header className="border-b bg-background">
|
||||||
<div className="flex h-[4.5rem] items-center justify-between px-6">
|
<div className="flex h-[4.5rem] items-center justify-between px-6">
|
||||||
<a href="/" className="flex items-center gap-2 py-1">
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (currentPage !== 'dashboard') {
|
||||||
|
window.history.pushState({}, '', '/');
|
||||||
|
onNavigate?.('dashboard');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 py-1 hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
<SiGitea className="h-6 w-6" />
|
<SiGitea className="h-6 w-6" />
|
||||||
<span className="text-xl font-bold">Gitea Mirror</span>
|
<span className="text-xl font-bold">Gitea Mirror</span>
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{showLiveButton && (
|
{showLiveButton && (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect, createContext, useContext } from "react";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
import { Dashboard } from "@/components/dashboard/Dashboard";
|
import { Dashboard } from "@/components/dashboard/Dashboard";
|
||||||
@@ -9,6 +10,12 @@ import { Organization } from "../organizations/Organization";
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRepoSync } from "@/hooks/useSyncRepo";
|
import { useRepoSync } from "@/hooks/useSyncRepo";
|
||||||
|
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||||
|
|
||||||
|
// Navigation context to signal when navigation happens
|
||||||
|
const NavigationContext = createContext<{ navigationKey: number }>({ navigationKey: 0 });
|
||||||
|
|
||||||
|
export const useNavigation = () => useContext(NavigationContext);
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
page:
|
page:
|
||||||
@@ -32,8 +39,12 @@ export default function App({ page }: AppProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppWithProviders({ page }: AppProps) {
|
function AppWithProviders({ page: initialPage }: AppProps) {
|
||||||
const { user } = useAuth();
|
const { user, isLoading: authLoading } = useAuth();
|
||||||
|
const { isLoading: configLoading } = useConfigStatus();
|
||||||
|
const [currentPage, setCurrentPage] = useState<AppProps['page']>(initialPage);
|
||||||
|
const [navigationKey, setNavigationKey] = useState(0);
|
||||||
|
|
||||||
useRepoSync({
|
useRepoSync({
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
enabled: user?.syncEnabled,
|
enabled: user?.syncEnabled,
|
||||||
@@ -42,20 +53,63 @@ function AppWithProviders({ page }: AppProps) {
|
|||||||
nextSync: user?.nextSync,
|
nextSync: user?.nextSync,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle navigation from sidebar
|
||||||
|
const handleNavigation = (pageName: string) => {
|
||||||
|
setCurrentPage(pageName as AppProps['page']);
|
||||||
|
// Increment navigation key to force components to refresh their loading state
|
||||||
|
setNavigationKey(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle browser back/forward navigation
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = () => {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const pageMap: Record<string, AppProps['page']> = {
|
||||||
|
'/': 'dashboard',
|
||||||
|
'/repositories': 'repositories',
|
||||||
|
'/organizations': 'organizations',
|
||||||
|
'/config': 'configuration',
|
||||||
|
'/activity': 'activity-log'
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageName = pageMap[path] || 'dashboard';
|
||||||
|
setCurrentPage(pageName);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
return () => window.removeEventListener('popstate', handlePopState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Show loading state only during initial auth/config loading
|
||||||
|
const isInitialLoading = authLoading || (configLoading && !user);
|
||||||
|
|
||||||
|
if (isInitialLoading) {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col">
|
<NavigationContext.Provider value={{ navigationKey }}>
|
||||||
<Header currentPage={page} />
|
<main className="flex min-h-screen flex-col">
|
||||||
<div className="flex flex-1">
|
<Header currentPage={currentPage} onNavigate={handleNavigation} />
|
||||||
<Sidebar />
|
<div className="flex flex-1">
|
||||||
<section className="flex-1 p-6 overflow-y-auto h-[calc(100dvh-4.55rem)]">
|
<Sidebar onNavigate={handleNavigation} />
|
||||||
{page === "dashboard" && <Dashboard />}
|
<section className="flex-1 p-6 overflow-y-auto h-[calc(100dvh-4.55rem)]">
|
||||||
{page === "repositories" && <Repository />}
|
{currentPage === "dashboard" && <Dashboard />}
|
||||||
{page === "organizations" && <Organization />}
|
{currentPage === "repositories" && <Repository />}
|
||||||
{page === "configuration" && <ConfigTabs />}
|
{currentPage === "organizations" && <Organization />}
|
||||||
{page === "activity-log" && <ActivityLog />}
|
{currentPage === "configuration" && <ConfigTabs />}
|
||||||
</section>
|
{currentPage === "activity-log" && <ActivityLog />}
|
||||||
</div>
|
</section>
|
||||||
<Toaster />
|
</div>
|
||||||
</main>
|
<Toaster />
|
||||||
|
</main>
|
||||||
|
</NavigationContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { VersionInfo } from "./VersionInfo";
|
|||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onNavigate?: (page: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ className }: SidebarProps) {
|
export function Sidebar({ className, onNavigate }: SidebarProps) {
|
||||||
const [currentPath, setCurrentPath] = useState<string>("");
|
const [currentPath, setCurrentPath] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -18,6 +19,39 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
console.log("Hydrated path:", path); // Should log now
|
console.log("Hydrated path:", path); // Should log now
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Listen for URL changes (browser back/forward)
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = () => {
|
||||||
|
setCurrentPath(window.location.pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
return () => window.removeEventListener('popstate', handlePopState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNavigation = (href: string, event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Don't navigate if already on the same page
|
||||||
|
if (currentPath === href) return;
|
||||||
|
|
||||||
|
// Update URL without page reload
|
||||||
|
window.history.pushState({}, '', href);
|
||||||
|
setCurrentPath(href);
|
||||||
|
|
||||||
|
// Map href to page name for the parent component
|
||||||
|
const pageMap: Record<string, string> = {
|
||||||
|
'/': 'dashboard',
|
||||||
|
'/repositories': 'repositories',
|
||||||
|
'/organizations': 'organizations',
|
||||||
|
'/config': 'configuration',
|
||||||
|
'/activity': 'activity-log'
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageName = pageMap[href] || 'dashboard';
|
||||||
|
onNavigate?.(pageName);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={cn("w-64 border-r bg-background", className)}>
|
<aside className={cn("w-64 border-r bg-background", className)}>
|
||||||
<div className="flex flex-col h-full pt-4">
|
<div className="flex flex-col h-full pt-4">
|
||||||
@@ -27,11 +61,11 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
const Icon = link.icon;
|
const Icon = link.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
href={link.href}
|
onClick={(e) => handleNavigation(link.href, e)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors w-full text-left",
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
@@ -39,7 +73,7 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
>
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
{link.label}
|
{link.label}
|
||||||
</a>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useFilterParams } from "@/hooks/useFilterParams";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||||
|
import { useNavigation } from "@/components/layout/MainLayout";
|
||||||
|
|
||||||
export function Organization() {
|
export function Organization() {
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
@@ -34,6 +35,7 @@ export function Organization() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { registerRefreshCallback } = useLiveRefresh();
|
const { registerRefreshCallback } = useLiveRefresh();
|
||||||
const { isGitHubConfigured } = useConfigStatus();
|
const { isGitHubConfigured } = useConfigStatus();
|
||||||
|
const { navigationKey } = useNavigation();
|
||||||
const { filter, setFilter } = useFilterParams({
|
const { filter, setFilter } = useFilterParams({
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
membershipRole: "",
|
membershipRole: "",
|
||||||
@@ -63,7 +65,7 @@ export function Organization() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fetchOrganizations = useCallback(async () => {
|
const fetchOrganizations = useCallback(async () => {
|
||||||
if (!user || !user.id) {
|
if (!user?.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +100,13 @@ export function Organization() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, isGitHubConfigured]);
|
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Reset loading state when component becomes active
|
||||||
|
setIsLoading(true);
|
||||||
fetchOrganizations();
|
fetchOrganizations();
|
||||||
}, [fetchOrganizations]);
|
}, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
// Register with global live refresh system
|
// Register with global live refresh system
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ import type { SyncRepoRequest, SyncRepoResponse } from "@/types/sync";
|
|||||||
import { OwnerCombobox, OrganizationCombobox } from "./RepositoryComboboxes";
|
import { OwnerCombobox, OrganizationCombobox } from "./RepositoryComboboxes";
|
||||||
import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry";
|
import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry";
|
||||||
import AddRepositoryDialog from "./AddRepositoryDialog";
|
import AddRepositoryDialog from "./AddRepositoryDialog";
|
||||||
import type { ConfigApiResponse } from "@/types/config";
|
|
||||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||||
|
import { useNavigation } from "@/components/layout/MainLayout";
|
||||||
|
|
||||||
export default function Repository() {
|
export default function Repository() {
|
||||||
const [repositories, setRepositories] = useState<Repository[]>([]);
|
const [repositories, setRepositories] = useState<Repository[]>([]);
|
||||||
@@ -37,6 +38,7 @@ export default function Repository() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { registerRefreshCallback } = useLiveRefresh();
|
const { registerRefreshCallback } = useLiveRefresh();
|
||||||
const { isGitHubConfigured } = useConfigStatus();
|
const { isGitHubConfigured } = useConfigStatus();
|
||||||
|
const { navigationKey } = useNavigation();
|
||||||
const { filter, setFilter } = useFilterParams({
|
const { filter, setFilter } = useFilterParams({
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
status: "",
|
status: "",
|
||||||
@@ -79,7 +81,7 @@ export default function Repository() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fetchRepositories = useCallback(async () => {
|
const fetchRepositories = useCallback(async () => {
|
||||||
if (!user) return;
|
if (!user?.id) return;
|
||||||
|
|
||||||
// Don't fetch repositories if GitHub is not configured or still loading config
|
// Don't fetch repositories if GitHub is not configured or still loading config
|
||||||
if (!isGitHubConfigured) {
|
if (!isGitHubConfigured) {
|
||||||
@@ -112,11 +114,13 @@ export default function Repository() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, isGitHubConfigured]);
|
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Reset loading state when component becomes active
|
||||||
|
setIsLoading(true);
|
||||||
fetchRepositories();
|
fetchRepositories();
|
||||||
}, [fetchRepositories]);
|
}, [fetchRepositories, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
// Register with global live refresh system
|
// Register with global live refresh system
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import { useAuth } from './useAuth';
|
import { useAuth } from './useAuth';
|
||||||
import { apiRequest } from '@/lib/utils';
|
import { apiRequest } from '@/lib/utils';
|
||||||
import type { ConfigApiResponse } from '@/types/config';
|
import type { ConfigApiResponse } from '@/types/config';
|
||||||
@@ -11,9 +11,19 @@ interface ConfigStatus {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache to prevent duplicate API calls across components
|
||||||
|
let configCache: { data: ConfigApiResponse | null; timestamp: number; userId: string | null } = {
|
||||||
|
data: null,
|
||||||
|
timestamp: 0,
|
||||||
|
userId: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const CACHE_DURATION = 30000; // 30 seconds cache
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to check if GitHub and Gitea are properly configured
|
* Hook to check if GitHub and Gitea are properly configured
|
||||||
* Returns configuration status and prevents unnecessary API calls when not configured
|
* Returns configuration status and prevents unnecessary API calls when not configured
|
||||||
|
* Uses caching to prevent duplicate API calls across components
|
||||||
*/
|
*/
|
||||||
export function useConfigStatus(): ConfigStatus {
|
export function useConfigStatus(): ConfigStatus {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -25,6 +35,9 @@ export function useConfigStatus(): ConfigStatus {
|
|||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track if this hook has already checked config to prevent multiple calls
|
||||||
|
const hasCheckedRef = useRef(false);
|
||||||
|
|
||||||
const checkConfiguration = useCallback(async () => {
|
const checkConfiguration = useCallback(async () => {
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
setConfigStatus({
|
setConfigStatus({
|
||||||
@@ -37,22 +50,23 @@ export function useConfigStatus(): ConfigStatus {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Check cache first
|
||||||
setConfigStatus(prev => ({ ...prev, isLoading: true, error: null }));
|
const now = Date.now();
|
||||||
|
const isCacheValid = configCache.data &&
|
||||||
|
configCache.userId === user.id &&
|
||||||
|
(now - configCache.timestamp) < CACHE_DURATION;
|
||||||
|
|
||||||
const configResponse = await apiRequest<ConfigApiResponse>(
|
if (isCacheValid && hasCheckedRef.current) {
|
||||||
`/config?userId=${user.id}`,
|
const configResponse = configCache.data!;
|
||||||
{ method: 'GET' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const isGitHubConfigured = !!(
|
const isGitHubConfigured = !!(
|
||||||
configResponse?.githubConfig?.username &&
|
configResponse?.githubConfig?.username &&
|
||||||
configResponse?.githubConfig?.token
|
configResponse?.githubConfig?.token
|
||||||
);
|
);
|
||||||
|
|
||||||
const isGiteaConfigured = !!(
|
const isGiteaConfigured = !!(
|
||||||
configResponse?.giteaConfig?.url &&
|
configResponse?.giteaConfig?.url &&
|
||||||
configResponse?.giteaConfig?.username &&
|
configResponse?.giteaConfig?.username &&
|
||||||
configResponse?.giteaConfig?.token
|
configResponse?.giteaConfig?.token
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -65,6 +79,49 @@ export function useConfigStatus(): ConfigStatus {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Only show loading if we haven't checked before or cache is invalid
|
||||||
|
if (!hasCheckedRef.current) {
|
||||||
|
setConfigStatus(prev => ({ ...prev, isLoading: true, error: null }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const configResponse = await apiRequest<ConfigApiResponse>(
|
||||||
|
`/config?userId=${user.id}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
configCache = {
|
||||||
|
data: configResponse,
|
||||||
|
timestamp: now,
|
||||||
|
userId: user.id
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGitHubConfigured = !!(
|
||||||
|
configResponse?.githubConfig?.username &&
|
||||||
|
configResponse?.githubConfig?.token
|
||||||
|
);
|
||||||
|
|
||||||
|
const isGiteaConfigured = !!(
|
||||||
|
configResponse?.giteaConfig?.url &&
|
||||||
|
configResponse?.giteaConfig?.username &&
|
||||||
|
configResponse?.giteaConfig?.token
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFullyConfigured = isGitHubConfigured && isGiteaConfigured;
|
||||||
|
|
||||||
|
setConfigStatus({
|
||||||
|
isGitHubConfigured,
|
||||||
|
isGiteaConfigured,
|
||||||
|
isFullyConfigured,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
hasCheckedRef.current = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setConfigStatus({
|
setConfigStatus({
|
||||||
isGitHubConfigured: false,
|
isGitHubConfigured: false,
|
||||||
@@ -73,6 +130,7 @@ export function useConfigStatus(): ConfigStatus {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Failed to check configuration',
|
error: error instanceof Error ? error.message : 'Failed to check configuration',
|
||||||
});
|
});
|
||||||
|
hasCheckedRef.current = true;
|
||||||
}
|
}
|
||||||
}, [user?.id]);
|
}, [user?.id]);
|
||||||
|
|
||||||
@@ -82,3 +140,8 @@ export function useConfigStatus(): ConfigStatus {
|
|||||||
|
|
||||||
return configStatus;
|
return configStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export function to invalidate cache when config is updated
|
||||||
|
export function invalidateConfigCache() {
|
||||||
|
configCache = { data: null, timestamp: 0, userId: null };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user