From 78049282a770f912d4f28515937a3b7d0cd62658 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 25 Sep 2023 23:32:26 +0200 Subject: [PATCH] Use gql for "sources" - preferences --- src/components/source/filters/GroupFilter.tsx | 7 +- .../EditTextPreference.tsx | 15 +++-- .../sourceConfiguration/ListPreference.tsx | 22 ++++-- .../MultiSelectListPreference.tsx | 28 +++++--- .../TwoStatePreference.tsx | 39 +++++++++-- src/lib/requests/RequestManager.ts | 36 +++++----- src/screens/SourceConfigure.tsx | 36 +++++----- src/typings.ts | 67 +++++++++---------- 8 files changed, 147 insertions(+), 103 deletions(-) diff --git a/src/components/source/filters/GroupFilter.tsx b/src/components/source/filters/GroupFilter.tsx index f1cf7589..dd07d2e4 100644 --- a/src/components/source/filters/GroupFilter.tsx +++ b/src/components/source/filters/GroupFilter.tsx @@ -10,14 +10,11 @@ import { ExpandLess, ExpandMore } from '@mui/icons-material'; import { Collapse, ListItemButton, ListItemText, Stack, Box } from '@mui/material'; import React from 'react'; // eslint-disable-next-line import/no-cycle -import { SourceFilters } from '@/typings'; +import { ExtractByKeyValue, SourceFilters } from '@/typings'; import { Options } from '@/components/source/SourceOptions'; -// type ExcludeByName = T extends { __typename?: K } ? never : T; -type PickByName = T extends { __typename?: K } ? T : never; - interface Props { - state: PickByName['filters']; + state: ExtractByKeyValue['filters']; name: string; position: number; updateFilterValue: Function; diff --git a/src/components/sourceConfiguration/EditTextPreference.tsx b/src/components/sourceConfiguration/EditTextPreference.tsx index 45a58e8d..9a364b35 100644 --- a/src/components/sourceConfiguration/EditTextPreference.tsx +++ b/src/components/sourceConfiguration/EditTextPreference.tsx @@ -22,22 +22,29 @@ import { EditTextPreferenceProps } from '@/typings'; export default function EditTextPreference(props: EditTextPreferenceProps) { const { t } = useTranslation(); - const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props; + const { + EditTextPreferenceTitle: title, + summary, + dialogTitle, + dialogMessage, + EditTextPreferenceCurrentValue: currentValue, + updateValue, + } = props; - const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue); + const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? ''); const [dialogOpen, setDialogOpen] = useState(false); const handleDialogCancel = () => { setDialogOpen(false); // reset the dialog - setInternalCurrentValue(currentValue); + setInternalCurrentValue(currentValue ?? ''); }; const handleDialogSubmit = () => { setDialogOpen(false); - updateValue(internalCurrentValue); + updateValue('editTextState', internalCurrentValue); }; return ( diff --git a/src/components/sourceConfiguration/ListPreference.tsx b/src/components/sourceConfiguration/ListPreference.tsx index 6cee2401..0460be0c 100644 --- a/src/components/sourceConfiguration/ListPreference.tsx +++ b/src/components/sourceConfiguration/ListPreference.tsx @@ -85,12 +85,20 @@ function ListDialog(props: IListDialogProps) { } export default function ListPreference(props: ListPreferenceProps) { - const { title, summary, currentValue, updateValue, entryValues, entries } = props; - const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue); + const { + ListPreferenceTitle: title, + summary, + ListPreferenceCurrentValue: currentValue, + ListPreferenceDefault: defaultValue, + updateValue, + entryValues, + entries, + } = props; + const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue ?? ''); const [dialogOpen, setDialogOpen] = useState(false); useEffect(() => { - setInternalCurrentValue(currentValue); + setInternalCurrentValue(currentValue ?? defaultValue ?? ''); }, [currentValue]); const findEntryOf = (value: string) => { @@ -104,6 +112,10 @@ export default function ListPreference(props: ListPreferenceProps) { }; const getSummary = () => { + if (currentValue == null) { + return ''; + } + if (summary === '%s') { return findEntryOf(currentValue); } @@ -112,7 +124,7 @@ export default function ListPreference(props: ListPreferenceProps) { const handleDialogClose = (newValue: string | null) => { if (newValue !== null) { - updateValue(findEntryValueOf(newValue)); + updateValue('listState', findEntryValueOf(newValue)); // appear smooth setInternalCurrentValue(newValue); @@ -127,7 +139,7 @@ export default function ListPreference(props: ListPreferenceProps) { (currentValue); + const { + MultiSelectListPreferenceTitle: title, + summary, + MultiSelectListPreferenceCurrentValue: currentValue, + MultiSelectListPreferenceDefault: defaultValue, + updateValue, + entryValues, + entries, + } = props; + const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue); const [dialogOpen, setDialogOpen] = useState(false); useEffect(() => { setInternalCurrentValue(currentValue); }, [currentValue]); - const findEntriesOf = (values: string[]) => - values.map((value) => { + const findEntriesOf = (values?: string[] | null) => + values?.map((value) => { const idx = entryValues.indexOf(value); return entries[idx]; - }); + }) ?? []; - const findEntryValuesOf = (values: string[]) => - values.map((value) => { + const findEntryValuesOf = (values?: string[] | null) => + values?.map((value) => { const idx = entries.indexOf(value); return entryValues[idx]; - }); + }) ?? []; const getSummary = () => summary; const handleDialogClose = (newValue: string[] | null) => { if (newValue !== null) { // console.log(newValue); - updateValue(findEntryValuesOf(newValue)); + updateValue('multiSelectState', findEntryValuesOf(newValue)); // appear smooth setInternalCurrentValue(newValue); @@ -139,7 +147,7 @@ export default function MultiSelectListPreference(props: MultiSelectListPreferen { + if (props.type === 'CheckBoxPreference') { + return { + title: props.CheckBoxTitle, + defaultValue: props.CheckBoxDefault, + currentValue: props.CheckBoxCheckBoxCurrentValue, + }; + } + + return { + title: props.SwitchPreferenceTitle, + defaultValue: props.SwitchPreferenceDefault, + currentValue: props.SwitchPreferenceCurrentValue, + }; +}; + function TwoSatePreference(props: TwoStatePreferenceProps) { - const { title, summary, currentValue, updateValue, type } = props; - const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue); + const { title, defaultValue, currentValue, summary, updateValue, twoStateType } = { + ...props, + ...getTwoStateValues(props), + }; + const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue); useEffect(() => { - setInternalCurrentValue(currentValue); + setInternalCurrentValue(currentValue ?? defaultValue); }, [currentValue]); return ( - {createElement(getTwoStateType(type), { + {createElement(getTwoStateType(twoStateType), { edge: 'end', checked: internalCurrentValue, onChange: () => { - updateValue(!currentValue); + updateValue(twoStateType === 'Switch' ? 'switchState' : 'checkBoxState', !currentValue); // appear smooth setInternalCurrentValue(!currentValue); @@ -49,11 +74,11 @@ function TwoSatePreference(props: TwoStatePreferenceProps) { } export function CheckBoxPreference(props: CheckBoxPreferenceProps) { - return ; + return ; } export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) { - return ; + return ; } export default { CheckBoxPreference, SwitchPreferenceCompat }; diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 7721e253..2a7e3fe3 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -24,13 +24,7 @@ import { } from '@apollo/client'; import { OperationVariables } from '@apollo/client/core'; import { useEffect, useRef, useState } from 'react'; -import { - BackupValidationResult, - IChapter, - IMangaChapter, - PaginatedList, - SourcePreferences, -} from '@/typings.ts'; +import { BackupValidationResult, IChapter, IMangaChapter, PaginatedList } from '@/typings.ts'; import { HttpMethod as DefaultHttpMethod, IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts'; import storage from '@/util/localStorage.tsx'; import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts'; @@ -95,6 +89,7 @@ import { SetGlobalMetadataMutation, SetGlobalMetadataMutationVariables, SetMangaMetadataMutation, + SourcePreferenceChangeInput, StartDownloaderMutation, StartDownloaderMutationVariables, StopDownloaderMutation, @@ -123,6 +118,8 @@ import { UpdateMangaMutation, UpdateMangaMutationVariables, UpdateMangaPatchInput, + UpdateSourcePreferencesMutation, + UpdateSourcePreferencesMutationVariables, } from '@/lib/graphql/generated/graphql.ts'; import { GET_GLOBAL_METADATA, GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts'; import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts'; @@ -142,7 +139,7 @@ import { } from '@/lib/graphql/mutations/MangaMutation.ts'; import { GET_MANGA, GET_MANGAS } from '@/lib/graphql/queries/MangaQuery.ts'; import { GET_CATEGORIES, GET_CATEGORY, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts'; -import { GET_SOURCE_MANGAS_FETCH } from '@/lib/graphql/mutations/SourceMutation.ts'; +import { GET_SOURCE_MANGAS_FETCH, UPDATE_SOURCE_PREFERENCES } from '@/lib/graphql/mutations/SourceMutation.ts'; import { CLEAR_DOWNLOADER, DELETE_DOWNLOADED_CHAPTER, @@ -1195,15 +1192,20 @@ export class RequestManager { ); } - public useGetSourcePreferences( - sourceId: string, - swrOptions?: SWROptions, - ): AbortableSWRResponse { - return this.doRequest(HttpMethod.SWR_GET, `source/${sourceId}/preferences`, { swrOptions }); - } - - public setSourcePreferences(sourceId: string, position: number, value: string): AbortableAxiosResponse { - return this.doRequest(HttpMethod.POST, `source/${sourceId}/preferences`, { data: { position, value } }); + public setSourcePreferences( + source: string, + change: SourcePreferenceChangeInput, + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + UPDATE_SOURCE_PREFERENCES, + { input: { source, change } }, + { + refetchQueries: [GET_SOURCE], + ...options, + }, + ); } public useSourceSearch( diff --git a/src/screens/SourceConfigure.tsx b/src/screens/SourceConfigure.tsx index 9fc74c3d..243106bd 100644 --- a/src/screens/SourceConfigure.tsx +++ b/src/screens/SourceConfigure.tsx @@ -17,12 +17,13 @@ import { SwitchPreferenceCompat, CheckBoxPreference } from '@/components/sourceC import ListPreference from '@/components/sourceConfiguration/ListPreference'; import EditTextPreference from '@/components/sourceConfiguration/EditTextPreference'; import MultiSelectListPreference from '@/components/sourceConfiguration/MultiSelectListPreference'; +import { PreferenceProps } from '@/typings.ts'; function getPrefComponent(type: string) { switch (type) { case 'CheckBoxPreference': return CheckBoxPreference; - case 'SwitchPreferenceCompat': + case 'SwitchPreference': return SwitchPreferenceCompat; case 'ListPreference': return ListPreference; @@ -31,7 +32,7 @@ function getPrefComponent(type: string) { case 'MultiSelectListPreference': return MultiSelectListPreference; default: - return CheckBoxPreference; + throw new Error(`Unexpected preference type "${type}"`); } } @@ -45,33 +46,26 @@ export default function SourceConfigure() { }, [t]); const { sourceId } = useParams<{ sourceId: string }>(); - const { data: sourcePreferences = [], mutate } = requestManager.useGetSourcePreferences(sourceId); + const { data } = requestManager.useGetSource(sourceId); + const sourcePreferences = data?.source.preferences ?? []; - const convertToString = (position: number, value: any): string => { - switch (sourcePreferences[position].props.defaultValueType) { - case 'Set': - return JSON.stringify(value); - default: - return value.toString(); - } - }; - - const updateValue = (position: number) => (value: any) => { - requestManager - .setSourcePreferences(sourceId, position, convertToString(position, value)) - .response.then(() => mutate()); - }; + const updateValue = + (position: number): PreferenceProps['updateValue'] => + (type, value) => { + requestManager.setSourcePreferences(sourceId, { position, [type]: value }); + }; return ( {sourcePreferences.map((it, index) => { - const props = cloneObject(it.props); - props.updateValue = updateValue(index); - props.key = index; + const props = cloneObject(it); // TypeScript is dumb in detecting extra props // @ts-ignore - return createElement(getPrefComponent(it.type), props); + return createElement(getPrefComponent(it.type), { + ...props, + updateValue: updateValue(index), + }); })} ); diff --git a/src/typings.ts b/src/typings.ts index ca0669cc..ea9255d6 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -10,7 +10,19 @@ import { OverridableComponent } from '@mui/material/OverridableComponent'; import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon'; import { ParseKeys } from 'i18next'; import { Location } from 'react-router-dom'; -import { ExtensionType, GetSourceQuery, MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts'; +import { + ExtensionType, + GetSourceQuery, + MangaType, + MetaType, + SourcePreferenceChangeInput, +} from '@/lib/graphql/generated/graphql.ts'; + +export type ExtractByKeyValue = T extends + | Record + | Partial> + ? T + : never; export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] @@ -253,47 +265,34 @@ export interface IUpdateStatus { }; } +export type SourcePreferences = GetSourceQuery['source']['preferences'][number]; + export interface PreferenceProps { - key: string; - title: string; - summary: string; - defaultValue: any; - currentValue: any; - defaultValueType: string; + updateValue: >( + type: Key, + value: SourcePreferenceChangeInput[Key], + ) => void; +} +export type TwoStatePreferenceProps = (CheckBoxPreferenceProps | SwitchPreferenceCompatProps) & { // intetnal props - updateValue: any; -} + twoStateType: 'Switch' | 'Checkbox'; +}; -export interface TwoStatePreferenceProps extends PreferenceProps { - // intetnal props - type: 'Switch' | 'Checkbox'; -} +export type CheckBoxPreferenceProps = PreferenceProps & + ExtractByKeyValue; -export interface CheckBoxPreferenceProps extends PreferenceProps {} +export type SwitchPreferenceCompatProps = PreferenceProps & + ExtractByKeyValue; -export interface SwitchPreferenceCompatProps extends PreferenceProps {} +export type ListPreferenceProps = PreferenceProps & + ExtractByKeyValue; -export interface ListPreferenceProps extends PreferenceProps { - entries: string[]; - entryValues: string[]; -} +export type MultiSelectListPreferenceProps = PreferenceProps & + ExtractByKeyValue; -export interface MultiSelectListPreferenceProps extends PreferenceProps { - entries: string[]; - entryValues: string[]; -} - -export interface EditTextPreferenceProps extends PreferenceProps { - dialogTitle: string; - dialogMessage: string; - text: string; -} - -export interface SourcePreferences { - type: string; - props: any; -} +export type EditTextPreferenceProps = PreferenceProps & + ExtractByKeyValue; export interface NavbarItem { path: string;