mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-09 21:16:48 +03:00
refactor: improve loading state management and add live active indicator in RepositoryTable
This commit is contained in:
@@ -34,10 +34,10 @@ import { useNavigation } from "@/components/layout/MainLayout";
|
|||||||
|
|
||||||
export default function Repository() {
|
export default function Repository() {
|
||||||
const [repositories, setRepositories] = useState<Repository[]>([]);
|
const [repositories, setRepositories] = useState<Repository[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { registerRefreshCallback } = useLiveRefresh();
|
const { registerRefreshCallback, isLiveEnabled } = useLiveRefresh();
|
||||||
const { isGitHubConfigured } = useConfigStatus();
|
const { isGitHubConfigured, isFullyConfigured } = useConfigStatus();
|
||||||
const { navigationKey } = useNavigation();
|
const { navigationKey } = useNavigation();
|
||||||
const { filter, setFilter } = useFilterParams({
|
const { filter, setFilter } = useFilterParams({
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
@@ -80,17 +80,20 @@ export default function Repository() {
|
|||||||
onMessage: handleNewMessage,
|
onMessage: handleNewMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchRepositories = useCallback(async () => {
|
const fetchRepositories = useCallback(async (isLiveRefresh = false) => {
|
||||||
if (!user?.id) 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) {
|
||||||
setIsLoading(false);
|
setIsInitialLoading(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
// Set appropriate loading state based on refresh type
|
||||||
|
if (!isLiveRefresh) {
|
||||||
|
setIsInitialLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await apiRequest<RepositoryApiResponse>(
|
const response = await apiRequest<RepositoryApiResponse>(
|
||||||
`/github/repositories?userId=${user.id}`,
|
`/github/repositories?userId=${user.id}`,
|
||||||
@@ -103,23 +106,31 @@ export default function Repository() {
|
|||||||
setRepositories(response.repositories);
|
setRepositories(response.repositories);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error fetching repositories");
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
|
if (!isLiveRefresh) {
|
||||||
|
toast.error(response.error || "Error fetching repositories");
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
error instanceof Error ? error.message : "Error fetching repositories"
|
if (!isLiveRefresh) {
|
||||||
);
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Error fetching repositories"
|
||||||
|
);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (!isLiveRefresh) {
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset loading state when component becomes active
|
// Reset loading state when component becomes active
|
||||||
setIsLoading(true);
|
setIsInitialLoading(true);
|
||||||
fetchRepositories();
|
fetchRepositories(false); // Manual refresh, not live
|
||||||
}, [fetchRepositories, navigationKey]); // Include navigationKey to trigger on navigation
|
}, [fetchRepositories, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
// Register with global live refresh system
|
// Register with global live refresh system
|
||||||
@@ -130,14 +141,14 @@ export default function Repository() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unregister = registerRefreshCallback(() => {
|
const unregister = registerRefreshCallback(() => {
|
||||||
fetchRepositories();
|
fetchRepositories(true); // Live refresh
|
||||||
});
|
});
|
||||||
|
|
||||||
return unregister;
|
return unregister;
|
||||||
}, [registerRefreshCallback, fetchRepositories, isGitHubConfigured]);
|
}, [registerRefreshCallback, fetchRepositories, isGitHubConfigured]);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
const success = await fetchRepositories();
|
const success = await fetchRepositories(false); // Manual refresh, show loading skeleton
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Repositories refreshed successfully.");
|
toast.success("Repositories refreshed successfully.");
|
||||||
}
|
}
|
||||||
@@ -363,7 +374,7 @@ export default function Repository() {
|
|||||||
toast.success(`Repository added successfully`);
|
toast.success(`Repository added successfully`);
|
||||||
setRepositories((prevRepos) => [...prevRepos, response.repository]);
|
setRepositories((prevRepos) => [...prevRepos, response.repository]);
|
||||||
|
|
||||||
await fetchRepositories();
|
await fetchRepositories(false); // Manual refresh after adding repository
|
||||||
|
|
||||||
setFilter((prev) => ({
|
setFilter((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -463,7 +474,7 @@ export default function Repository() {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handleMirrorAllRepos}
|
onClick={handleMirrorAllRepos}
|
||||||
disabled={isLoading || loadingRepoIds.size > 0}
|
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
Mirror All
|
Mirror All
|
||||||
@@ -490,7 +501,8 @@ export default function Repository() {
|
|||||||
) : (
|
) : (
|
||||||
<RepositoryTable
|
<RepositoryTable
|
||||||
repositories={repositories}
|
repositories={repositories}
|
||||||
isLoading={isLoading || !connected}
|
isLoading={isInitialLoading || !connected}
|
||||||
|
isLiveActive={isLiveEnabled && isFullyConfigured}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
onMirror={handleMirrorRepo}
|
onMirror={handleMirrorRepo}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
|||||||
interface RepositoryTableProps {
|
interface RepositoryTableProps {
|
||||||
repositories: Repository[];
|
repositories: Repository[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isLiveActive?: boolean;
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
setFilter: (filter: FilterParams) => void;
|
setFilter: (filter: FilterParams) => void;
|
||||||
onMirror: ({ repoId }: { repoId: string }) => Promise<void>;
|
onMirror: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||||
@@ -24,6 +25,7 @@ interface RepositoryTableProps {
|
|||||||
export default function RepositoryTable({
|
export default function RepositoryTable({
|
||||||
repositories,
|
repositories,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isLiveActive = false,
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
onMirror,
|
onMirror,
|
||||||
@@ -345,15 +347,38 @@ export default function RepositoryTable({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Bar */}
|
{/* Status Bar */}
|
||||||
<div className="h-[40px] flex items-center justify-between border-t bg-muted/30 px-3">
|
<div className="h-[40px] flex items-center justify-between border-t bg-muted/30 px-3 relative">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-1.5 w-1.5 rounded-full bg-primary" />
|
<div className={`h-1.5 w-1.5 rounded-full ${isLiveActive ? 'bg-emerald-500' : 'bg-primary'}`} />
|
||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{hasAnyFilter
|
{hasAnyFilter
|
||||||
? `Showing ${filteredRepositories.length} of ${repositories.length} repositories`
|
? `Showing ${filteredRepositories.length} of ${repositories.length} repositories`
|
||||||
: `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}
|
: `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Center - Live active indicator */}
|
||||||
|
{isLiveActive && (
|
||||||
|
<div className="flex items-center gap-1.5 absolute left-1/2 transform -translate-x-1/2">
|
||||||
|
<div
|
||||||
|
className="h-1 w-1 rounded-full bg-emerald-500"
|
||||||
|
style={{
|
||||||
|
animation: 'pulse 2s ease-in-out infinite'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
|
||||||
|
Live active
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="h-1 w-1 rounded-full bg-emerald-500"
|
||||||
|
style={{
|
||||||
|
animation: 'pulse 2s ease-in-out infinite',
|
||||||
|
animationDelay: '1s'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasAnyFilter && (
|
{hasAnyFilter && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
Filters applied
|
Filters applied
|
||||||
|
|||||||
Reference in New Issue
Block a user