diff --git a/src/components/navbar/action/CategorySelect.tsx b/src/components/navbar/action/CategorySelect.tsx index 467e688e..7087fe63 100644 --- a/src/components/navbar/action/CategorySelect.tsx +++ b/src/components/navbar/action/CategorySelect.tsx @@ -30,7 +30,8 @@ export default function CategorySelect(props: IProps) { const { open, setOpen, mangaId } = props; const { data: mangaCategoriesData, mutate } = requestManager.useGetMangaCategories(mangaId); - const { data: categoriesData } = requestManager.useGetCategories(); + const { data } = requestManager.useGetCategories(); + const categoriesData = data?.categories.nodes; const [triggerMutate] = requestManager.useUpdateMangaCategories(); const allCategories = useMemo(() => { diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 85f81298..f6521af4 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -27,7 +27,6 @@ import { IChapter, IManga, IMangaChapter, - IncludeInGlobalUpdate, ISourceFilters, IUpdateStatus, PaginatedList, @@ -39,8 +38,14 @@ import { HttpMethod as DefaultHttpMethod, IRestClient, RestClient } from '@/lib/ import storage from '@/util/localStorage.tsx'; import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts'; import { + CategoryOrderBy, CheckForServerUpdatesQuery, CheckForServerUpdatesQueryVariables, + CreateCategoryInput, + CreateCategoryMutation, + CreateCategoryMutationVariables, + DeleteCategoryMutation, + DeleteCategoryMutationVariables, DeleteDownloadedChapterMutation, DeleteDownloadedChapterMutationVariables, DeleteDownloadedChaptersMutation, @@ -49,6 +54,8 @@ import { GetAboutQueryVariables, GetExtensionsFetchMutation, GetExtensionsFetchMutationVariables, + GetCategoriesQuery, + GetCategoriesQueryVariables, GetExtensionsQuery, GetExtensionsQueryVariables, GetGlobalMetadatasQuery, @@ -59,11 +66,18 @@ import { GetSourcesQueryVariables, InstallExternalExtensionMutation, InstallExternalExtensionMutationVariables, + SetCategoryMetadataMutation, + SetCategoryMetadataMutationVariables, SetChapterMetadataMutation, SetChapterMetadataMutationVariables, SetGlobalMetadataMutation, SetGlobalMetadataMutationVariables, SetMangaMetadataMutation, + UpdateCategoryMutation, + UpdateCategoryMutationVariables, + UpdateCategoryOrderMutation, + UpdateCategoryOrderMutationVariables, + UpdateCategoryPatchInput, UpdateChapterMutation, UpdateChapterMutationVariables, UpdateChapterPatchInput, @@ -95,6 +109,13 @@ import { GET_SOURCE_MANGAS_FETCH } from '@/lib/graphql/mutations/SourceMutation. import { DELETE_DOWNLOADED_CHAPTER, DELETE_DOWNLOADED_CHAPTERS } from '@/lib/graphql/mutations/DownloaderMutation.ts'; import { GET_CHAPTER, GET_CHAPTERS } from '@/lib/graphql/queries/ChapterQuery.ts'; import { SET_CHAPTER_METADATA, UPDATE_CHAPTER, UPDATE_CHAPTERS } from '@/lib/graphql/mutations/ChapterMutation.ts'; +import { + CREATE_CATEGORY, + DELETE_CATEGORY, + SET_CATEGORY_METADATA, + UPDATE_CATEGORY, + UPDATE_CATEGORY_ORDER, +} from '@/lib/graphql/mutations/CategoryMutation.ts'; enum SWRHttpMethod { SWR_GET, @@ -822,37 +843,66 @@ export class RequestManager { ); } - public useGetCategories(swrOptions?: SWROptions): AbortableSWRResponse { - return this.doRequest(HttpMethod.SWR_GET, `category`, { swrOptions }); - } - - public createCategory(name: string): AbortableAxiosResponse { - return this.doRequest(HttpMethod.POST, `category`, { formData: { name } }); - } - - public reorderCategory(currentPosition: number, newPosition: number): AbortableAxiosResponse { - return this.doRequest(HttpMethod.PATCH, `category/reorder`, { - formData: { from: currentPosition, to: newPosition }, + public useGetCategories(): AbortableApolloUseQueryResponse { + return this.doRequestNew(GQLMethod.USE_QUERY, GET_CATEGORIES, { + orderBy: CategoryOrderBy.Order, }); } + public createCategory(input: CreateCategoryInput): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + CREATE_CATEGORY, + { input }, + { refetchQueries: [GET_CATEGORIES] }, + ); + } + + public reorderCategory(id: number, position: number): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + UPDATE_CATEGORY_ORDER, + { input: { id, position } }, + { refetchQueries: [GET_CATEGORIES] }, + ); + } + public useGetCategoryMangas(categoryId: number, swrOptions?: SWROptions): AbortableSWRResponse { return this.doRequest(HttpMethod.SWR_GET, `category/${categoryId}`, { swrOptions }); } - public deleteCategory(categoryId: number): AbortableAxiosResponse { - return this.doRequest(HttpMethod.DELETE, `category/${categoryId}`); + public deleteCategory(categoryId: number): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + DELETE_CATEGORY, + { input: { categoryId } }, + { refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORIES] }, + ); } public updateCategory( - categoryId: number, - change: { name?: string; default?: boolean; includeInUpdate?: IncludeInGlobalUpdate } = {}, - ): AbortableAxiosResponse { - return this.doRequest(HttpMethod.PATCH, `category/${categoryId}`, { formData: change }); + id: number, + patch: UpdateCategoryPatchInput, + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + UPDATE_CATEGORY, + { input: { id, patch } }, + { refetchQueries: [GET_CATEGORY, GET_CATEGORIES] }, + ); } - public setCategoryMeta(categoryId: number, key: string, value: any): AbortableAxiosResponse { - return this.doRequest(HttpMethod.PATCH, `category/${categoryId}/meta`, { formData: { key, value } }); + public setCategoryMeta( + categoryId: number, + key: string, + value: any, + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + SET_CATEGORY_METADATA, + { input: { meta: { categoryId, key, value: `${value}` } } }, + { refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY, GET_CATEGORIES] }, + ); } public restoreBackupFile(file: File): AbortableAxiosResponse { diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index b6722da2..ecaf4279 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -63,9 +63,13 @@ export default function Library() { const { options } = useLibraryOptionsContext(); const [lastLibraryUpdate, setLastLibraryUpdate] = useState(Date.now()); - const { data: tabsData, error: tabsError, isLoading: areCategoriesLoading } = requestManager.useGetCategories(); + const { data, error: tabsError, loading: areCategoriesLoading } = requestManager.useGetCategories(); + const tabsData = data?.categories.nodes.filter((category) => category.id !== 0); const tabs = tabsData ?? []; - const librarySize = useMemo(() => tabs.map((tab) => tab.size).reduce((prev, curr) => prev + curr, 0), [tabs]); + const librarySize = useMemo( + () => tabs.map((tab) => tab.mangas.totalCount).reduce((prev, curr) => prev + curr, 0), + [tabs], + ); const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam); @@ -108,7 +112,7 @@ export default function Library() { return ( ); } @@ -154,7 +158,7 @@ export default function Library() { label={ {tab.name} - {options.showTabSize ? : null} + {options.showTabSize ? : null} } value={tab.order} diff --git a/src/screens/settings/Categories.tsx b/src/screens/settings/Categories.tsx index 938bfec3..001e4691 100644 --- a/src/screens/settings/Categories.tsx +++ b/src/screens/settings/Categories.tsx @@ -24,11 +24,11 @@ import DialogTitle from '@mui/material/DialogTitle'; import Checkbox from '@mui/material/Checkbox'; import FormControlLabel from '@mui/material/FormControlLabel'; import { useTranslation } from 'react-i18next'; -import { ICategory } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; import StrictModeDroppable from '@/lib/StrictModeDroppable'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext'; +import { CategoryType } from '@/lib/graphql/generated/graphql.ts'; const getItemStyle = ( isDragging: boolean, @@ -52,13 +52,13 @@ export default function Categories() { setAction(null); }, [t]); - const { data, mutate } = requestManager.useGetCategories(); + const { data } = requestManager.useGetCategories(); const categories = useMemo(() => { - const res = [...(data ?? [])]; + const res = [...(data?.categories.nodes ?? [])]; if (res.length > 0 && res[0].name === 'Default') { res.shift(); } - return res; + return res as CategoryType[]; }, [data]); const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category @@ -69,13 +69,15 @@ export default function Categories() { useSetDefaultBackTo('settings'); - const categoryReorder = (list: ICategory[], from: number, to: number) => { + const categoryReorder = (list: CategoryType[], from: number, to: number) => { + const reorderedCategory = list[from]; const newData = [...list]; const [removed] = newData.splice(from, 1); newData.splice(to, 0, removed); - mutate(newData, { revalidate: false }); + // TODO - update cache immediately + // mutate(categoriesEndpoint, newData, { revalidate: false }); - requestManager.reorderCategory(from + 1, to + 1).response.finally(() => mutate()); + requestManager.reorderCategory(reorderedCategory.id, to + 1); }; const onDragEnd = (result: DropResult) => { @@ -113,18 +115,16 @@ export default function Categories() { setDialogOpen(false); if (categoryToEdit === -1) { - requestManager.createCategory(dialogName).response.finally(() => mutate()); + requestManager.createCategory({ name: dialogName, default: dialogDefault }); } else { const category = categories[categoryToEdit]; - requestManager - .updateCategory(category.id, { name: dialogName, default: dialogDefault }) - .response.finally(() => mutate()); + requestManager.updateCategory(category.id, { name: dialogName, default: dialogDefault }); } }; const deleteCategory = (index: number) => { const category = categories[index]; - requestManager.deleteCategory(category.id).response.finally(() => mutate()); + requestManager.deleteCategory(category.id); }; return ( diff --git a/src/screens/settings/LibrarySettings.tsx b/src/screens/settings/LibrarySettings.tsx index e690ced7..fa601885 100644 --- a/src/screens/settings/LibrarySettings.tsx +++ b/src/screens/settings/LibrarySettings.tsx @@ -20,12 +20,12 @@ import DialogTitle from '@mui/material/DialogTitle'; import { styled } from '@mui/material'; import { t as translate } from 'i18next'; import { useTranslation } from 'react-i18next'; -import { ICategory, IncludeInGlobalUpdate } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; import makeToast from '@/components/util/Toast'; import ThreeStateCheckboxInput from '@/components/atoms/ThreeStateCheckboxInput'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext'; import SearchSettings from '@/screens/settings/SearchSettings'; +import { CategoryType, IncludeInUpdate } from '@/lib/graphql/generated/graphql.ts'; const CategoriesDiv = styled('div')({ display: 'flex', @@ -34,16 +34,35 @@ const CategoriesDiv = styled('div')({ overflow: 'auto', }); -const includeInUpdateStatusToBoolean = (status: IncludeInGlobalUpdate) => { - if (status === IncludeInGlobalUpdate.UNSET) { - return null; +const booleanToIncludeInStatus = (status: boolean | null | undefined): IncludeInUpdate => { + switch (status) { + case false: + return IncludeInUpdate.Exclude; + case true: + return IncludeInUpdate.Include; + case null: + case undefined: + return IncludeInUpdate.Unset; + default: + throw new Error(`booleanToIncludeInStatus: unexpected IncludeInUpdate status "${status}"`); } +}; - return !!status; +const includeInUpdateStatusToBoolean = (status: IncludeInUpdate): boolean | null => { + switch (status) { + case IncludeInUpdate.Exclude: + return false; + case IncludeInUpdate.Include: + return true; + case IncludeInUpdate.Unset: + return null; + default: + throw new Error(`includeInUpdateStatusToBoolean: unexpected IncludeInUpdate status "${status}"`); + } }; const getCategoryUpdateInfo = ( - categories: ICategory[], + categories: CategoryType[], areIncluded: boolean, unsetCategories: number, allCategories: number, @@ -80,20 +99,21 @@ export default function LibrarySettings() { useSetDefaultBackTo('settings'); - const { data: categories = [], error: requestError, mutate } = requestManager.useGetCategories(); - const [dialogCategories, setDialogCategories] = useState(categories); + const { data, error: requestError } = requestManager.useGetCategories(); + const categories = (data?.categories.nodes ?? []) as CategoryType[]; + const [dialogCategories, setDialogCategories] = useState(categories); const [isDialogOpen, setIsDialogOpen] = useState(false); useEffect(() => { setDialogCategories(categories); }, [categories]); - const unsetCategories: ICategory[] = - categories?.filter((category) => category.includeInUpdate === IncludeInGlobalUpdate.UNSET) ?? []; - const excludedCategories: ICategory[] = - categories?.filter((category) => category.includeInUpdate === IncludeInGlobalUpdate.EXCLUDE) ?? []; - const includedCategories: ICategory[] = - categories?.filter((category) => category.includeInUpdate === IncludeInGlobalUpdate.INCLUDE) ?? []; + const unsetCategories: CategoryType[] = + categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Unset) ?? []; + const excludedCategories: CategoryType[] = + categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Exclude) ?? []; + const includedCategories: CategoryType[] = + categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Include) ?? []; const excludedCategoriesText = getCategoryUpdateInfo( excludedCategories, false, @@ -109,7 +129,7 @@ export default function LibrarySettings() { requestError, ); - const updateCategory = (category: ICategory) => + const updateCategory = (category: CategoryType) => requestManager.updateCategory(category.id, { includeInUpdate: category.includeInUpdate }).response; const updateCategories = async () => { @@ -127,10 +147,11 @@ export default function LibrarySettings() { try { await Promise.all(categoriesToUpdate.map((category) => updateCategory(category))); - mutate([...dialogCategories], { revalidate: false }); + // TODO - update cache immediately + // mutate(categoriesEndpoint, [...dialogCategories], { revalidate: false }); } catch (error) { makeToast(t('global.error.label.failed_to_save_changes'), 'error'); - mutate([...categories]); + // mutate(categoriesEndpoint, [...categories]); } }; @@ -192,13 +213,12 @@ export default function LibrarySettings() { label={category.name} checked={includeInUpdateStatusToBoolean(category.includeInUpdate)} onChange={(checked) => { - const newIncludeState: IncludeInGlobalUpdate = - checked == null ? IncludeInGlobalUpdate.UNSET : Number(checked); + const newIncludeState = booleanToIncludeInStatus(checked); const categoryIndex = dialogCategories.findIndex( (category_) => category_ === category, ); - const updatedDialogCategories: ICategory[] = [ + const updatedDialogCategories: CategoryType[] = [ ...dialogCategories.slice(0, categoryIndex), { ...category,