Use gql for "mangas" III - global search
This commit is contained in:
@@ -7,10 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IMangaCard } from '@/typings';
|
|
||||||
import MangaGrid, { IMangaGridProps } from '@/components/MangaGrid';
|
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;
|
return mangas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import gql from 'graphql-tag';
|
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`
|
export const GET_SOURCE_MANGAS_FETCH = gql`
|
||||||
${FULL_MANGA_FIELDS}
|
${BASE_MANGA_FIELDS}
|
||||||
mutation GET_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) {
|
mutation GET_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) {
|
||||||
fetchSourceManga(input: $input) {
|
fetchSourceManga(input: $input) {
|
||||||
clientMutationId
|
clientMutationId
|
||||||
hasNextPage
|
hasNextPage
|
||||||
mangas {
|
mangas {
|
||||||
...FULL_MANGA_FIELDS
|
...BASE_MANGA_FIELDS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
import { OperationVariables } from '@apollo/client/core';
|
import { OperationVariables } from '@apollo/client/core';
|
||||||
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BackupValidationResult,
|
BackupValidationResult,
|
||||||
ICategory,
|
ICategory,
|
||||||
@@ -58,6 +59,9 @@ import {
|
|||||||
EnqueueChapterDownloadMutationVariables,
|
EnqueueChapterDownloadMutationVariables,
|
||||||
EnqueueChapterDownloadsMutation,
|
EnqueueChapterDownloadsMutation,
|
||||||
EnqueueChapterDownloadsMutationVariables,
|
EnqueueChapterDownloadsMutationVariables,
|
||||||
|
FetchSourceMangaInput,
|
||||||
|
FetchSourceMangaType,
|
||||||
|
FilterChangeInput,
|
||||||
GetAboutQuery,
|
GetAboutQuery,
|
||||||
GetAboutQueryVariables,
|
GetAboutQueryVariables,
|
||||||
GetExtensionsFetchMutation,
|
GetExtensionsFetchMutation,
|
||||||
@@ -74,6 +78,8 @@ import {
|
|||||||
GetMangaFetchMutationVariables,
|
GetMangaFetchMutationVariables,
|
||||||
GetMangaQuery,
|
GetMangaQuery,
|
||||||
GetMangaQueryVariables,
|
GetMangaQueryVariables,
|
||||||
|
GetSourceMangasFetchMutation,
|
||||||
|
GetSourceMangasFetchMutationVariables,
|
||||||
GetSourceQuery,
|
GetSourceQuery,
|
||||||
GetSourceQueryVariables,
|
GetSourceQueryVariables,
|
||||||
GetSourcesQuery,
|
GetSourcesQuery,
|
||||||
@@ -214,6 +220,13 @@ type AbortableApolloUseMutationResponse<Data = any, Variables extends OperationV
|
|||||||
MutationTuple<Data, Variables>[0],
|
MutationTuple<Data, Variables>[0],
|
||||||
MutationTuple<Data, Variables>[1] & AbortableRequest,
|
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;
|
type AbortableApolloMutationResponse<Data = any> = { response: Promise<FetchResult<Data>> } & AbortableRequest;
|
||||||
|
|
||||||
const isLoadingMore = (swrResult: SWRInfiniteResponse): boolean => {
|
const isLoadingMore = (swrResult: SWRInfiniteResponse): boolean => {
|
||||||
@@ -625,6 +638,99 @@ export class RequestManager {
|
|||||||
return this.doRequestNew(GQLMethod.USE_QUERY, GET_SOURCE, { id }, options);
|
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(
|
public useGetSourcePopularMangas(
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
initialPages?: number,
|
initialPages?: number,
|
||||||
@@ -683,21 +789,15 @@ export class RequestManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public useSourceSearch(
|
public useSourceSearch(
|
||||||
sourceId: string,
|
source: string,
|
||||||
searchTerm: string,
|
query: string,
|
||||||
initialPages?: number,
|
filters?: FilterChangeInput[],
|
||||||
swrOptions?: SWRInfiniteOptions<SourceSearchResult>,
|
options?: MutationHookOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
|
||||||
): AbortableSWRInfiniteResponse<SourceSearchResult> {
|
): AbortableApolloUseMutationPaginatedResponse<
|
||||||
return this.doRequest(HttpMethod.SWR_GET_INFINITE, '', {
|
GetSourceMangasFetchMutation,
|
||||||
swrOptions: {
|
GetSourceMangasFetchMutationVariables
|
||||||
getEndpoint: (page, previousData) =>
|
> {
|
||||||
previousData?.hasNextPage ?? true
|
return this.useGetSourceMangas({ type: FetchSourceMangaType.Search, source, query, filters, page: 1 }, options);
|
||||||
? `source/${sourceId}/search?searchTerm=${searchTerm}&pageNum=${page + 1}`
|
|
||||||
: null,
|
|
||||||
initialSize: initialPages,
|
|
||||||
...swrOptions,
|
|
||||||
} as typeof swrOptions,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public useSourceQuickSearch(
|
public useSourceQuickSearch(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import LangSelect from '@/components/navbar/action/LangSelect';
|
|||||||
import MangaGrid from '@/components/MangaGrid';
|
import MangaGrid from '@/components/MangaGrid';
|
||||||
import NavbarContext from '@/components/context/NavbarContext';
|
import NavbarContext from '@/components/context/NavbarContext';
|
||||||
import { useDebounce } from '@/components/manga/hooks';
|
import { useDebounce } from '@/components/manga/hooks';
|
||||||
|
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
|
||||||
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
|
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
|
||||||
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
|
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
|
||||||
@@ -99,15 +100,17 @@ const SourceSearchPreview = React.memo(
|
|||||||
const skipRequest = !searchString;
|
const skipRequest = !searchString;
|
||||||
|
|
||||||
const { id, displayName, lang } = source;
|
const { id, displayName, lang } = source;
|
||||||
const {
|
const [loadPage, results] = requestManager.useSourceSearch(id, searchString ?? '', []);
|
||||||
data: searchResult,
|
const { data: searchResult, loading: isLoading, error, abortRequest } = results[0]!;
|
||||||
isLoading,
|
const mangas = (searchResult?.fetchSourceManga.mangas as MangaType[]) ?? [];
|
||||||
error,
|
|
||||||
abortRequest,
|
|
||||||
} = requestManager.useSourceQuickSearch(id, searchString ?? '', [], 1, { skipRequest });
|
|
||||||
const mangas = !isLoading ? searchResult?.[0]?.mangaList ?? [] : [];
|
|
||||||
const noMangasFound = !isLoading && !mangas.length;
|
const noMangasFound = !isLoading && !mangas.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!skipRequest) {
|
||||||
|
loadPage(1);
|
||||||
|
}
|
||||||
|
}, [skipRequest, searchString]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
|
onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
|
||||||
}, [isLoading, noMangasFound, searchString]);
|
}, [isLoading, noMangasFound, searchString]);
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ import { ParseKeys } from 'i18next';
|
|||||||
import { Location } from 'react-router-dom';
|
import { Location } from 'react-router-dom';
|
||||||
import { ExtensionType, MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts';
|
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 };
|
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
|
||||||
|
|
||||||
declare module 'react-router-dom' {
|
declare module 'react-router-dom' {
|
||||||
|
|||||||
Reference in New Issue
Block a user