From 78310496aa7483f65787b4022662bf662579d653 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 9 Dec 2023 23:08:27 +0100 Subject: [PATCH] Feature/about screen add option to check for and trigger updates (#485) * [VersionMapping] Require server version "r1436" for preview * Move ts util types to new type file * Add buttons for updating server and webUI --- src/UtilTypes.d.ts | 25 ++ src/components/source/filters/GroupFilter.tsx | 2 +- src/i18n/locale/en.json | 12 + src/lib/graphql/Fragments.ts | 15 + src/lib/graphql/generated/graphql.ts | 9 + .../graphql/mutations/ServerInfoMutation.ts | 14 +- src/lib/graphql/queries/ServerInfoQuery.ts | 11 +- src/lib/requests/RequestManager.ts | 39 ++- src/lib/requests/client/GraphQLClient.ts | 2 + src/screens/settings/About.tsx | 277 +++++++++++++++++- src/screens/settings/WebUISettings.tsx | 4 + src/typings.ts | 16 - versionToServerVersionMapping.json | 2 +- 13 files changed, 389 insertions(+), 39 deletions(-) create mode 100644 src/UtilTypes.d.ts diff --git a/src/UtilTypes.d.ts b/src/UtilTypes.d.ts new file mode 100644 index 00000000..c2d77733 --- /dev/null +++ b/src/UtilTypes.d.ts @@ -0,0 +1,25 @@ +/* + * 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/. + */ + +type ExtractByKeyValue = T extends + | Record + | Partial> + ? T + : never; + +type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object | undefined + ? RecursivePartial + : T[P]; +}; + +type OptionalProperty = Omit & Partial>; + +type PropertiesNever = { [key in keyof T]?: never }; diff --git a/src/components/source/filters/GroupFilter.tsx b/src/components/source/filters/GroupFilter.tsx index 413d937a..3d65c01f 100644 --- a/src/components/source/filters/GroupFilter.tsx +++ b/src/components/source/filters/GroupFilter.tsx @@ -10,7 +10,7 @@ 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 { ExtractByKeyValue, SourceFilters } from '@/typings'; +import { SourceFilters } from '@/typings'; import { Options } from '@/components/source/SourceOptions'; interface Props { diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 3c813714..e127e317 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -339,6 +339,16 @@ }, "time": { "hour_short": "h" + }, + "update": { + "label": { + "available": "Update available", + "check_failure": "Could not check for update", + "checking": "Checking for update", + "up_to_date": "This is the latest version", + "update_failure": "Update failed", + "updating": "{{progress}}% | Updating…" + } } }, "library": { @@ -531,6 +541,8 @@ "label": { "channel": "$t(settings.webui.title.webui) channel", "github": "$t(global.label.github) $t(settings.webui.title.webui)", + "update_failure": "Could not update $t(settings.webui.title.webui)", + "update_success": "Updated $t(settings.webui.title.webui) to version {{version}}. Refresh required", "version": "$t(settings.webui.title.webui) version" } } diff --git a/src/lib/graphql/Fragments.ts b/src/lib/graphql/Fragments.ts index 9bc436aa..48f5baf1 100644 --- a/src/lib/graphql/Fragments.ts +++ b/src/lib/graphql/Fragments.ts @@ -412,6 +412,21 @@ export const FULL_UPDATER_STATUS = gql` } `; +export const ABOUT_WEBUI = gql` + fragment ABOUT_WEBUI on AboutWebUI { + channel + tag + } +`; + +export const WEBUI_UPDATE_CHECK = gql` + fragment WEBUI_UPDATE_CHECK on WebUIUpdateCheck { + channel + tag + updateAvailable + } +`; + export const WEBUI_UPDATE_INFO = gql` fragment WEBUI_UPDATE_INFO on WebUIUpdateInfo { channel diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index b3b2f2dc..0092e425 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2143,6 +2143,10 @@ export type PartialUpdaterStatusFragment = { __typename?: 'UpdateStatus', isRunn export type FullUpdaterStatusFragment = { __typename?: 'UpdateStatus', isRunning: boolean, completeJobs: { __typename?: 'UpdateStatusType', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', unreadCount: number, downloadCount: number, artist?: string | null, author?: string | null, chaptersLastFetchedAt?: any | null, description?: string | null, genre: Array, id: number, inLibrary: boolean, inLibraryAt: any, initialized: boolean, lastFetchedAt?: any | null, realUrl?: string | null, status: MangaStatus, thumbnailUrl?: string | null, title: string, url: string, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, meta: Array<{ __typename?: 'MangaMetaType', key: string, value: string }>, source?: { __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean } | null }> } }, failedJobs: { __typename?: 'UpdateStatusType', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null }> } }, pendingJobs: { __typename?: 'UpdateStatusType', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null }> } }, runningJobs: { __typename?: 'UpdateStatusType', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null }> } }, skippedJobs: { __typename?: 'UpdateStatusType', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null }> } }, updatingCategories: { __typename?: 'UpdateStatusCategoryType', categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', id: number, name: string, includeInUpdate: IncludeInUpdate }> } }, skippedCategories: { __typename?: 'UpdateStatusCategoryType', categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', id: number, name: string, includeInUpdate: IncludeInUpdate }> } } }; +export type AboutWebuiFragment = { __typename?: 'AboutWebUI', channel: string, tag: string }; + +export type WebuiUpdateCheckFragment = { __typename?: 'WebUIUpdateCheck', channel: string, tag: string, updateAvailable: boolean }; + export type WebuiUpdateInfoFragment = { __typename?: 'WebUIUpdateInfo', channel: string, tag: string }; export type WebuiUpdateStatusFragment = { __typename?: 'WebUIUpdateStatus', progress: number, state: UpdateState, info: { __typename?: 'WebUIUpdateInfo', channel: string, tag: string } }; @@ -2458,6 +2462,11 @@ export type UpdateWebuiMutationVariables = Exact<{ export type UpdateWebuiMutation = { __typename?: 'Mutation', updateWebUI: { __typename?: 'WebUIUpdatePayload', clientMutationId?: string | null, updateStatus: { __typename?: 'WebUIUpdateStatus', progress: number, state: UpdateState, info: { __typename?: 'WebUIUpdateInfo', channel: string, tag: string } } } }; +export type ResetWebuiUpdateStatusMutationVariables = Exact<{ [key: string]: never; }>; + + +export type ResetWebuiUpdateStatusMutation = { __typename?: 'Mutation', resetWebUIUpdateStatus: { __typename?: 'WebUIUpdateStatus', state: UpdateState, info: { __typename?: 'WebUIUpdateInfo', channel: string, tag: string } } }; + export type ResetServerSettingsMutationVariables = Exact<{ input: ResetSettingsInput; }>; diff --git a/src/lib/graphql/mutations/ServerInfoMutation.ts b/src/lib/graphql/mutations/ServerInfoMutation.ts index 6f1b9e30..96e60d16 100644 --- a/src/lib/graphql/mutations/ServerInfoMutation.ts +++ b/src/lib/graphql/mutations/ServerInfoMutation.ts @@ -7,7 +7,7 @@ */ import gql from 'graphql-tag'; -import { WEBUI_UPDATE_STATUS } from '@/lib/graphql/Fragments'; +import { WEBUI_UPDATE_INFO, WEBUI_UPDATE_STATUS } from '@/lib/graphql/Fragments'; export const UPDATE_WEBUI = gql` ${WEBUI_UPDATE_STATUS} @@ -20,3 +20,15 @@ export const UPDATE_WEBUI = gql` } } `; + +export const RESET_WEBUI_UPDATE_STATUS = gql` + ${WEBUI_UPDATE_INFO} + mutation RESET_WEBUI_UPDATE_STATUS { + resetWebUIUpdateStatus { + info { + ...WEBUI_UPDATE_INFO + } + state + } + } +`; diff --git a/src/lib/graphql/queries/ServerInfoQuery.ts b/src/lib/graphql/queries/ServerInfoQuery.ts index b1d8b9d5..355bdc19 100644 --- a/src/lib/graphql/queries/ServerInfoQuery.ts +++ b/src/lib/graphql/queries/ServerInfoQuery.ts @@ -7,9 +7,10 @@ */ import gql from 'graphql-tag'; -import { WEBUI_UPDATE_STATUS } from '@/lib/graphql/Fragments'; +import { ABOUT_WEBUI, WEBUI_UPDATE_CHECK, WEBUI_UPDATE_STATUS } from '@/lib/graphql/Fragments'; export const GET_ABOUT = gql` + ${ABOUT_WEBUI} query GET_ABOUT { aboutServer { buildTime @@ -21,8 +22,7 @@ export const GET_ABOUT = gql` version } aboutWebUI { - channel - tag + ...ABOUT_WEBUI } } `; @@ -38,11 +38,10 @@ export const CHECK_FOR_SERVER_UPDATES = gql` `; export const CHECK_FOR_WEBUI_UPDATE = gql` + ${WEBUI_UPDATE_CHECK} query CHECK_FOR_WEBUI_UPDATE { checkForWebUIUpdate { - channel - tag - updateAvailable + ...WEBUI_UPDATE_CHECK } } `; diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 656a4e6a..a59ac915 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -36,6 +36,8 @@ import { CheckForServerUpdatesQuery, CheckForServerUpdatesQueryVariables, ClearCachedImagesInput, + CheckForWebuiUpdateQuery, + CheckForWebuiUpdateQueryVariables, ClearDownloaderMutation, ClearDownloaderMutationVariables, ClearServerCacheMutation, @@ -151,12 +153,17 @@ import { UpdateServerSettingsMutationVariables, UpdateSourcePreferencesMutation, UpdateSourcePreferencesMutationVariables, + UpdateWebuiMutation, + UpdateWebuiMutationVariables, ValidateBackupQuery, ValidateBackupQueryVariables, + WebuiUpdateSubscription, + ResetWebuiUpdateStatusMutation, + ResetWebuiUpdateStatusMutationVariables, } from '@/lib/graphql/generated/graphql.ts'; import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts'; import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts'; -import { CHECK_FOR_SERVER_UPDATES, GET_ABOUT } from '@/lib/graphql/queries/ServerInfoQuery.ts'; +import { CHECK_FOR_SERVER_UPDATES, CHECK_FOR_WEBUI_UPDATE, GET_ABOUT } from '@/lib/graphql/queries/ServerInfoQuery.ts'; import { GET_EXTENSIONS } from '@/lib/graphql/queries/ExtensionQuery.ts'; import { GET_EXTENSIONS_FETCH, @@ -216,6 +223,8 @@ import { GET_SERVER_SETTINGS } from '@/lib/graphql/queries/SettingsQuery.ts'; import { UPDATE_SERVER_SETTINGS } from '@/lib/graphql/mutations/SettingsMutation.ts'; import { BASE_MANGA_FIELDS, FULL_EXTENSION_FIELDS } from '@/lib/graphql/Fragments.ts'; import { CLEAR_SERVER_CACHE } from '@/lib/graphql/mutations/ImageMutation.ts'; +import { RESET_WEBUI_UPDATE_STATUS, UPDATE_WEBUI } from '@/lib/graphql/mutations/ServerInfoMutation.ts'; +import { WEBUI_UPDATE_SUBSCRIPTION } from '@/lib/graphql/subscriptions/ServerInfoSubscription.ts'; enum GQLMethod { QUERY = 'QUERY', @@ -318,7 +327,7 @@ export const SPECIAL_ED_SOURCES = { export class RequestManager { public static readonly API_VERSION = '/api/v1/'; - private readonly graphQLClient = new GraphQLClient(); + public readonly graphQLClient = new GraphQLClient(); private readonly restClient: RestClient = new RestClient(); @@ -862,12 +871,24 @@ export class RequestManager { return this.doRequest(GQLMethod.USE_QUERY, GET_ABOUT, {}, options); } - public useCheckForUpdate( + public useCheckForServerUpdate( options?: QueryHookOptions, ): AbortableApolloUseQueryResponse { return this.doRequest(GQLMethod.USE_QUERY, CHECK_FOR_SERVER_UPDATES, {}, options); } + public useCheckForWebUIUpdate( + options?: QueryHookOptions, + ): AbortableApolloUseQueryResponse { + return this.doRequest(GQLMethod.USE_QUERY, CHECK_FOR_WEBUI_UPDATE, {}, options); + } + + public updateWebUI( + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest(GQLMethod.MUTATION, UPDATE_WEBUI, undefined, options); + } + public useGetExtensionList( options?: QueryHookOptions, ): AbortableApolloUseQueryResponse { @@ -2004,6 +2025,18 @@ export class RequestManager { ): AbortableApolloUseMutationResponse { return this.doRequest(GQLMethod.USE_MUTATION, CLEAR_SERVER_CACHE, { input }, options); } + + public useWebUIUpdateSubscription( + options?: SubscriptionHookOptions, + ): SubscriptionResult { + return this.doRequest(GQLMethod.USE_SUBSCRIPTION, WEBUI_UPDATE_SUBSCRIPTION, undefined, options); + } + + public resetWebUIUpdateStatus( + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest(GQLMethod.MUTATION, RESET_WEBUI_UPDATE_STATUS, undefined, options); + } } export const requestManager = new RequestManager(); diff --git a/src/lib/requests/client/GraphQLClient.ts b/src/lib/requests/client/GraphQLClient.ts index 3fb64f85..d8503526 100644 --- a/src/lib/requests/client/GraphQLClient.ts +++ b/src/lib/requests/client/GraphQLClient.ts @@ -19,7 +19,9 @@ const typePolicies: StrictTypedTypePolicies = { GlobalMetaType: { keyFields: ['key'] }, ExtensionType: { keyFields: ['apkName'] }, AboutServerPayload: { keyFields: [] }, + AboutWebUI: { keyFields: [] }, WebUIUpdateInfo: { keyFields: [] }, + WebUIUpdateCheck: { keyFields: [] }, SettingsType: { keyFields: [] }, Query: { fields: { diff --git a/src/screens/settings/About.tsx b/src/screens/settings/About.tsx index 08728dee..2a87dbef 100644 --- a/src/screens/settings/About.tsx +++ b/src/screens/settings/About.tsx @@ -12,11 +12,175 @@ import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import { useTranslation } from 'react-i18next'; import ListSubheader from '@mui/material/ListSubheader'; -import { Divider } from '@mui/material'; +import { Button, CircularProgress, Divider, Stack, Typography } from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import DownloadIcon from '@mui/icons-material/Download'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { t as translate } from 'i18next'; +import DownloadingIcon from '@mui/icons-material/Downloading'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ListItemLink } from '@/components/util/ListItemLink'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder'; +import { GetAboutQuery, UpdateState } from '@/lib/graphql/generated/graphql.ts'; +import { ABOUT_WEBUI, WEBUI_UPDATE_CHECK } from '@/lib/graphql/Fragments.ts'; +import { makeToast } from '@/components/util/Toast.tsx'; + +type AboutServer = GetAboutQuery['aboutServer']; + +const getVersion = (aboutServer: AboutServer) => { + if (aboutServer.buildType === 'Stable') return `${aboutServer.version}`; + return `${aboutServer.version}-${aboutServer.revision}`; +}; + +const getBuildTime = (aboutServer: AboutServer) => new Date(aboutServer.buildTime * 1000).toUTCString(); + +const getUpdateCheckButtonIcon = ( + isLoading: boolean, + isUpdateAvailable: boolean, + updateState?: UpdateState, + asLink: boolean = false, +) => { + const isUpdateInProgress = updateState === UpdateState.Downloading; + if (isUpdateInProgress) { + return ; + } + + if (isLoading) { + return ; + } + + const isRefreshRequired = !isUpdateAvailable || updateState === UpdateState.Error; + if (isRefreshRequired) { + return ; + } + + return asLink ? : ; +}; + +const getUpdateCheckButtonText = ( + isLoading: boolean, + isUpdateAvailable: boolean, + error: any, + updateState?: UpdateState, + progress: number = 0, +) => { + const isUpdating = updateState === UpdateState.Downloading; + if (isUpdating) { + return translate('global.update.label.updating', { progress }); + } + + const didUpdateFail = updateState === UpdateState.Error; + if (didUpdateFail) { + return translate('global.update.label.update_failure'); + } + + if (isLoading) { + return translate('global.update.label.checking'); + } + + if (error) { + return translate('global.update.label.check_failure'); + } + + if (isUpdateAvailable) { + return translate('global.update.label.available'); + } + + return translate('global.update.label.up_to_date'); +}; + +type BaseVersionInfoProps = { + version: string; + isCheckingForUpdate: boolean; + isUpdateAvailable: boolean; + updateCheckError: any; + checkForUpdate: () => void; +}; + +type LinkVersionInfoProps = { + downloadAsLink: true; + url: string; +}; + +type TriggerVersionInfoProps = { + triggerUpdate: () => void; + updateState: UpdateState; + progress: number; +}; + +type VersionInfoProps = + | (BaseVersionInfoProps & PropertiesNever & LinkVersionInfoProps) + | (BaseVersionInfoProps & TriggerVersionInfoProps & PropertiesNever); + +const VersionInfo = ({ + version, + isCheckingForUpdate, + isUpdateAvailable, + updateCheckError, + checkForUpdate, + triggerUpdate, + updateState, + progress, + downloadAsLink, + url, +}: VersionInfoProps) => { + const isUpdateInProgress = updateState === UpdateState.Downloading; + + const onClick = () => { + if (isUpdateInProgress) { + return; + } + + const shouldCheckForUpdate = !isUpdateAvailable || updateCheckError || updateState === UpdateState.Error; + if (shouldCheckForUpdate) { + checkForUpdate(); + return; + } + + if (isUpdateAvailable) { + triggerUpdate?.(); + } + }; + + return ( + + + {version} + + + + ); +}; export function About() { const { t } = useTranslation(); @@ -27,21 +191,82 @@ export function About() { setAction(null); }, [t]); + useSetDefaultBackTo('settings'); + const { data } = requestManager.useGetAbout(); const { aboutServer, aboutWebUI } = data ?? {}; - useSetDefaultBackTo('settings'); + const { + data: serverUpdateCheckData, + loading: isCheckingForServerUpdate, + refetch: checkForServerUpdate, + error: serverUpdateCheckError, + } = requestManager.useCheckForServerUpdate({ notifyOnNetworkStatusChange: true }); + const { + data: webUIUpdateData, + loading: isCheckingForWebUIUpdate, + refetch: checkForWebUIUpdate, + error: orgWebUIUpdateCheckError, + } = requestManager.useCheckForWebUIUpdate({ notifyOnNetworkStatusChange: true }); + const webUIUpdateCheckError = orgWebUIUpdateCheckError || webUIUpdateData?.checkForWebUIUpdate.tag === ''; + + const { data: webUIUpdateStatusData } = requestManager.useWebUIUpdateSubscription(); + const { state: webUIUpdateState, progress: webUIUpdateProgress } = + webUIUpdateStatusData?.webUIUpdateStatusChange ?? { state: UpdateState.Idle, progress: 0 }; + + useEffect(() => { + const isError = webUIUpdateState === UpdateState.Error; + if (isError) { + makeToast(t('settings.about.webui.label.update_failure'), 'error'); + } + + const updateFinished = webUIUpdateState === UpdateState.Finished; + + const resetUpdateStatus = isError || updateFinished; + if (resetUpdateStatus) { + requestManager.resetWebUIUpdateStatus().response.catch(() => {}); + } + + if (!updateFinished) { + return; + } + + makeToast( + t('settings.about.webui.label.update_success', { + version: webUIUpdateStatusData!.webUIUpdateStatusChange.info.tag, + }), + 'success', + ); + + requestManager.graphQLClient.client.cache.writeFragment({ + fragment: ABOUT_WEBUI, + data: { + __typename: 'AboutWebUI', + channel: webUIUpdateStatusData!.webUIUpdateStatusChange.info.channel, + tag: webUIUpdateStatusData!.webUIUpdateStatusChange.info.tag, + }, + }); + requestManager.graphQLClient.client.cache.writeFragment({ + fragment: WEBUI_UPDATE_CHECK, + data: { + __typename: 'WebUIUpdateCheck', + channel: webUIUpdateStatusData!.webUIUpdateStatusChange.info.channel, + tag: webUIUpdateStatusData!.webUIUpdateStatusChange.info.tag, + updateAvailable: false, + }, + }); + }, [webUIUpdateState]); if (!aboutServer || !aboutWebUI) { return ; } - const version = () => { - if (aboutServer.buildType === 'Stable') return `${aboutServer.version}`; - return `${aboutServer.version}-${aboutServer.revision}`; - }; - - const buildTime = () => new Date(aboutServer.buildTime * 1000).toUTCString(); + const selectedServerChannelInfo = serverUpdateCheckData?.checkForServerUpdates?.find( + (channel) => channel.channel === aboutServer.buildType, + ); + const isServerUpdateAvailable = + !!selectedServerChannelInfo?.tag && selectedServerChannelInfo.tag !== getVersion(aboutServer); + const isWebUIUpdateAvailable = !!webUIUpdateData?.checkForWebUIUpdate.updateAvailable; return ( @@ -60,10 +285,26 @@ export function About() { /> - + + } + /> - + @@ -76,7 +317,21 @@ export function About() { } > - + requestManager.updateWebUI().response.catch(() => {})} + progress={webUIUpdateProgress} + updateState={webUIUpdateState} + /> + } + /> { setting: Setting, value: WebUISettingsType[Setting], ) => { + if (setting === 'webUIChannel') { + requestManager.graphQLClient.client.cache.evict({ fieldName: 'checkForWebUIUpdate' }); + } + mutateSettings({ variables: { input: { settings: { [setting]: value } } } }); }; diff --git a/src/typings.ts b/src/typings.ts index ceff7352..4aab13c8 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -21,22 +21,6 @@ import { 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)[] - ? RecursivePartial[] - : T[P] extends object | undefined - ? RecursivePartial - : T[P]; -}; - -export type OptionalProperty = Omit & Partial>; - type GenericLocation = Omit & { state?: State }; declare module 'react-router-dom' { diff --git a/versionToServerVersionMapping.json b/versionToServerVersionMapping.json index 44e05c95..9ae1bc40 100644 --- a/versionToServerVersionMapping.json +++ b/versionToServerVersionMapping.json @@ -1,6 +1,6 @@ [ { "uiVersion": "PREVIEW", - "serverVersion": "r1431" + "serverVersion": "r1436" } ]