Use gql for "mangas" III - global search

This commit is contained in:
schroda
2023-09-08 00:06:24 +02:00
parent af9d49e5e9
commit 0a73496cba
5 changed files with 138 additions and 27 deletions

View File

@@ -7,10 +7,10 @@
*/
import { useTranslation } from 'react-i18next';
import { IMangaCard } from '@/typings';
import MangaGrid, { IMangaGridProps } from '@/components/MangaGrid';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
function filterManga(mangas: IMangaCard[]): IMangaCard[] {
function filterManga(mangas: MangaType[]): MangaType[] {
return mangas;
}

View File

@@ -7,16 +7,16 @@
*/
import gql from 'graphql-tag';
import { FULL_MANGA_FIELDS, FULL_SOURCE_FIELDS } from '@/lib/graphql/Fragments';
import { BASE_MANGA_FIELDS, FULL_SOURCE_FIELDS } from '@/lib/graphql/Fragments';
export const GET_SOURCE_MANGAS_FETCH = gql`
${FULL_MANGA_FIELDS}
${BASE_MANGA_FIELDS}
mutation GET_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) {
fetchSourceManga(input: $input) {
clientMutationId
hasNextPage
mangas {
...FULL_MANGA_FIELDS
...BASE_MANGA_FIELDS
}
}
}

View File

@@ -21,6 +21,7 @@ import {
useQuery,
} from '@apollo/client';
import { OperationVariables } from '@apollo/client/core';
import { useState } from 'react';
import {
BackupValidationResult,
ICategory,
@@ -58,6 +59,9 @@ import {
EnqueueChapterDownloadMutationVariables,
EnqueueChapterDownloadsMutation,
EnqueueChapterDownloadsMutationVariables,
FetchSourceMangaInput,
FetchSourceMangaType,
FilterChangeInput,
GetAboutQuery,
GetAboutQueryVariables,
GetExtensionsFetchMutation,
@@ -74,6 +78,8 @@ import {
GetMangaFetchMutationVariables,
GetMangaQuery,
GetMangaQueryVariables,
GetSourceMangasFetchMutation,
GetSourceMangasFetchMutationVariables,
GetSourceQuery,
GetSourceQueryVariables,
GetSourcesQuery,
@@ -214,6 +220,13 @@ type AbortableApolloUseMutationResponse<Data = any, Variables extends OperationV
MutationTuple<Data, Variables>[0],
MutationTuple<Data, Variables>[1] & AbortableRequest,
];
type AbortableApolloUseMutationPaginatedResponse<
Data = any,
Variables extends OperationVariables = OperationVariables,
> = [
(page: number) => Promise<FetchResult<Data>>,
(MutationTuple<Data, Variables>[1] & AbortableRequest & { size: number; loadingMore: boolean })[],
];
type AbortableApolloMutationResponse<Data = any> = { response: Promise<FetchResult<Data>> } & AbortableRequest;
const isLoadingMore = (swrResult: SWRInfiniteResponse): boolean => {
@@ -625,6 +638,99 @@ export class RequestManager {
return this.doRequestNew(GQLMethod.USE_QUERY, GET_SOURCE, { id }, options);
}
public useGetSourceMangas(
input: FetchSourceMangaInput,
options?: MutationHookOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
): AbortableApolloUseMutationPaginatedResponse<
GetSourceMangasFetchMutation,
GetSourceMangasFetchMutationVariables
> {
const createPaginatedResult = (
result: AbortableApolloUseMutationResponse[1],
page: number,
): AbortableApolloUseMutationPaginatedResponse[1][number] => {
const loading = result.loading || !result.called;
return {
...result,
loading,
size: page,
loadingMore: loading && page > 1,
};
};
// TODO - implement caching
// - ? global cache with revalidating (same as SWR does, revalidate each page starting with 1st until the first page is reached whose data didn't change)
// - ? saving fetched mangas in location state and only "cache" when navigating prev/next
const [mutate, result] = this.doRequestNew<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>(
GQLMethod.USE_MUTATION,
GET_SOURCE_MANGAS_FETCH,
{ input },
options,
);
const [previousResults, setPreviousResults] = useState<AbortableApolloUseMutationPaginatedResponse[1]>([
createPaginatedResult(result, input.page),
]);
const [contentType, setContentType] = useState(input.type);
const [query, setQuery] = useState(input.query);
const [page, setPage] = useState(input.page);
const paginatedResult = createPaginatedResult(result, page);
// TODO - option "global cache with revalidating"
// replace previousResults with cache
// cache specific response
// cache "base" key to specific page keys to be able to retrieve all necessary cached pages
// get cached results
// revalidate in background - revalidate first page -> result changed? revalidate every page until cached result and response is the same
// wrap "mutate" function to align with the expected type, which allows only passing a "page" argument
const wrappedMutate = (newPage: number) => {
const resetPreviousResultForInitialLoad = newPage < page;
if (resetPreviousResultForInitialLoad) {
setPreviousResults(previousResults.filter((prevResult) => prevResult.size <= newPage));
}
if (newPage !== page) {
setPage(newPage);
}
return mutate({
variables: {
input: {
...input,
page: newPage,
},
},
});
};
const contentTypeChanged = contentType !== input.type;
const queryChanged = query !== input.query;
// instantly return empty results in case the provided variables changed - wait until the hook returns empty data,
// otherwise, updating the previous results will revert the reset
const resetPreviousResult = (queryChanged || contentTypeChanged) && !paginatedResult.data;
let updatedResults = [
...(resetPreviousResult ? [{ ...paginatedResult, size: page, loadingMore: false }] : previousResults),
];
if (resetPreviousResult) {
setContentType(input.type);
setQuery(input.query);
setPreviousResults([paginatedResult]);
}
const resultChanged = previousResults[page - 1]?.loading !== paginatedResult.loading;
const updatePreviousResult = resultChanged && !resetPreviousResult;
if (updatePreviousResult) {
updatedResults = [...previousResults.slice(0, page - 1), paginatedResult];
setPreviousResults(updatedResults);
}
return [wrappedMutate, updatedResult];
}
public useGetSourcePopularMangas(
sourceId: string,
initialPages?: number,
@@ -683,21 +789,15 @@ export class RequestManager {
}
public useSourceSearch(
sourceId: string,
searchTerm: string,
initialPages?: number,
swrOptions?: SWRInfiniteOptions<SourceSearchResult>,
): AbortableSWRInfiniteResponse<SourceSearchResult> {
return this.doRequest(HttpMethod.SWR_GET_INFINITE, '', {
swrOptions: {
getEndpoint: (page, previousData) =>
previousData?.hasNextPage ?? true
? `source/${sourceId}/search?searchTerm=${searchTerm}&pageNum=${page + 1}`
: null,
initialSize: initialPages,
...swrOptions,
} as typeof swrOptions,
});
source: string,
query: string,
filters?: FilterChangeInput[],
options?: MutationHookOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
): AbortableApolloUseMutationPaginatedResponse<
GetSourceMangasFetchMutation,
GetSourceMangasFetchMutationVariables
> {
return this.useGetSourceMangas({ type: FetchSourceMangaType.Search, source, query, filters, page: 1 }, options);
}
public useSourceQuickSearch(

View File

@@ -21,6 +21,7 @@ import LangSelect from '@/components/navbar/action/LangSelect';
import MangaGrid from '@/components/MangaGrid';
import NavbarContext from '@/components/context/NavbarContext';
import { useDebounce } from '@/components/manga/hooks';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
@@ -99,15 +100,17 @@ const SourceSearchPreview = React.memo(
const skipRequest = !searchString;
const { id, displayName, lang } = source;
const {
data: searchResult,
isLoading,
error,
abortRequest,
} = requestManager.useSourceQuickSearch(id, searchString ?? '', [], 1, { skipRequest });
const mangas = !isLoading ? searchResult?.[0]?.mangaList ?? [] : [];
const [loadPage, results] = requestManager.useSourceSearch(id, searchString ?? '', []);
const { data: searchResult, loading: isLoading, error, abortRequest } = results[0]!;
const mangas = (searchResult?.fetchSourceManga.mangas as MangaType[]) ?? [];
const noMangasFound = !isLoading && !mangas.length;
useEffect(() => {
if (!skipRequest) {
loadPage(1);
}
}, [skipRequest, searchString]);
useEffect(() => {
onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
}, [isLoading, noMangasFound, searchString]);

View File

@@ -12,6 +12,14 @@ import { ParseKeys } from 'i18next';
import { Location } from 'react-router-dom';
import { ExtensionType, MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts';
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object | undefined
? RecursivePartial<T[P]>
: T[P];
};
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
declare module 'react-router-dom' {