Extract fetch logic into parent setting components

This commit is contained in:
schroda
2024-04-28 01:00:43 +02:00
parent 58f5e9ff79
commit f1ec97b0e6
9 changed files with 164 additions and 87 deletions

View File

@@ -23,6 +23,14 @@ import { IncludeOrExclude } from '@/lib/graphql/generated/graphql.ts';
import { TCategory } from '@/typings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { CheckboxContainer } from '@/components/settings/globalUpdate/CheckboxContainer.ts';
import {
CategoryDownloadInclusionInfo,
CategoryIdInfo,
CategoryNameInfo,
CategoryUpdateInclusionInfo,
} from '@/lib/data/Categories.ts';
type CategoryType = CategoryIdInfo & CategoryNameInfo & CategoryUpdateInclusionInfo & CategoryDownloadInclusionInfo;
const booleanToIncludeOrExcludeStatus = (status: boolean | null | undefined): IncludeOrExclude => {
switch (status) {
@@ -52,19 +60,11 @@ const includeInUpdateStatusToBoolean = (status: IncludeOrExclude): boolean | nul
};
const getCategoryUpdateInfo = (
categories: TCategory[],
categories: CategoryType[],
areIncluded: boolean,
unsetCategories: number,
allCategories: number,
error: any,
) => {
if (error) {
return translate('global.error.label.failed_to_load_data');
}
if (allCategories === -1) {
return translate('global.label.loading');
}
const noSpecificallyIncludedCategories = areIncluded && !categories.length && unsetCategories;
const includesAllCategories = categories.length === allCategories;
if (noSpecificallyIncludedCategories || includesAllCategories) {
@@ -80,18 +80,20 @@ const getCategoryUpdateInfo = (
type CategoryIncludeField = keyof Pick<TCategory, 'includeInUpdate' | 'includeInDownload'>;
type CategoriesInclusionSettingProps = {
export type CategoriesInclusionSettingProps = {
categories: CategoryType[];
includeField: CategoryIncludeField;
dialogText?: string;
};
export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProps) => {
export const CategoriesInclusionSetting = ({
categories,
includeField,
dialogText,
}: CategoriesInclusionSettingProps) => {
const { t } = useTranslation();
const { includeField, dialogText } = props;
const { data, error: requestError } = requestManager.useGetCategories();
const categories = data?.categories.nodes;
const [dialogCategories, setDialogCategories] = useState<TCategory[]>(categories ?? []);
const [dialogCategories, setDialogCategories] = useState(categories);
const [isDialogOpen, setIsDialogOpen] = useState(false);
useEffect(() => {
@@ -102,28 +104,23 @@ export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProp
setDialogCategories(categories);
}, [categories]);
const unsetCategories: TCategory[] =
categories?.filter((category) => category[includeField] === IncludeOrExclude.Unset) ?? [];
const excludedCategories: TCategory[] =
categories?.filter((category) => category[includeField] === IncludeOrExclude.Exclude) ?? [];
const includedCategories: TCategory[] =
categories?.filter((category) => category[includeField] === IncludeOrExclude.Include) ?? [];
const unsetCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Unset);
const excludedCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Exclude);
const includedCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Include);
const excludedCategoriesText = getCategoryUpdateInfo(
excludedCategories,
false,
unsetCategories.length,
categories?.length ?? -1,
requestError,
categories.length,
);
const includedCategoriesText = getCategoryUpdateInfo(
includedCategories,
true,
unsetCategories.length,
categories?.length ?? -1,
requestError,
categories.length,
);
const updateCategory = (category: TCategory) =>
const updateCategory = (category: CategoryType) =>
requestManager.updateCategory(category.id, { [includeField]: category[includeField] }).response;
const updateCategories = async () => {
@@ -150,7 +147,7 @@ export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProp
};
const closeDialog = () => {
setDialogCategories(categories ?? []);
setDialogCategories(categories);
setIsDialogOpen(false);
};
@@ -193,7 +190,7 @@ export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProp
const categoryIndex = dialogCategories.findIndex(
(category_) => category_ === category,
);
const updatedDialogCategories: TCategory[] = [
const updatedDialogCategories = [
...dialogCategories.slice(0, categoryIndex),
{
...category,

View File

@@ -13,21 +13,21 @@ import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx';
import { updateMetadataServerSettings, useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { MetadataDownloadSettings } from '@/typings.ts';
import { updateMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { MetadataDownloadSettings, MetadataServerSettings } from '@/typings.ts';
import { makeToast } from '@/components/util/Toast.tsx';
const MIN_LIMIT = 2;
const MAX_LIMIT = 10;
const DEFAULT_LIMIT = MIN_LIMIT;
export const DownloadAheadSetting = () => {
export const DownloadAheadSetting = ({
downloadAheadLimit,
}: {
downloadAheadLimit: MetadataServerSettings['downloadAheadLimit'];
}) => {
const { t } = useTranslation();
const {
settings: { downloadAheadLimit },
} = useMetadataServerSettings();
const shouldDownloadAhead = !!downloadAheadLimit;
const [currentDownloadAheadLimit, persistDownloadAheadLimit] = usePersistedValue(
'lastDownloadAheadLimit',

View File

@@ -12,7 +12,10 @@ import { useTranslation } from 'react-i18next';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { CategoriesInclusionSetting } from '@/components/settings/CategoriesInclusionSetting.tsx';
import {
CategoriesInclusionSetting,
CategoriesInclusionSettingProps,
} from '@/components/settings/CategoriesInclusionSetting.tsx';
import { GlobalUpdateSettingsEntries } from '@/components/settings/globalUpdate/GlobalUpdateSettingsEntries.tsx';
import { GlobalUpdateSettingsInterval } from '@/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -21,11 +24,16 @@ import { ServerSettings } from '@/typings.ts';
type LibrarySettingsType = Pick<ServerSettings, 'updateMangas'>;
export const GlobalUpdateSettings = () => {
export const GlobalUpdateSettings = ({
serverSettings,
categories,
}: {
serverSettings: ServerSettings;
categories: CategoriesInclusionSettingProps['categories'];
}) => {
const { t } = useTranslation();
const { data } = requestManager.useGetServerSettings();
const updateMangas = !!data?.settings.updateMangas;
const { updateMangas } = serverSettings;
const [mutateSettings] = requestManager.useUpdateServerSettings();
const updateSetting = async <Setting extends keyof LibrarySettingsType>(
@@ -47,9 +55,10 @@ export const GlobalUpdateSettings = () => {
</ListSubheader>
}
>
<GlobalUpdateSettingsInterval />
<GlobalUpdateSettingsEntries />
<GlobalUpdateSettingsInterval globalUpdateInterval={serverSettings.globalUpdateInterval} />
<GlobalUpdateSettingsEntries serverSettings={serverSettings} />
<CategoriesInclusionSetting
categories={categories}
includeField="includeInUpdate"
dialogText={t('library.settings.global_update.categories.label.info')}
/>

View File

@@ -33,15 +33,7 @@ const settingToTextMap: { [setting in keyof GlobalUpdateSkipEntriesSettings]: Tr
excludeCompleted: 'library.settings.global_update.entries.label.completed',
};
const getSkipMangasText = (settings: GlobalUpdateSkipEntriesSettings | undefined, isLoading: boolean, error: any) => {
if (error) {
return translate('global.error.label.failed_to_load_data');
}
if (!settings || isLoading) {
return translate('global.label.loading');
}
const getSkipMangasText = (settings: GlobalUpdateSkipEntriesSettings) => {
const skipSettings: string[] = [];
if (settings.excludeUnreadChapters) {
@@ -70,10 +62,10 @@ const extractSkipEntriesSettings = (serverSettings: ServerSettings): GlobalUpdat
excludeUnreadChapters: serverSettings.excludeUnreadChapters,
});
export const GlobalUpdateSettingsEntries = () => {
export const GlobalUpdateSettingsEntries = ({ serverSettings }: { serverSettings: ServerSettings }) => {
const { t } = useTranslation();
const { data, loading, error: requestError } = requestManager.useGetServerSettings();
const globalUpdateSettings = data ? extractSkipEntriesSettings(data.settings) : undefined;
const globalUpdateSettings = extractSkipEntriesSettings(serverSettings);
const [mutateSettings] = requestManager.useUpdateServerSettings();
const [dialogSettings, setDialogSettings] = useState<GlobalUpdateSkipEntriesSettings>(
@@ -81,7 +73,7 @@ export const GlobalUpdateSettingsEntries = () => {
);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const skipEntriesText = getSkipMangasText(globalUpdateSettings, loading, requestError);
const skipEntriesText = getSkipMangasText(globalUpdateSettings);
const updateSettings = async () => {
const didSettingsChange =

View File

@@ -15,16 +15,20 @@ import { useCallback } from 'react';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx';
import { ServerSettings } from '@/typings.ts';
const DEFAULT_INTERVAL_HOURS = 12;
const MIN_INTERVAL_HOURS = 6;
const MAX_INTERVAL_HOURS = 24 * 7 * 4; // 1 month
export const GlobalUpdateSettingsInterval = () => {
export const GlobalUpdateSettingsInterval = ({
globalUpdateInterval,
}: {
globalUpdateInterval: ServerSettings['globalUpdateInterval'];
}) => {
const { t } = useTranslation();
const { data } = requestManager.useGetServerSettings();
const autoUpdateIntervalHours = data?.settings.globalUpdateInterval;
const autoUpdateIntervalHours = globalUpdateInterval;
const doAutoUpdates = !!autoUpdateIntervalHours;
const [mutateSettings] = requestManager.useUpdateServerSettings();
const [currentAutoUpdateIntervalHours, persistAutoUpdateIntervalHours] = usePersistedValue(
@@ -35,18 +39,18 @@ export const GlobalUpdateSettingsInterval = () => {
);
const updateSetting = useCallback(
(globalUpdateInterval: number) => {
(newGlobalUpdateInterval: number) => {
persistAutoUpdateIntervalHours(
globalUpdateInterval === 0 ? currentAutoUpdateIntervalHours : globalUpdateInterval,
newGlobalUpdateInterval === 0 ? currentAutoUpdateIntervalHours : newGlobalUpdateInterval,
);
mutateSettings({ variables: { input: { settings: { globalUpdateInterval } } } });
mutateSettings({ variables: { input: { settings: { globalUpdateInterval: newGlobalUpdateInterval } } } });
},
[currentAutoUpdateIntervalHours],
);
const setDoAutoUpdates = (enable: boolean) => {
const globalUpdateInterval = enable ? currentAutoUpdateIntervalHours : 0;
updateSetting(globalUpdateInterval);
const newGlobalUpdateInterval = enable ? currentAutoUpdateIntervalHours : 0;
updateSetting(newGlobalUpdateInterval);
};
return (

View File

@@ -15,16 +15,21 @@ import { useCallback } from 'react';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx';
import { ServerSettings } from '@/typings.ts';
const DEFAULT_VALUE = 23;
const MIN_VALUE = 1;
const MAX_VALUE = 23; // 1 month
export const WebUIUpdateIntervalSetting = ({ disabled = false }: { disabled?: boolean }) => {
export const WebUIUpdateIntervalSetting = ({
disabled = false,
updateCheckInterval,
}: {
disabled?: boolean;
updateCheckInterval: ServerSettings['webUIUpdateCheckInterval'];
}) => {
const { t } = useTranslation();
const { data } = requestManager.useGetServerSettings();
const updateCheckInterval = data?.settings.webUIUpdateCheckInterval;
const shouldAutoUpdate = !!updateCheckInterval;
const [mutateSettings] = requestManager.useUpdateServerSettings();
const [currentUpdateCheckInterval, persistUpdateCheckInterval] = usePersistedValue(

View File

@@ -11,7 +11,10 @@ import { TCategory } from '@/typings.ts';
export const DEFAULT_CATEGORY_ID = 0;
export type CategoryIdInfo = Pick<TCategory, 'id'>;
export type CategoryNameInfo = Pick<TCategory, 'name'>;
export type CategoryDefaultInfo = Pick<TCategory, 'default'>;
export type CategoryUpdateInclusionInfo = Pick<TCategory, 'includeInUpdate'>;
export type CategoryDownloadInclusionInfo = Pick<TCategory, 'includeInDownload'>;
export class Categories {
static getIds(categories: CategoryIdInfo[]): number[] {

View File

@@ -65,12 +65,48 @@ export const DownloadSettings = () => {
};
}, [t]);
const { data, loading, error, refetch } = requestManager.useGetServerSettings({
notifyOnNetworkStatusChange: true,
});
const downloadSettings = data ? extractDownloadSettings(data.settings) : undefined;
const categories = requestManager.useGetCategories();
const serverSettings = requestManager.useGetServerSettings({ notifyOnNetworkStatusChange: true });
const [mutateSettings] = requestManager.useUpdateServerSettings();
const { settings: metadataSettings } = useMetadataServerSettings();
const {
settings: metadataSettings,
loading: areMetadataServerSettingsLoading,
request: { error: metadataServerSettingsError, refetch: refetchMetadataServerSettings },
} = useMetadataServerSettings();
const loading = serverSettings.loading || areMetadataServerSettingsLoading || categories.loading;
if (loading) {
return <LoadingPlaceholder />;
}
const error = serverSettings.error ?? metadataServerSettingsError ?? categories.error;
if (error) {
return (
<EmptyView
message={t('global.error.label.failed_to_load_data')}
messageExtra={error.message}
retry={() => {
if (serverSettings.error) {
serverSettings
.refetch()
.catch(defaultPromiseErrorHandler('DownloadSettings::refetchServerSettings'));
}
if (metadataServerSettingsError) {
refetchMetadataServerSettings().catch(
defaultPromiseErrorHandler('refetchMetadataServerSettings::'),
);
}
if (categories.error) {
categories.refetch().catch(defaultPromiseErrorHandler('LibrarySettings::refetchCategories'));
}
}}
/>
);
}
const downloadSettings = extractDownloadSettings(serverSettings.data!.settings);
const updateSetting = <Setting extends keyof DownloadSettingsType>(
setting: Setting,
@@ -85,20 +121,6 @@ export const DownloadSettings = () => {
makeToast(t('global.error.label.failed_to_save_changes'), 'error'),
);
if (loading) {
return <LoadingPlaceholder />;
}
if (error) {
return (
<EmptyView
message={t('global.error.label.failed_to_load_data')}
messageExtra={error.message}
retry={() => refetch().catch(defaultPromiseErrorHandler('DownloadSettings::refetch'))}
/>
);
}
return (
<List>
<TextSetting
@@ -204,6 +226,7 @@ export const DownloadSettings = () => {
/>
</ListItem>
<CategoriesInclusionSetting
categories={categories.data!.categories.nodes}
includeField="includeInDownload"
dialogText={t('download.settings.auto_download.categories.label.include_in_download')}
/>
@@ -215,7 +238,7 @@ export const DownloadSettings = () => {
</ListSubheader>
}
>
<DownloadAheadSetting />
<DownloadAheadSetting downloadAheadLimit={metadataSettings.downloadAheadLimit} />
</List>
</List>
);

View File

@@ -25,6 +25,9 @@ import {
} from '@/lib/metadata/metadataServerSettings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Mangas } from '@/lib/data/Mangas.ts';
import { EmptyView } from '@/components/util/EmptyView';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
const removeNonLibraryMangasFromCategories = async (): Promise<void> => {
try {
@@ -61,12 +64,50 @@ export function LibrarySettings() {
useSetDefaultBackTo('settings');
const { settings } = useMetadataServerSettings();
const categories = requestManager.useGetCategories();
const serverSettings = requestManager.useGetServerSettings({ notifyOnNetworkStatusChange: true });
const {
settings,
loading: areMetadataServerSettingsLoading,
request: { error: metadataServerSettingsError, refetch: refetchMetadataServerSettings },
} = useMetadataServerSettings();
const setSettingValue = createUpdateMetadataServerSettings<keyof MetadataLibrarySettings>(() =>
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'),
);
const loading = serverSettings.loading || areMetadataServerSettingsLoading || categories.loading;
if (loading) {
return <LoadingPlaceholder />;
}
const error = serverSettings.error ?? metadataServerSettingsError ?? categories.error;
if (error) {
return (
<EmptyView
message={t('global.error.label.failed_to_load_data')}
messageExtra={error.message}
retry={() => {
if (serverSettings.error) {
serverSettings
?.refetch()
.catch(defaultPromiseErrorHandler('LibrarySettings::refetchServerSettings'));
}
if (metadataServerSettingsError) {
refetchMetadataServerSettings().catch(
defaultPromiseErrorHandler('LibrarySettings::refetchMetadataServerSettings'),
);
}
if (categories.error) {
categories.refetch().catch(defaultPromiseErrorHandler('LibrarySettings::refetchCategories'));
}
}}
/>
);
}
return (
<List>
<List
@@ -112,7 +153,10 @@ export function LibrarySettings() {
/>
</ListItem>
</List>
<GlobalUpdateSettings />
<GlobalUpdateSettings
serverSettings={serverSettings.data!.settings}
categories={categories.data!.categories.nodes}
/>
<List
subheader={
<ListSubheader component="div" id="library-advanced">