|
|
@@ -5,16 +5,21 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|
|
import { RefreshCw } from "lucide-react";
|
|
|
|
|
|
import { useAuth } from "@/components/auth/authContext";
|
|
|
-import { ApiClientError, getBranches, search } from "@/lib/frontend/apiClient";
|
|
|
import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
|
|
|
import { searchPath } from "@/lib/frontend/routes";
|
|
|
|
|
|
import {
|
|
|
- SEARCH_SCOPE,
|
|
|
parseSearchUrlState,
|
|
|
serializeSearchUrlState,
|
|
|
+ SEARCH_SCOPE,
|
|
|
} from "@/lib/frontend/search/urlState";
|
|
|
+import { normalizeSearchUrlStateForUser } from "@/lib/frontend/search/normalizeState";
|
|
|
import { mapSearchError } from "@/lib/frontend/search/errorMapping";
|
|
|
+import { useSearchQuery } from "@/lib/frontend/search/useSearchQuery";
|
|
|
+import {
|
|
|
+ useSearchBranches,
|
|
|
+ BRANCH_LIST_STATE,
|
|
|
+} from "@/lib/frontend/search/useSearchBranches";
|
|
|
|
|
|
import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
|
|
|
import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard";
|
|
|
@@ -24,351 +29,169 @@ import { Button } from "@/components/ui/button";
|
|
|
import SearchForm from "@/components/search/SearchForm";
|
|
|
import SearchResults from "@/components/search/SearchResults";
|
|
|
|
|
|
-const PAGE_LIMIT = 100;
|
|
|
-
|
|
|
-const BRANCH_LIST_STATE = Object.freeze({
|
|
|
- IDLE: "idle",
|
|
|
- LOADING: "loading",
|
|
|
- READY: "ready",
|
|
|
- ERROR: "error",
|
|
|
-});
|
|
|
+function buildSearchHref({ routeBranch, state }) {
|
|
|
+ const base = searchPath(routeBranch);
|
|
|
+ const qs = serializeSearchUrlState(state);
|
|
|
+ return qs ? `${base}?${qs}` : base;
|
|
|
+}
|
|
|
|
|
|
-export default function SearchPage({ branch }) {
|
|
|
+export default function SearchPage({ branch: routeBranch }) {
|
|
|
const router = useRouter();
|
|
|
const searchParams = useSearchParams();
|
|
|
- const { status, user } = useAuth();
|
|
|
+ const { status: authStatus, user } = useAuth();
|
|
|
|
|
|
- const isAuthenticated = status === "authenticated" && user;
|
|
|
+ const isAuthenticated = authStatus === "authenticated" && user;
|
|
|
const isAdminDev =
|
|
|
isAuthenticated && (user.role === "admin" || user.role === "dev");
|
|
|
- const isBranchUser = isAuthenticated && user.role === "branch";
|
|
|
|
|
|
+ // 1) URL -> parsed state (pure helper)
|
|
|
const parsedUrlState = React.useMemo(() => {
|
|
|
- return parseSearchUrlState(searchParams, { routeBranch: branch });
|
|
|
- }, [searchParams, branch]);
|
|
|
+ return parseSearchUrlState(searchParams, { routeBranch });
|
|
|
+ }, [searchParams, routeBranch]);
|
|
|
|
|
|
- // Enforce "single = this route branch" and keep branch users safe.
|
|
|
+ // 2) Normalize for user role + route context (pure helper)
|
|
|
const urlState = React.useMemo(() => {
|
|
|
- // AuthGate ensures auth before rendering, but keep this defensive.
|
|
|
- if (!user) return parsedUrlState;
|
|
|
-
|
|
|
- // Branch users: always single, no cross-branch scope params.
|
|
|
- if (user.role === "branch") {
|
|
|
- return {
|
|
|
- ...parsedUrlState,
|
|
|
- scope: SEARCH_SCOPE.SINGLE,
|
|
|
- branch,
|
|
|
- branches: [],
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // Admin/dev: single scope is always the route branch context.
|
|
|
- if (parsedUrlState.scope === SEARCH_SCOPE.SINGLE) {
|
|
|
- return { ...parsedUrlState, branch };
|
|
|
- }
|
|
|
-
|
|
|
- return parsedUrlState;
|
|
|
- }, [parsedUrlState, user, branch]);
|
|
|
+ return normalizeSearchUrlStateForUser(parsedUrlState, {
|
|
|
+ routeBranch,
|
|
|
+ user,
|
|
|
+ });
|
|
|
+ }, [parsedUrlState, routeBranch, user]);
|
|
|
|
|
|
+ // 3) The identity of a first-page search (cursor intentionally excluded).
|
|
|
const searchKey = React.useMemo(() => {
|
|
|
- // This is our "query identity" without cursor.
|
|
|
return serializeSearchUrlState(urlState);
|
|
|
}, [urlState]);
|
|
|
|
|
|
- // Keep a ref of the latest key so async "load more" cannot append to a new search.
|
|
|
- const searchKeyRef = React.useRef(searchKey);
|
|
|
- React.useEffect(() => {
|
|
|
- searchKeyRef.current = searchKey;
|
|
|
- }, [searchKey]);
|
|
|
-
|
|
|
- // Input draft (URL remains the single source of truth for executed searches).
|
|
|
+ // 4) Draft input (URL remains SoT for executed searches).
|
|
|
const [qDraft, setQDraft] = React.useState(urlState.q || "");
|
|
|
React.useEffect(() => {
|
|
|
setQDraft(urlState.q || "");
|
|
|
}, [urlState.q]);
|
|
|
|
|
|
- // Admin/dev: load branch list for multi-select (fail-open).
|
|
|
- const [branchList, setBranchList] = React.useState({
|
|
|
- status: BRANCH_LIST_STATE.IDLE,
|
|
|
- branches: null,
|
|
|
- });
|
|
|
-
|
|
|
- React.useEffect(() => {
|
|
|
- if (!isAdminDev) return;
|
|
|
-
|
|
|
- let cancelled = false;
|
|
|
- setBranchList({ status: BRANCH_LIST_STATE.LOADING, branches: null });
|
|
|
-
|
|
|
- (async () => {
|
|
|
- try {
|
|
|
- const res = await getBranches();
|
|
|
- if (cancelled) return;
|
|
|
+ // 5) Admin/dev only: branches list for multi select (fail-open)
|
|
|
+ const branchesQuery = useSearchBranches({ enabled: isAdminDev });
|
|
|
|
|
|
- const branches = Array.isArray(res?.branches) ? res.branches : [];
|
|
|
- setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
|
|
|
- } catch (err) {
|
|
|
- if (cancelled) return;
|
|
|
-
|
|
|
- console.error("[SearchPage] getBranches failed:", err);
|
|
|
- setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
|
|
|
- }
|
|
|
- })();
|
|
|
-
|
|
|
- return () => {
|
|
|
- cancelled = true;
|
|
|
- };
|
|
|
- }, [isAdminDev, user?.userId]);
|
|
|
-
|
|
|
- // Search results state.
|
|
|
- const [statusState, setStatusState] = React.useState("idle"); // idle|loading|success|error
|
|
|
- const [items, setItems] = React.useState([]);
|
|
|
- const [nextCursor, setNextCursor] = React.useState(null);
|
|
|
- const [error, setError] = React.useState(null);
|
|
|
-
|
|
|
- const [isLoadingMore, setIsLoadingMore] = React.useState(false);
|
|
|
- const [loadMoreError, setLoadMoreError] = React.useState(null);
|
|
|
-
|
|
|
- const searchRequestIdRef = React.useRef(0);
|
|
|
- const loadMoreRequestIdRef = React.useRef(0);
|
|
|
+ // 6) Data lifecycle (first page + load more)
|
|
|
+ const query = useSearchQuery({
|
|
|
+ searchKey,
|
|
|
+ urlState,
|
|
|
+ routeBranch,
|
|
|
+ user,
|
|
|
+ limit: urlState.limit,
|
|
|
+ });
|
|
|
|
|
|
- const mappedError = React.useMemo(() => mapSearchError(error), [error]);
|
|
|
+ // 7) Map errors to German UX copy
|
|
|
+ const mappedError = React.useMemo(
|
|
|
+ () => mapSearchError(query.error),
|
|
|
+ [query.error]
|
|
|
+ );
|
|
|
const mappedLoadMoreError = React.useMemo(
|
|
|
- () => mapSearchError(loadMoreError),
|
|
|
- [loadMoreError]
|
|
|
+ () => mapSearchError(query.loadMoreError),
|
|
|
+ [query.loadMoreError]
|
|
|
);
|
|
|
|
|
|
- // Redirect on unauthenticated search calls (session expired mid-session).
|
|
|
+ // 8) Redirect when unauthenticated mid-request
|
|
|
React.useEffect(() => {
|
|
|
if (mappedError?.kind !== "unauthenticated") return;
|
|
|
|
|
|
const next =
|
|
|
typeof window !== "undefined"
|
|
|
? `${window.location.pathname}${window.location.search}`
|
|
|
- : searchPath(branch);
|
|
|
+ : searchPath(routeBranch);
|
|
|
|
|
|
window.location.replace(
|
|
|
buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next })
|
|
|
);
|
|
|
- }, [mappedError?.kind, branch]);
|
|
|
-
|
|
|
- function buildHref(nextState, { push } = {}) {
|
|
|
- const base = searchPath(branch);
|
|
|
- const qs = serializeSearchUrlState(nextState);
|
|
|
- const href = qs ? `${base}?${qs}` : base;
|
|
|
+ }, [mappedError?.kind, routeBranch]);
|
|
|
+
|
|
|
+ // 9) URL write helpers (search is URL-driven)
|
|
|
+ const pushStateToUrl = React.useCallback(
|
|
|
+ (nextState) => {
|
|
|
+ router.push(buildSearchHref({ routeBranch, state: nextState }));
|
|
|
+ },
|
|
|
+ [router, routeBranch]
|
|
|
+ );
|
|
|
|
|
|
- if (push) router.push(href);
|
|
|
- else router.replace(href);
|
|
|
- }
|
|
|
+ const replaceStateToUrl = React.useCallback(
|
|
|
+ (nextState) => {
|
|
|
+ router.replace(buildSearchHref({ routeBranch, state: nextState }));
|
|
|
+ },
|
|
|
+ [router, routeBranch]
|
|
|
+ );
|
|
|
|
|
|
- function handleSubmit() {
|
|
|
+ // 10) Handlers
|
|
|
+ const handleSubmit = React.useCallback(() => {
|
|
|
const nextState = {
|
|
|
+ ...urlState,
|
|
|
q: qDraft,
|
|
|
- // Branch users always single; admin/dev use selected scope.
|
|
|
- scope: isAdminDev ? urlState.scope : SEARCH_SCOPE.SINGLE,
|
|
|
- branch, // explicit for shareability (even though route already contains it)
|
|
|
- branches: urlState.branches,
|
|
|
- from: urlState.from,
|
|
|
- to: urlState.to,
|
|
|
+ branch: routeBranch,
|
|
|
};
|
|
|
|
|
|
- buildHref(nextState, { push: true });
|
|
|
- }
|
|
|
+ pushStateToUrl(nextState);
|
|
|
+ }, [urlState, qDraft, routeBranch, pushStateToUrl]);
|
|
|
|
|
|
- function handleScopeChange(nextScope) {
|
|
|
- if (!isAdminDev) return;
|
|
|
+ const handleScopeChange = React.useCallback(
|
|
|
+ (nextScope) => {
|
|
|
+ if (!isAdminDev) return;
|
|
|
|
|
|
- const nextState = {
|
|
|
- // Scope changes rerun the currently executed query (URL q), not the draft.
|
|
|
- q: urlState.q,
|
|
|
- scope: nextScope,
|
|
|
- branch,
|
|
|
- branches: nextScope === SEARCH_SCOPE.MULTI ? urlState.branches : [],
|
|
|
- from: urlState.from,
|
|
|
- to: urlState.to,
|
|
|
- };
|
|
|
-
|
|
|
- buildHref(nextState, { push: false });
|
|
|
- }
|
|
|
-
|
|
|
- function toggleMultiBranch(branchId) {
|
|
|
- if (!isAdminDev) return;
|
|
|
-
|
|
|
- const current = Array.isArray(urlState.branches) ? urlState.branches : [];
|
|
|
- const set = new Set(current);
|
|
|
-
|
|
|
- if (set.has(branchId)) set.delete(branchId);
|
|
|
- else set.add(branchId);
|
|
|
-
|
|
|
- const nextState = {
|
|
|
- q: urlState.q,
|
|
|
- scope: SEARCH_SCOPE.MULTI,
|
|
|
- branches: Array.from(set),
|
|
|
- from: urlState.from,
|
|
|
- to: urlState.to,
|
|
|
- };
|
|
|
-
|
|
|
- buildHref(nextState, { push: false });
|
|
|
- }
|
|
|
-
|
|
|
- function buildSearchRequest({ cursor = null } = {}) {
|
|
|
- const q = urlState.q;
|
|
|
-
|
|
|
- if (!q) return null;
|
|
|
-
|
|
|
- const base = {
|
|
|
- q,
|
|
|
- limit: PAGE_LIMIT,
|
|
|
- };
|
|
|
-
|
|
|
- if (urlState.from) base.from = urlState.from;
|
|
|
- if (urlState.to) base.to = urlState.to;
|
|
|
- if (cursor) base.cursor = cursor;
|
|
|
-
|
|
|
- // Branch role: never send scope/branches from URL (avoid forbidden / keep safe).
|
|
|
- if (isBranchUser) {
|
|
|
- base.branch = branch;
|
|
|
- return base;
|
|
|
- }
|
|
|
-
|
|
|
- // Admin/dev scopes:
|
|
|
- if (urlState.scope === SEARCH_SCOPE.ALL) {
|
|
|
- base.scope = "all";
|
|
|
- return base;
|
|
|
- }
|
|
|
-
|
|
|
- if (urlState.scope === SEARCH_SCOPE.MULTI) {
|
|
|
- base.scope = "multi";
|
|
|
- base.branches = urlState.branches;
|
|
|
- return base;
|
|
|
- }
|
|
|
-
|
|
|
- // Single (explicit branch param for shareability)
|
|
|
- base.branch = branch;
|
|
|
- return base;
|
|
|
- }
|
|
|
-
|
|
|
- async function runFirstPage() {
|
|
|
- // Reset "load more" UI whenever we start a new first-page search.
|
|
|
- setIsLoadingMore(false);
|
|
|
- setLoadMoreError(null);
|
|
|
-
|
|
|
- const q = urlState.q;
|
|
|
-
|
|
|
- // No search yet.
|
|
|
- if (!q) {
|
|
|
- setStatusState("idle");
|
|
|
- setItems([]);
|
|
|
- setNextCursor(null);
|
|
|
- setError(null);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Local validation: multi scope requires at least one branch.
|
|
|
- if (isAdminDev && urlState.scope === SEARCH_SCOPE.MULTI) {
|
|
|
- const branches = Array.isArray(urlState.branches)
|
|
|
- ? urlState.branches
|
|
|
- : [];
|
|
|
- if (branches.length === 0) {
|
|
|
- setStatusState("error");
|
|
|
- setItems([]);
|
|
|
- setNextCursor(null);
|
|
|
- setError(
|
|
|
- new ApiClientError({
|
|
|
- status: 400,
|
|
|
- code: "VALIDATION_SEARCH_BRANCHES",
|
|
|
- message: "Invalid branches",
|
|
|
- })
|
|
|
- );
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const req = buildSearchRequest({ cursor: null });
|
|
|
- if (!req) return;
|
|
|
-
|
|
|
- const id = ++searchRequestIdRef.current;
|
|
|
-
|
|
|
- setStatusState("loading");
|
|
|
- setItems([]);
|
|
|
- setNextCursor(null);
|
|
|
- setError(null);
|
|
|
-
|
|
|
- try {
|
|
|
- const res = await search(req);
|
|
|
-
|
|
|
- if (id !== searchRequestIdRef.current) return;
|
|
|
-
|
|
|
- const nextItems = Array.isArray(res?.items) ? res.items : [];
|
|
|
- const next = typeof res?.nextCursor === "string" ? res.nextCursor : null;
|
|
|
-
|
|
|
- setItems(nextItems);
|
|
|
- setNextCursor(next);
|
|
|
- setStatusState("success");
|
|
|
- } catch (err) {
|
|
|
- if (id !== searchRequestIdRef.current) return;
|
|
|
-
|
|
|
- setItems([]);
|
|
|
- setNextCursor(null);
|
|
|
- setError(err);
|
|
|
- setStatusState("error");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function loadMore() {
|
|
|
- if (!nextCursor) return;
|
|
|
- if (isLoadingMore) return;
|
|
|
-
|
|
|
- const baseKey = searchKeyRef.current;
|
|
|
-
|
|
|
- const req = buildSearchRequest({ cursor: nextCursor });
|
|
|
- if (!req) return;
|
|
|
+ const nextState = {
|
|
|
+ ...urlState,
|
|
|
+ scope: nextScope,
|
|
|
+ branch: routeBranch,
|
|
|
+ branches: nextScope === SEARCH_SCOPE.MULTI ? urlState.branches : [],
|
|
|
+ };
|
|
|
|
|
|
- const id = ++loadMoreRequestIdRef.current;
|
|
|
+ replaceStateToUrl(nextState);
|
|
|
+ },
|
|
|
+ [isAdminDev, urlState, routeBranch, replaceStateToUrl]
|
|
|
+ );
|
|
|
|
|
|
- setIsLoadingMore(true);
|
|
|
- setLoadMoreError(null);
|
|
|
+ const handleToggleBranch = React.useCallback(
|
|
|
+ (branchId) => {
|
|
|
+ if (!isAdminDev) return;
|
|
|
|
|
|
- try {
|
|
|
- const res = await search(req);
|
|
|
+ const current = Array.isArray(urlState.branches) ? urlState.branches : [];
|
|
|
+ const set = new Set(current);
|
|
|
|
|
|
- // If a newer "load more" started, ignore this result.
|
|
|
- if (id !== loadMoreRequestIdRef.current) return;
|
|
|
+ if (set.has(branchId)) set.delete(branchId);
|
|
|
+ else set.add(branchId);
|
|
|
|
|
|
- // If the base search changed, do not append.
|
|
|
- if (searchKeyRef.current !== baseKey) return;
|
|
|
+ const nextState = {
|
|
|
+ ...urlState,
|
|
|
+ scope: SEARCH_SCOPE.MULTI,
|
|
|
+ branches: Array.from(set),
|
|
|
+ };
|
|
|
|
|
|
- const moreItems = Array.isArray(res?.items) ? res.items : [];
|
|
|
- const next = typeof res?.nextCursor === "string" ? res.nextCursor : null;
|
|
|
+ replaceStateToUrl(nextState);
|
|
|
+ },
|
|
|
+ [isAdminDev, urlState, replaceStateToUrl]
|
|
|
+ );
|
|
|
|
|
|
- setItems((prev) => [...prev, ...moreItems]);
|
|
|
- setNextCursor(next);
|
|
|
- } catch (err) {
|
|
|
- if (id !== loadMoreRequestIdRef.current) return;
|
|
|
- setLoadMoreError(err);
|
|
|
- } finally {
|
|
|
- if (id === loadMoreRequestIdRef.current) {
|
|
|
- setIsLoadingMore(false);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ const handleLimitChange = React.useCallback(
|
|
|
+ (nextLimit) => {
|
|
|
+ const nextState = {
|
|
|
+ ...urlState,
|
|
|
+ limit: nextLimit,
|
|
|
+ branch: routeBranch,
|
|
|
+ };
|
|
|
|
|
|
- // Run first-page search whenever the URL-driven search identity changes.
|
|
|
- React.useEffect(() => {
|
|
|
- runFirstPage();
|
|
|
- // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
- }, [searchKey, isAdminDev, isBranchUser, branch]);
|
|
|
+ // Like scope changes: rerun based on URL state (executed query), not on draft.
|
|
|
+ replaceStateToUrl(nextState);
|
|
|
+ },
|
|
|
+ [urlState, routeBranch, replaceStateToUrl]
|
|
|
+ );
|
|
|
|
|
|
- // Forbidden (keep consistent with Explorer UX).
|
|
|
+ // Forbidden stays consistent with Explorer UX.
|
|
|
if (mappedError?.kind === "forbidden") {
|
|
|
- return <ForbiddenView attemptedBranch={branch} />;
|
|
|
+ return <ForbiddenView attemptedBranch={routeBranch} />;
|
|
|
}
|
|
|
|
|
|
const actions = (
|
|
|
<Button
|
|
|
variant="outline"
|
|
|
size="sm"
|
|
|
- onClick={() => runFirstPage()}
|
|
|
- disabled={!urlState.q || statusState === "loading"}
|
|
|
+ onClick={query.retry}
|
|
|
+ disabled={!urlState.q || query.status === "loading"}
|
|
|
title="Aktualisieren"
|
|
|
>
|
|
|
<RefreshCw className="h-4 w-4" />
|
|
|
@@ -383,13 +206,13 @@ export default function SearchPage({ branch }) {
|
|
|
) : null;
|
|
|
|
|
|
const resultsDescription = urlState.q
|
|
|
- ? `Niederlassung ${branch}`
|
|
|
+ ? `Niederlassung ${routeBranch}`
|
|
|
: "Geben Sie einen Suchbegriff ein, um zu starten.";
|
|
|
|
|
|
return (
|
|
|
<ExplorerPageShell
|
|
|
title="Suche"
|
|
|
- description={`Lieferscheine durchsuchen • Niederlassung ${branch}`}
|
|
|
+ description={`Lieferscheine durchsuchen • Niederlassung ${routeBranch}`}
|
|
|
actions={actions}
|
|
|
>
|
|
|
<ExplorerSectionCard
|
|
|
@@ -397,24 +220,26 @@ export default function SearchPage({ branch }) {
|
|
|
description="Suchbegriff und Suchbereich auswählen."
|
|
|
>
|
|
|
<SearchForm
|
|
|
- branch={branch}
|
|
|
+ branch={routeBranch}
|
|
|
qDraft={qDraft}
|
|
|
onQDraftChange={setQDraft}
|
|
|
onSubmit={handleSubmit}
|
|
|
currentQuery={urlState.q}
|
|
|
- isSubmitting={statusState === "loading"}
|
|
|
+ isSubmitting={query.status === "loading"}
|
|
|
isAdminDev={isAdminDev}
|
|
|
scope={urlState.scope}
|
|
|
onScopeChange={handleScopeChange}
|
|
|
availableBranches={
|
|
|
- branchList.status === BRANCH_LIST_STATE.READY &&
|
|
|
- Array.isArray(branchList.branches)
|
|
|
- ? branchList.branches
|
|
|
+ branchesQuery.status === BRANCH_LIST_STATE.READY &&
|
|
|
+ Array.isArray(branchesQuery.branches)
|
|
|
+ ? branchesQuery.branches
|
|
|
: []
|
|
|
}
|
|
|
- branchesStatus={branchList.status}
|
|
|
+ branchesStatus={branchesQuery.status}
|
|
|
selectedBranches={urlState.branches}
|
|
|
- onToggleBranch={toggleMultiBranch}
|
|
|
+ onToggleBranch={handleToggleBranch}
|
|
|
+ limit={urlState.limit}
|
|
|
+ onLimitChange={handleLimitChange}
|
|
|
/>
|
|
|
</ExplorerSectionCard>
|
|
|
|
|
|
@@ -424,15 +249,16 @@ export default function SearchPage({ branch }) {
|
|
|
headerRight={resultsHeaderRight}
|
|
|
>
|
|
|
<SearchResults
|
|
|
- branch={branch}
|
|
|
+ branch={routeBranch}
|
|
|
scope={urlState.scope}
|
|
|
- status={statusState}
|
|
|
- items={items}
|
|
|
+ status={query.status}
|
|
|
+ items={query.items}
|
|
|
+ total={query.total}
|
|
|
error={mappedError}
|
|
|
- onRetry={runFirstPage}
|
|
|
- nextCursor={nextCursor}
|
|
|
- onLoadMore={loadMore}
|
|
|
- isLoadingMore={isLoadingMore}
|
|
|
+ onRetry={query.retry}
|
|
|
+ nextCursor={query.nextCursor}
|
|
|
+ onLoadMore={query.loadMore}
|
|
|
+ isLoadingMore={query.isLoadingMore}
|
|
|
loadMoreError={mappedLoadMoreError}
|
|
|
/>
|
|
|
</ExplorerSectionCard>
|