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

View File

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

View File

@@ -12,7 +12,10 @@ import { useTranslation } from 'react-i18next';
import ListItem from '@mui/material/ListItem'; import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch'; 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 { GlobalUpdateSettingsEntries } from '@/components/settings/globalUpdate/GlobalUpdateSettingsEntries.tsx';
import { GlobalUpdateSettingsInterval } from '@/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx'; import { GlobalUpdateSettingsInterval } from '@/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -21,11 +24,16 @@ import { ServerSettings } from '@/typings.ts';
type LibrarySettingsType = Pick<ServerSettings, 'updateMangas'>; type LibrarySettingsType = Pick<ServerSettings, 'updateMangas'>;
export const GlobalUpdateSettings = () => { export const GlobalUpdateSettings = ({
serverSettings,
categories,
}: {
serverSettings: ServerSettings;
categories: CategoriesInclusionSettingProps['categories'];
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = requestManager.useGetServerSettings(); const { updateMangas } = serverSettings;
const updateMangas = !!data?.settings.updateMangas;
const [mutateSettings] = requestManager.useUpdateServerSettings(); const [mutateSettings] = requestManager.useUpdateServerSettings();
const updateSetting = async <Setting extends keyof LibrarySettingsType>( const updateSetting = async <Setting extends keyof LibrarySettingsType>(
@@ -47,9 +55,10 @@ export const GlobalUpdateSettings = () => {
</ListSubheader> </ListSubheader>
} }
> >
<GlobalUpdateSettingsInterval /> <GlobalUpdateSettingsInterval globalUpdateInterval={serverSettings.globalUpdateInterval} />
<GlobalUpdateSettingsEntries /> <GlobalUpdateSettingsEntries serverSettings={serverSettings} />
<CategoriesInclusionSetting <CategoriesInclusionSetting
categories={categories}
includeField="includeInUpdate" includeField="includeInUpdate"
dialogText={t('library.settings.global_update.categories.label.info')} 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', excludeCompleted: 'library.settings.global_update.entries.label.completed',
}; };
const getSkipMangasText = (settings: GlobalUpdateSkipEntriesSettings | undefined, isLoading: boolean, error: any) => { const getSkipMangasText = (settings: GlobalUpdateSkipEntriesSettings) => {
if (error) {
return translate('global.error.label.failed_to_load_data');
}
if (!settings || isLoading) {
return translate('global.label.loading');
}
const skipSettings: string[] = []; const skipSettings: string[] = [];
if (settings.excludeUnreadChapters) { if (settings.excludeUnreadChapters) {
@@ -70,10 +62,10 @@ const extractSkipEntriesSettings = (serverSettings: ServerSettings): GlobalUpdat
excludeUnreadChapters: serverSettings.excludeUnreadChapters, excludeUnreadChapters: serverSettings.excludeUnreadChapters,
}); });
export const GlobalUpdateSettingsEntries = () => { export const GlobalUpdateSettingsEntries = ({ serverSettings }: { serverSettings: ServerSettings }) => {
const { t } = useTranslation(); 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 [mutateSettings] = requestManager.useUpdateServerSettings();
const [dialogSettings, setDialogSettings] = useState<GlobalUpdateSkipEntriesSettings>( const [dialogSettings, setDialogSettings] = useState<GlobalUpdateSkipEntriesSettings>(
@@ -81,7 +73,7 @@ export const GlobalUpdateSettingsEntries = () => {
); );
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const skipEntriesText = getSkipMangasText(globalUpdateSettings, loading, requestError); const skipEntriesText = getSkipMangasText(globalUpdateSettings);
const updateSettings = async () => { const updateSettings = async () => {
const didSettingsChange = const didSettingsChange =

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,9 @@ import {
} from '@/lib/metadata/metadataServerSettings.ts'; } from '@/lib/metadata/metadataServerSettings.ts';
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 { EmptyView } from '@/components/util/EmptyView';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
const removeNonLibraryMangasFromCategories = async (): Promise<void> => { const removeNonLibraryMangasFromCategories = async (): Promise<void> => {
try { try {
@@ -61,12 +64,50 @@ export function LibrarySettings() {
useSetDefaultBackTo('settings'); 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>(() => const setSettingValue = createUpdateMetadataServerSettings<keyof MetadataLibrarySettings>(() =>
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'), 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 ( return (
<List> <List>
<List <List
@@ -112,7 +153,10 @@ export function LibrarySettings() {
/> />
</ListItem> </ListItem>
</List> </List>
<GlobalUpdateSettings /> <GlobalUpdateSettings
serverSettings={serverSettings.data!.settings}
categories={categories.data!.categories.nodes}
/>
<List <List
subheader={ subheader={
<ListSubheader component="div" id="library-advanced"> <ListSubheader component="div" id="library-advanced">