mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
abbd2a94ce
On the desktop admin GitHub panel, when "Load more" failed (for example GitHub rate limiting), the whole release history was replaced by the error card for the rest of the session: no list, no Load more button, no way to retry. Had the user found a way to load again, the next load would also have skipped the page that failed. Cause: in its one page per load mode useGithubReleases advanced the page counter whether the request worked or not and never cleared the error, and GitHubPanel showed the error card whenever an error was set. The phone (fillPages mode) already held the page, cleared the error on the next good load and drew the error inline under its timeline. Fix: the one page mode now moves the page on only after a good load and clears the error then, like fillPages. GitHubPanel shows the error card only when nothing has loaded yet; after that it keeps the timeline, shows the error as a line above Load more and leaves the button there. The page walk difference behind fillPages stays as it was. Red tests: - FE-COMP-GHREL-HOOK-012 (new, useGithubReleases.test.ts) failed with "AssertionError: expected [ 1, 2, 3 ] to deeply equal [ 1, 2, 2 ]". - FE-ADMIN-GH-017 (new, GitHubPanel.test.tsx) failed with "Unable to find an element with the text: /v1\.\d\.0/" after the failed load. Changed assertions in src/components/Admin/useGithubReleases.test.ts: - FE-COMP-GHREL-HOOK-004 (renamed from "sets the error and still moves the page on" to "sets the error and holds the page"): requested pages were [1, 2, 3], now [1, 2, 2].
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
import apiClient from '../../api/client';
|
|
import { getLocaleForLanguage, useTranslation } from '../../i18n';
|
|
|
|
const PER_PAGE = 10;
|
|
const MAX_PAGES_PER_LOAD = 5;
|
|
|
|
export interface GithubRelease {
|
|
id: number;
|
|
prerelease: boolean;
|
|
tag_name: string;
|
|
name: string | null;
|
|
body: string | null;
|
|
published_at: string | null;
|
|
created_at: string;
|
|
author: { login: string } | null;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface UseGithubReleasesOptions {
|
|
/** Whether this install runs a prerelease: prereleases only show to those. */
|
|
isPrerelease: boolean;
|
|
/**
|
|
* Keep pulling pages until at least one release survives the prerelease filter, up to
|
|
* MAX_PAGES_PER_LOAD per load. Without it each load reads exactly one page. Either way
|
|
* a failed page keeps what is already shown and leaves the page counter where it was,
|
|
* and the next good load clears the error.
|
|
*/
|
|
fillPages?: boolean;
|
|
}
|
|
|
|
/**
|
|
* The GitHub release history behind both admin shells (the desktop panel and the phone
|
|
* panel render their own timeline over it): the paged fetch from the server's proxy,
|
|
* which releases show, which ones are expanded, and their dates in the user's locale.
|
|
*/
|
|
export function useGithubReleases({ isPrerelease, fillPages = false }: UseGithubReleasesOptions) {
|
|
const { language } = useTranslation();
|
|
const [releases, setReleases] = useState<GithubRelease[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [expanded, setExpanded] = useState<Record<number, boolean>>({});
|
|
const [page, setPage] = useState(1);
|
|
const [hasMore, setHasMore] = useState(true);
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
|
|
const isShown = (release: GithubRelease) => isPrerelease || !release.prerelease;
|
|
|
|
const fetchPage = async (pageNum: number) => {
|
|
try {
|
|
const res = await apiClient.get(`/admin/github-releases`, { params: { per_page: PER_PAGE, page: pageNum } });
|
|
return Array.isArray(res.data) ? (res.data as GithubRelease[]) : [];
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const fetchReleases = async (pageNum = 1, append = false) => {
|
|
const data = await fetchPage(pageNum);
|
|
if (!data) return false;
|
|
setReleases((prev) => (append ? [...prev, ...data] : data));
|
|
setHasMore(data.length === PER_PAGE);
|
|
setError(null);
|
|
return true;
|
|
};
|
|
|
|
// Keep pulling pages until at least one release survives the prerelease filter,
|
|
// otherwise a page of nothing but prereleases leaves an empty timeline behind a
|
|
// "Load more" button. MAX_PAGES_PER_LOAD bounds the walk.
|
|
const loadFrom = async (startPage: number, append: boolean) => {
|
|
const collected: GithubRelease[] = [];
|
|
let pageNum = startPage;
|
|
let more = true;
|
|
|
|
for (let i = 0; i < MAX_PAGES_PER_LOAD; i++) {
|
|
// Step on only when another page follows, so `page` ends on the last one read.
|
|
if (i > 0) pageNum += 1;
|
|
const data = await fetchPage(pageNum);
|
|
if (!data) return;
|
|
collected.push(...data);
|
|
more = data.length === PER_PAGE;
|
|
if (!more || collected.some(isShown)) break;
|
|
}
|
|
|
|
setReleases((prev) => (append ? [...prev, ...collected] : collected));
|
|
setHasMore(more);
|
|
setPage(pageNum);
|
|
setError(null);
|
|
};
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
void (fillPages ? loadFrom(1, false) : fetchReleases(1)).finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const handleLoadMore = async () => {
|
|
if (fillPages) {
|
|
setLoadingMore(true);
|
|
await loadFrom(page + 1, true);
|
|
setLoadingMore(false);
|
|
return;
|
|
}
|
|
const next = page + 1;
|
|
setLoadingMore(true);
|
|
if (await fetchReleases(next, true)) setPage(next);
|
|
setLoadingMore(false);
|
|
};
|
|
|
|
const toggleExpand = (id: number) => {
|
|
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
};
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString(getLocaleForLanguage(language), { day: 'numeric', month: 'short', year: 'numeric' });
|
|
};
|
|
|
|
return {
|
|
releases,
|
|
shownReleases: releases.filter(isShown),
|
|
loading,
|
|
error,
|
|
expanded,
|
|
toggleExpand,
|
|
hasMore,
|
|
loadingMore,
|
|
handleLoadMore,
|
|
formatDate,
|
|
};
|
|
}
|