Feature/improve add to library category selection dialog (#631)

* Create "Categories" util class

* Only show add to library category selection in case categories exist

* Add edit categories button in select categories dialog

* Add option to disable add to library dialog to the dialog
This commit is contained in:
schroda
2024-03-02 15:23:12 +01:00
committed by GitHub
parent 92fe3eaaa6
commit eb233aef3d
4 changed files with 102 additions and 31 deletions

View File

@@ -21,6 +21,8 @@ import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx'; import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
import { Mangas } from '@/lib/data/Mangas.ts'; import { Mangas } from '@/lib/data/Mangas.ts';
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx'; import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { Categories } from '@/lib/data/Categories.ts';
const DetailsWrapper = styled('div')(({ theme }) => ({ const DetailsWrapper = styled('div')(({ theme }) => ({
width: '100%', width: '100%',
@@ -177,6 +179,12 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
loading: areSettingsLoading, loading: areSettingsLoading,
} = useMetadataServerSettings(); } = useMetadataServerSettings();
const categories = requestManager.useGetCategories();
const userCreatedCategories = useMemo(
() => Categories.getUserCreated(categories.data?.categories.nodes ?? []),
[categories.data?.categories.nodes],
);
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false); const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
useEffect(() => { useEffect(() => {
@@ -195,7 +203,21 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
}; };
const handleAddToLibraryClick = () => { const handleAddToLibraryClick = () => {
if (!showAddToLibraryCategorySelectDialog) { if (categories.loading) {
makeToast(t('global.label.load_in_progress'), 'info');
return;
}
if (categories.error) {
makeToast(t('category.error.label.request_failure'), 'error');
categories
.refetch()
.catch(defaultPromiseErrorHandler('MangaDetails::handleAddToLibraryClick: refetch categories'));
return;
}
const showCategorySelectDialog = showAddToLibraryCategorySelectDialog && !!userCreatedCategories.length;
if (!showCategorySelectDialog) {
addToLibrary(); addToLibrary();
return; return;
} }
@@ -236,7 +258,7 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
<MangaButtonsContainer inLibrary={manga.inLibrary}> <MangaButtonsContainer inLibrary={manga.inLibrary}>
<div> <div>
<Button <Button
disabled={areSettingsLoading} disabled={areSettingsLoading || categories.loading}
startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />} startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick} onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick}
size="large" size="large"

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
@@ -14,11 +14,17 @@ import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import FormGroup from '@mui/material/FormGroup'; import FormGroup from '@mui/material/FormGroup';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Stack } from '@mui/material';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Mangas } from '@/lib/data/Mangas.ts'; import { Mangas } from '@/lib/data/Mangas.ts';
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts'; import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx'; import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
import { TCategory } from '@/typings.ts'; import { Categories } from '@/lib/data/Categories.ts';
import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata.ts';
import { makeToast } from '@/components/util/Toast.tsx';
type BaseProps = { type BaseProps = {
open: boolean; open: boolean;
@@ -46,7 +52,7 @@ const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => {
return []; return [];
} }
return mangaResult.manga.categories.nodes.map((category) => category.id); return Categories.getIds(mangaResult.manga.categories.nodes);
}, [mangaResult?.manga.categories.nodes, mangaId]); }, [mangaResult?.manga.categories.nodes, mangaId]);
}; };
@@ -71,9 +77,6 @@ const getCategoryCheckedState = (
return undefined; return undefined;
}; };
const getDefaultCategoryIds = (categories: TCategory[]) =>
categories.filter(({ default: isDefault }) => isDefault).map(({ id }) => id);
export function CategorySelect(props: Props) { export function CategorySelect(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -82,20 +85,17 @@ export function CategorySelect(props: Props) {
const isSingleSelectionMode = mangaId !== undefined; const isSingleSelectionMode = mangaId !== undefined;
const mangaIds = passedMangaIds ?? [mangaId]; const mangaIds = passedMangaIds ?? [mangaId];
const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false);
const { metadata: serverMetadata } = useMetadataServerSettings();
const mangaCategoryIds = useGetMangaCategoryIds(mangaId); const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
const { data } = requestManager.useGetCategories(); const { data } = requestManager.useGetCategories();
const categoriesData = data?.categories.nodes; const categoriesData = data?.categories.nodes;
const allCategories = useMemo(() => { const allCategories = useMemo(() => Categories.getUserCreated(categoriesData ?? []), [categoriesData]);
const cats = [...(categoriesData ?? [])]; // make copy
if (cats.length > 0 && cats[0].name === 'Default') {
cats.shift(); // remove first category if it is 'Default'
}
return cats;
}, [categoriesData]);
const defaultCategoryIds = useMemo( const defaultCategoryIds = useMemo(
() => (addToLibrary ? getDefaultCategoryIds(allCategories) : []), () => (addToLibrary ? Categories.getIds(Categories.getDefaults(allCategories)) : []),
[allCategories], [allCategories],
); );
@@ -134,6 +134,12 @@ export function CategorySelect(props: Props) {
? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId)) ? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId))
: categoriesToRemove; : categoriesToRemove;
if (doNotShowAddToLibraryDialogAgain) {
requestUpdateServerMetadata(convertToGqlMeta(serverMetadata)! ?? {}, [
['showAddToLibraryCategorySelectDialog', false],
]).catch(() => makeToast(t('search.error.label.failed_to_save_settings'), 'error'));
}
const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length; const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length;
if (!isUpdateRequired) { if (!isUpdateRequired) {
return; return;
@@ -161,13 +167,7 @@ export function CategorySelect(props: Props) {
<DialogTitle>{t('category.title.set_categories')}</DialogTitle> <DialogTitle>{t('category.title.set_categories')}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<FormGroup> <FormGroup>
{allCategories.length === 0 && ( {allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
<span>
{t('category.error.no_categories_found.label.info')}
<br />
{t('category.error.no_categories_found.label.hint')}
</span>
)}
{allCategories.map((category) => ( {allCategories.map((category) => (
<ThreeStateCheckboxInput <ThreeStateCheckboxInput
checked={getCategoryCheckedState( checked={getCategoryCheckedState(
@@ -195,12 +195,31 @@ export function CategorySelect(props: Props) {
</FormGroup> </FormGroup>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus onClick={handleCancel} color="primary"> <Stack sx={{ width: '100%' }}>
{t('global.button.cancel')} {addToLibrary && (
</Button> <CheckboxInput
<Button onClick={handleOk} color="primary"> sx={{ margin: 0 }}
{t('global.button.ok')} size="small"
</Button> label={t('global.button.dont_show_dialog_again')}
onChange={(e) => setDoNotShowAddToLibraryDialogAgain(e.target.checked)}
/>
)}
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between" alignItems="end">
<Button component={Link} to="/settings/categories">
{t(allCategories.length ? 'global.button.edit' : 'global.button.create')}
</Button>
<Stack direction="row">
<Button autoFocus onClick={handleCancel} color="primary">
{t('global.button.cancel')}
</Button>
{!!allCategories.length && (
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
</Button>
)}
</Stack>
</Stack>
</Stack>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );

View File

@@ -13,8 +13,7 @@
}, },
"no_categories_found": { "no_categories_found": {
"label": { "label": {
"hint": "You should make some from the settings.", "info": "You don't have any categories yet."
"info": "No categories found."
} }
} }
}, },
@@ -296,7 +295,9 @@
"cancel": "Cancel", "cancel": "Cancel",
"clear": "Clear", "clear": "Clear",
"copy": "Copy", "copy": "Copy",
"create": "Create",
"deselect": "Deselect", "deselect": "Deselect",
"dont_show_dialog_again": "Don't show this dialog again",
"edit": "Edit", "edit": "Edit",
"filter": "Filter", "filter": "Filter",
"latest": "Latest", "latest": "Latest",
@@ -370,6 +371,7 @@
"general": "General", "general": "General",
"github": "GitHub", "github": "GitHub",
"links": "Links", "links": "Links",
"load_in_progress": "Still loading required data…",
"loading": "Loading…", "loading": "Loading…",
"never": "Never", "never": "Never",
"none": "None", "none": "None",

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { TCategory } from '@/typings.ts';
export const DEFAULT_CATEGORY_ID = 0;
export type CategoryIdInfo = Pick<TCategory, 'id'>;
export type CategoryDefaultInfo = Pick<TCategory, 'default'>;
export class Categories {
static getIds(categories: CategoryIdInfo[]): number[] {
return categories.map((category) => category.id);
}
static getUserCreated<Category extends CategoryIdInfo>(categories: Category[]): Category[] {
return categories.filter((category) => category.id !== DEFAULT_CATEGORY_ID);
}
static getDefaults<Category extends CategoryDefaultInfo>(categories: Category[]): Category[] {
return categories.filter((category) => category.default);
}
}