diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts new file mode 100644 index 000000000..d511963fc --- /dev/null +++ b/frontend/src/App/State/AppSectionState.ts @@ -0,0 +1,48 @@ +import SortDirection from 'Helpers/Props/SortDirection'; + +export interface Error { + responseJSON: { + message: string; + }; +} + +export interface AppSectionDeleteState { + isDeleting: boolean; + deleteError: Error; +} + +export interface AppSectionSaveState { + isSaving: boolean; + saveError: Error; +} + +export interface PagedAppSectionState { + pageSize: number; +} + +export interface AppSectionSchemaState { + isSchemaFetching: boolean; + isSchemaPopulated: boolean; + schemaError: Error; + schema: { + items: T[]; + }; +} + +export interface AppSectionItemState { + isFetching: boolean; + isPopulated: boolean; + error: Error; + item: T; +} + +interface AppSectionState { + isFetching: boolean; + isPopulated: boolean; + error: Error; + items: T[]; + sortKey: string; + sortDirection: SortDirection; +} + +export default AppSectionState; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts new file mode 100644 index 000000000..e6f33a849 --- /dev/null +++ b/frontend/src/App/State/AppState.ts @@ -0,0 +1,52 @@ +import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; +import EpisodeFilesAppState from './EpisodeFilesAppState'; +import EpisodesAppState from './EpisodesAppState'; +import QueueAppState from './QueueAppState'; +import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState'; +import SettingsAppState from './SettingsAppState'; +import TagsAppState from './TagsAppState'; + +interface FilterBuilderPropOption { + id: string; + name: string; +} + +export interface FilterBuilderProp { + name: string; + label: string; + type: string; + valueType?: string; + optionsSelector?: (items: T[]) => FilterBuilderPropOption[]; +} + +export interface PropertyFilter { + key: string; + value: boolean | string | number | string[] | number[]; + type: string; +} + +export interface Filter { + key: string; + label: string; + filers: PropertyFilter[]; +} + +export interface CustomFilter { + id: number; + type: string; + label: string; + filers: PropertyFilter[]; +} + +interface AppState { + episodesSelection: EpisodesAppState; + episodeFiles: EpisodeFilesAppState; + interactiveImport: InteractiveImportAppState; + seriesIndex: SeriesIndexAppState; + settings: SettingsAppState; + series: SeriesAppState; + tags: TagsAppState; + queue: QueueAppState; +} + +export default AppState; diff --git a/frontend/src/App/State/ClientSideCollectionAppState.ts b/frontend/src/App/State/ClientSideCollectionAppState.ts new file mode 100644 index 000000000..f4110ef73 --- /dev/null +++ b/frontend/src/App/State/ClientSideCollectionAppState.ts @@ -0,0 +1,8 @@ +import { CustomFilter } from './AppState'; + +interface ClientSideCollectionAppState { + totalItems: number; + customFilters: CustomFilter[]; +} + +export default ClientSideCollectionAppState; diff --git a/frontend/src/App/State/CustomFiltersAppState.ts b/frontend/src/App/State/CustomFiltersAppState.ts new file mode 100644 index 000000000..6ac4820c7 --- /dev/null +++ b/frontend/src/App/State/CustomFiltersAppState.ts @@ -0,0 +1,10 @@ +import AppSectionState, { + AppSectionDeleteState, +} from 'App/State/AppSectionState'; +import { CustomFilter } from './AppState'; + +interface CustomFiltersAppState + extends AppSectionState, + AppSectionDeleteState {} + +export default CustomFiltersAppState; diff --git a/frontend/src/App/State/EpisodeFilesAppState.ts b/frontend/src/App/State/EpisodeFilesAppState.ts new file mode 100644 index 000000000..5e6e94a06 --- /dev/null +++ b/frontend/src/App/State/EpisodeFilesAppState.ts @@ -0,0 +1,10 @@ +import AppSectionState, { + AppSectionDeleteState, +} from 'App/State/AppSectionState'; +import { EpisodeFile } from 'EpisodeFile/EpisodeFile'; + +interface EpisodeFilesAppState + extends AppSectionState, + AppSectionDeleteState {} + +export default EpisodeFilesAppState; diff --git a/frontend/src/App/State/EpisodesAppState.ts b/frontend/src/App/State/EpisodesAppState.ts new file mode 100644 index 000000000..4234c0bcb --- /dev/null +++ b/frontend/src/App/State/EpisodesAppState.ts @@ -0,0 +1,6 @@ +import AppSectionState from 'App/State/AppSectionState'; +import Episode from 'Episode/Episode'; + +type EpisodesAppState = AppSectionState; + +export default EpisodesAppState; diff --git a/frontend/src/App/State/InteractiveImportAppState.ts b/frontend/src/App/State/InteractiveImportAppState.ts new file mode 100644 index 000000000..c65c4abb4 --- /dev/null +++ b/frontend/src/App/State/InteractiveImportAppState.ts @@ -0,0 +1,12 @@ +import AppSectionState from 'App/State/AppSectionState'; +import RecentFolder from 'InteractiveImport/Folder/RecentFolder'; +import ImportMode from '../../InteractiveImport/ImportMode'; +import InteractiveImport from '../../InteractiveImport/InteractiveImport'; + +interface InteractiveImportAppState extends AppSectionState { + originalItems: InteractiveImport[]; + importMode: ImportMode; + recentFolders: RecentFolder[]; +} + +export default InteractiveImportAppState; diff --git a/frontend/src/App/State/QueueAppState.ts b/frontend/src/App/State/QueueAppState.ts new file mode 100644 index 000000000..05fc5a59a --- /dev/null +++ b/frontend/src/App/State/QueueAppState.ts @@ -0,0 +1,53 @@ +import ModelBase from 'App/ModelBase'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; +import AppSectionState, { AppSectionItemState, Error } from './AppSectionState'; + +export interface StatusMessage { + title: string; + messages: string[]; +} + +export interface Queue extends ModelBase { + languages: Language[]; + quality: QualityModel; + customFormats: CustomFormat[]; + size: number; + title: string; + sizeleft: number; + timeleft: string; + estimatedCompletionTime: string; + status: string; + trackedDownloadStatus: string; + trackedDownloadState: string; + statusMessages: StatusMessage[]; + errorMessage: string; + downloadId: string; + protocol: string; + downloadClient: string; + outputPath: string; + episodeHasFile: boolean; + seriesId?: number; + episodeId?: number; + seasonNumber?: number; +} + +export interface QueueDetailsAppState extends AppSectionState { + params: unknown; +} + +export interface QueuePagedAppState extends AppSectionState { + isGrabbing: boolean; + grabError: Error; + isRemoving: boolean; + removeError: Error; +} + +interface QueueAppState { + status: AppSectionItemState; + details: QueueDetailsAppState; + paged: QueuePagedAppState; +} + +export default QueueAppState; diff --git a/frontend/src/App/State/SeriesAppState.ts b/frontend/src/App/State/SeriesAppState.ts new file mode 100644 index 000000000..f9c216bdc --- /dev/null +++ b/frontend/src/App/State/SeriesAppState.ts @@ -0,0 +1,62 @@ +import AppSectionState, { + AppSectionDeleteState, + AppSectionSaveState, +} from 'App/State/AppSectionState'; +import Column from 'Components/Table/Column'; +import SortDirection from 'Helpers/Props/SortDirection'; +import Series from 'Series/Series'; +import { Filter, FilterBuilderProp } from './AppState'; + +export interface SeriesIndexAppState { + sortKey: string; + sortDirection: SortDirection; + secondarySortKey: string; + secondarySortDirection: SortDirection; + view: string; + + posterOptions: { + detailedProgressBar: boolean; + size: string; + showTitle: boolean; + showMonitored: boolean; + showQualityProfile: boolean; + showSearchAction: boolean; + }; + + overviewOptions: { + detailedProgressBar: boolean; + size: string; + showMonitored: boolean; + showNetwork: boolean; + showQualityProfile: boolean; + showPreviousAiring: boolean; + showAdded: boolean; + showSeasonCount: boolean; + showPath: boolean; + showSizeOnDisk: boolean; + showSearchAction: boolean; + }; + + tableOptions: { + showBanners: boolean; + showSearchAction: boolean; + }; + + selectedFilterKey: string; + filterBuilderProps: FilterBuilderProp[]; + filters: Filter[]; + columns: Column[]; +} + +interface SeriesAppState + extends AppSectionState, + AppSectionDeleteState, + AppSectionSaveState { + itemMap: Record; + + deleteOptions: { + addImportListExclusion: boolean; + }; +} + +export default SeriesAppState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts new file mode 100644 index 000000000..20f8a6ad6 --- /dev/null +++ b/frontend/src/App/State/SettingsAppState.ts @@ -0,0 +1,28 @@ +import AppSectionState, { + AppSectionDeleteState, + AppSectionSchemaState, +} from 'App/State/AppSectionState'; +import Language from 'Language/Language'; +import DownloadClient from 'typings/DownloadClient'; +import QualityProfile from 'typings/QualityProfile'; +import { UiSettings } from 'typings/UiSettings'; + +export interface DownloadClientAppState + extends AppSectionState, + AppSectionDeleteState {} + +export interface QualityProfilesAppState + extends AppSectionState, + AppSectionSchemaState {} + +export type LanguageSettingsAppState = AppSectionState; +export type UiSettingsAppState = AppSectionState; + +interface SettingsAppState { + downloadClients: DownloadClientAppState; + language: LanguageSettingsAppState; + uiSettings: UiSettingsAppState; + qualityProfiles: QualityProfilesAppState; +} + +export default SettingsAppState; diff --git a/frontend/src/App/State/TagsAppState.ts b/frontend/src/App/State/TagsAppState.ts new file mode 100644 index 000000000..d1f1d5a2f --- /dev/null +++ b/frontend/src/App/State/TagsAppState.ts @@ -0,0 +1,12 @@ +import ModelBase from 'App/ModelBase'; +import AppSectionState, { + AppSectionDeleteState, +} from 'App/State/AppSectionState'; + +export interface Tag extends ModelBase { + label: string; +} + +interface TagsAppState extends AppSectionState, AppSectionDeleteState {} + +export default TagsAppState; diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts new file mode 100644 index 000000000..45a5beed7 --- /dev/null +++ b/frontend/src/Commands/Command.ts @@ -0,0 +1,37 @@ +import ModelBase from 'App/ModelBase'; + +export interface CommandBody { + sendUpdatesToClient: boolean; + updateScheduledTask: boolean; + completionMessage: string; + requiresDiskAccess: boolean; + isExclusive: boolean; + isLongRunning: boolean; + name: string; + lastExecutionTime: string; + lastStartTime: string; + trigger: string; + suppressMessages: boolean; + seriesId?: number; +} + +interface Command extends ModelBase { + name: string; + commandName: string; + message: string; + body: CommandBody; + priority: string; + status: string; + result: string; + queued: string; + started: string; + ended: string; + duration: string; + trigger: string; + stateChangeTime: string; + sendUpdatesToClient: boolean; + updateScheduledTask: boolean; + lastExecutionTime: string; +} + +export default Command; diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 5a56d34ec..42510e5c3 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -23,7 +23,9 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) { info, } = props; - const [detailedError, setDetailedError] = useState(null); + const [detailedError, setDetailedError] = useState< + StackTrace.StackFrame[] | null + >(null); useEffect(() => { if (error) { diff --git a/frontend/src/Components/Link/Link.tsx b/frontend/src/Components/Link/Link.tsx index 2ad0b2d2c..5015a1fe3 100644 --- a/frontend/src/Components/Link/Link.tsx +++ b/frontend/src/Components/Link/Link.tsx @@ -1,5 +1,10 @@ import classNames from 'classnames'; -import React, { ComponentClass, FunctionComponent, useCallback } from 'react'; +import React, { + ComponentClass, + FunctionComponent, + SyntheticEvent, + useCallback, +} from 'react'; import { Link as RouterLink } from 'react-router-dom'; import styles from './Link.css'; @@ -17,7 +22,7 @@ export interface LinkProps extends React.HTMLProps { target?: string; isDisabled?: boolean; noRouter?: boolean; - onPress?(event: Event): void; + onPress?(event: SyntheticEvent): void; } function Link(props: LinkProps) { const { @@ -33,7 +38,7 @@ function Link(props: LinkProps) { } = props; const onClick = useCallback( - (event) => { + (event: SyntheticEvent) => { if (!isDisabled && onPress) { onPress(event); } @@ -57,6 +62,8 @@ function Link(props: LinkProps) { linkProps.href = to; linkProps.target = target || '_self'; } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore el = RouterLink; linkProps.to = `${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`; linkProps.target = target; diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index 75317f113..a36010749 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -1,5 +1,5 @@ -import React, { forwardRef, ReactNode, useCallback } from 'react'; -import Scroller from 'Components/Scroller/Scroller'; +import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react'; +import Scroller, { OnScroll } from 'Components/Scroller/Scroller'; import ScrollDirection from 'Helpers/Props/ScrollDirection'; import { isLocked } from 'Utilities/scrollLock'; import styles from './PageContentBody.css'; @@ -9,14 +9,11 @@ interface PageContentBodyProps { innerClassName: string; children: ReactNode; initialScrollTop?: number; - onScroll?: (payload) => void; + onScroll?: (payload: OnScroll) => void; } const PageContentBody = forwardRef( - ( - props: PageContentBodyProps, - ref: React.MutableRefObject - ) => { + (props: PageContentBodyProps, ref: ForwardedRef) => { const { className = styles.contentBody, innerClassName = styles.innerContentBody, @@ -26,7 +23,7 @@ const PageContentBody = forwardRef( } = props; const onScrollWrapper = useCallback( - (payload) => { + (payload: OnScroll) => { if (onScroll && !isLocked()) { onScroll(payload); } diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx index 2bcb899aa..37b16eebd 100644 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ b/frontend/src/Components/Scroller/Scroller.tsx @@ -1,9 +1,21 @@ import classNames from 'classnames'; import { throttle } from 'lodash'; -import React, { forwardRef, ReactNode, useEffect, useRef } from 'react'; +import React, { + ForwardedRef, + forwardRef, + MutableRefObject, + ReactNode, + useEffect, + useRef, +} from 'react'; import ScrollDirection from 'Helpers/Props/ScrollDirection'; import styles from './Scroller.css'; +export interface OnScroll { + scrollLeft: number; + scrollTop: number; +} + interface ScrollerProps { className?: string; scrollDirection?: ScrollDirection; @@ -12,11 +24,11 @@ interface ScrollerProps { scrollTop?: number; initialScrollTop?: number; children?: ReactNode; - onScroll?: (payload) => void; + onScroll?: (payload: OnScroll) => void; } const Scroller = forwardRef( - (props: ScrollerProps, ref: React.MutableRefObject) => { + (props: ScrollerProps, ref: ForwardedRef) => { const { className, autoFocus = false, @@ -30,7 +42,7 @@ const Scroller = forwardRef( } = props; const internalRef = useRef(); - const currentRef = ref ?? internalRef; + const currentRef = (ref as MutableRefObject) ?? internalRef; useEffect( () => { diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index f9ff7287c..8c2122c65 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -1,8 +1,10 @@ +import React from 'react'; + interface Column { name: string; - label: string; - columnLabel: string; - isSortable: boolean; + label: string | React.ReactNode; + columnLabel?: string; + isSortable?: boolean; isVisible: boolean; isModifiable?: boolean; } diff --git a/frontend/src/Components/withScrollPosition.tsx b/frontend/src/Components/withScrollPosition.tsx index ec13c6ab8..f688a6253 100644 --- a/frontend/src/Components/withScrollPosition.tsx +++ b/frontend/src/Components/withScrollPosition.tsx @@ -1,24 +1,30 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import scrollPositions from 'Store/scrollPositions'; -function withScrollPosition(WrappedComponent, scrollPositionKey) { - function ScrollPosition(props) { +interface WrappedComponentProps { + initialScrollTop: number; +} + +interface ScrollPositionProps { + history: RouteComponentProps['history']; + location: RouteComponentProps['location']; + match: RouteComponentProps['match']; +} + +function withScrollPosition( + WrappedComponent: React.FC, + scrollPositionKey: string +) { + function ScrollPosition(props: ScrollPositionProps) { const { history } = props; const initialScrollTop = - history.action === 'POP' || - (history.location.state && history.location.state.restoreScrollPosition) - ? scrollPositions[scrollPositionKey] - : 0; + history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0; return ; } - ScrollPosition.propTypes = { - history: PropTypes.object.isRequired, - }; - return ScrollPosition; } diff --git a/frontend/src/EpisodeFile/EpisodeFile.ts b/frontend/src/EpisodeFile/EpisodeFile.ts new file mode 100644 index 000000000..85e545a64 --- /dev/null +++ b/frontend/src/EpisodeFile/EpisodeFile.ts @@ -0,0 +1,20 @@ +import ModelBase from 'App/ModelBase'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; +import MediaInfo from 'typings/MediaInfo'; + +export interface EpisodeFile extends ModelBase { + seriesId: number; + seasonNumber: number; + relativePath: string; + path: string; + size: number; + dateAdded: string; + sceneName: string; + releaseGroup: string; + languages: CustomFormat[]; + quality: QualityModel; + customFormats: CustomFormat[]; + mediaInfo: MediaInfo; + qualityCutoffNotMet: boolean; +} diff --git a/frontend/src/Helpers/Hooks/useSelectState.tsx b/frontend/src/Helpers/Hooks/useSelectState.tsx index 3ec9f2aed..8fb96e42a 100644 --- a/frontend/src/Helpers/Hooks/useSelectState.tsx +++ b/frontend/src/Helpers/Hooks/useSelectState.tsx @@ -5,7 +5,7 @@ import areAllSelected from 'Utilities/Table/areAllSelected'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; -type SelectedState = Record; +export type SelectedState = Record; export interface SelectState { selectedState: SelectedState; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModal.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModal.tsx index bb4fd8165..8b96080b1 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModal.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModal.tsx @@ -7,8 +7,8 @@ import SelectEpisodeModalContent, { interface SelectEpisodeModalProps { isOpen: boolean; selectedIds: number[] | string[]; - seriesId: number; - seasonNumber: number; + seriesId?: number; + seasonNumber?: number; selectedDetails?: string; isAnime: boolean; modalTitle: string; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx index 7e5be5134..b7d08b67c 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import EpisodesAppState from 'App/State/EpisodesAppState'; import TextInput from 'Components/Form/TextInput'; import Button from 'Components/Link/Button'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -14,12 +15,15 @@ import TableBody from 'Components/Table/TableBody'; import Episode from 'Episode/Episode'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds, scrollDirections } from 'Helpers/Props'; +import SortDirection from 'Helpers/Props/SortDirection'; import { clearEpisodes, fetchEpisodes, setEpisodesSort, } from 'Store/Actions/episodeSelectionActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { CheckInputChanged } from 'typings/inputs'; +import { SelectStateInputProps } from 'typings/props'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getSelectedIds from 'Utilities/Table/getSelectedIds'; import SelectEpisodeRow from './SelectEpisodeRow'; @@ -47,7 +51,7 @@ const columns = [ function episodesSelector() { return createSelector( createClientSideCollectionSelector('episodeSelection'), - (episodes) => { + (episodes: EpisodesAppState) => { return episodes; } ); @@ -60,8 +64,8 @@ export interface SelectedEpisode { interface SelectEpisodeModalContentProps { selectedIds: number[] | string[]; - seriesId: number; - seasonNumber: number; + seriesId?: number; + seasonNumber?: number; selectedDetails?: string; isAnime: boolean; sortKey?: string; @@ -100,26 +104,26 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { const filterEpisodeNumber = parseInt(filter); const errorMessage = getErrorMessage(error, 'Unable to load episodes'); const selectedCount = selectedIds.length; - const selectedEpisodesCount = getSelectedIds(selectState).length; + const selectedEpisodesCount = getSelectedIds(selectedState).length; const selectionIsValid = selectedEpisodesCount > 0 && selectedEpisodesCount % selectedCount === 0; const onFilterChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setFilter(value.toLowerCase()); }, [setFilter] ); const onSelectAllChange = useCallback( - ({ value }) => { + ({ value }: CheckInputChanged) => { setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); }, [items, setSelectState] ); const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { + ({ id, value, shiftKey = false }: SelectStateInputProps) => { setSelectState({ type: 'toggleSelected', items, @@ -132,7 +136,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { ); const onSortPress = useCallback( - (newSortKey, newSortDirection) => { + (newSortKey: string, newSortDirection: SortDirection) => { dispatch( setEpisodesSort({ sortKey: newSortKey, @@ -144,9 +148,9 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { ); const onEpisodesSelectWrapper = useCallback(() => { - const episodeIds = getSelectedIds(selectedState); + const episodeIds: number[] = getSelectedIds(selectedState); - const selectedEpisodes = items.reduce((acc, item) => { + const selectedEpisodes = items.reduce((acc: Episode[], item) => { if (episodeIds.indexOf(item.id) > -1) { acc.push(item); } @@ -167,7 +171,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { ); return { - fileId, + fileId: fileId as number, episodes, }; }); diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx index 4f7a9123e..66ce29fa8 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import * as commandNames from 'Commands/commandNames'; import PathInputConnector from 'Components/Form/PathInputConnector'; import Icon from 'Components/Icon'; @@ -18,7 +19,6 @@ import { removeRecentFolder, } from 'Store/Actions/interactiveImportActions'; import translate from 'Utilities/String/translate'; -import RecentFolder from './RecentFolder'; import RecentFolderRow from './RecentFolderRow'; import styles from './InteractiveImportSelectFolderModalContent.css'; @@ -49,9 +49,9 @@ function InteractiveImportSelectFolderModalContent( const { modalTitle, onFolderSelect, onModalClose } = props; const [folder, setFolder] = useState(''); const dispatch = useDispatch(); - const recentFolders: RecentFolder[] = useSelector( + const recentFolders = useSelector( createSelector( - (state) => state.interactiveImport.recentFolders, + (state: AppState) => state.interactiveImport.recentFolders, (recentFolders) => { return recentFolders; } @@ -59,14 +59,14 @@ function InteractiveImportSelectFolderModalContent( ); const onPathChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setFolder(value); }, [setFolder] ); const onRecentPathPress = useCallback( - (value) => { + (value: string) => { setFolder(value); }, [setFolder] @@ -91,8 +91,8 @@ function InteractiveImportSelectFolderModalContent( }, [folder, onFolderSelect, dispatch]); const onRemoveRecentFolderPress = useCallback( - (f) => { - dispatch(removeRecentFolder({ folder: f })); + (folderToRemove: string) => { + dispatch(removeRecentFolder({ folder: folderToRemove })); }, [dispatch] ); diff --git a/frontend/src/InteractiveImport/ImportMode.ts b/frontend/src/InteractiveImport/ImportMode.ts index 0c8ee6472..29d02b892 100644 --- a/frontend/src/InteractiveImport/ImportMode.ts +++ b/frontend/src/InteractiveImport/ImportMode.ts @@ -1,7 +1,3 @@ -enum ImportMode { - Auto = 'auto', - Move = 'move', - Copy = 'copy', -} +type ImportMode = 'auto' | 'move' | 'copy' | 'chooseImportMode'; export default ImportMode; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index 4e6743e36..9af3bb328 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -2,6 +2,8 @@ import { cloneDeep, without } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; import * as commandNames from 'Commands/commandNames'; import SelectInput from 'Components/Form/SelectInput'; import Icon from 'Components/Icon'; @@ -20,16 +22,24 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; +import { EpisodeFile } from 'EpisodeFile/EpisodeFile'; import usePrevious from 'Helpers/Hooks/usePrevious'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { align, icons, kinds, scrollDirections } from 'Helpers/Props'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; +import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; import ImportMode from 'InteractiveImport/ImportMode'; +import InteractiveImport, { + InteractiveImportCommandOptions, +} from 'InteractiveImport/InteractiveImport'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import Series from 'Series/Series'; import { executeCommand } from 'Store/Actions/commandActions'; import { deleteEpisodeFiles, @@ -44,6 +54,8 @@ import { updateInteractiveImportItems, } from 'Store/Actions/interactiveImportActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { SortCallback } from 'typings/callbacks'; +import { SelectStateInputProps } from 'typings/props'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import getSelectedIds from 'Utilities/Table/getSelectedIds'; @@ -59,6 +71,13 @@ type SelectType = | 'quality' | 'language'; +type FilterExistingFiles = 'all' | 'new'; + +// TODO: This feels janky to do, but not sure of a better way currently +type OnSelectedChangeCallback = React.ComponentProps< + typeof InteractiveImportRow +>['onSelectedChange']; + const COLUMNS = [ { name: 'relativePath', @@ -125,25 +144,23 @@ const COLUMNS = [ }, ]; -const filterExistingFilesOptions = { - ALL: 'all', - NEW: 'new', -}; - const importModeOptions = [ { key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true }, { key: 'move', value: 'Move Files' }, { key: 'copy', value: 'Hardlink/Copy Files' }, ]; -function isSameEpisodeFile(file, originalFile) { +function isSameEpisodeFile( + file: InteractiveImport, + originalFile?: InteractiveImport +) { const { series, seasonNumber, episodes } = file; if (!originalFile) { return false; } - if (!originalFile.series || series.id !== originalFile.series.id) { + if (!originalFile.series || series?.id !== originalFile.series.id) { return false; } @@ -155,8 +172,8 @@ function isSameEpisodeFile(file, originalFile) { } const episodeFilesInfoSelector = createSelector( - (state) => state.episodeFiles.isDeleting, - (state) => state.episodeFiles.deleteError, + (state: AppState) => state.episodeFiles.isDeleting, + (state: AppState) => state.episodeFiles.deleteError, (isDeleting, deleteError) => { return { isDeleting, @@ -166,7 +183,7 @@ const episodeFilesInfoSelector = createSelector( ); const importModeSelector = createSelector( - (state) => state.interactiveImport.importMode, + (state: AppState) => state.interactiveImport.importMode, (importMode) => { return importMode; } @@ -178,7 +195,6 @@ interface InteractiveImportModalContentProps { seasonNumber?: number; showSeries?: boolean; allowSeriesChange?: boolean; - autoSelectRow?: boolean; showDelete?: boolean; showImportMode?: boolean; showFilterExistingFiles?: boolean; @@ -200,7 +216,6 @@ function InteractiveImportModalContent( seriesId, seasonNumber, allowSeriesChange = true, - autoSelectRow = true, showSeries = true, showFilterExistingFiles = false, showDelete = false, @@ -221,16 +236,18 @@ function InteractiveImportModalContent( originalItems, sortKey, sortDirection, - } = useSelector(createClientSideCollectionSelector('interactiveImport')); + }: InteractiveImportAppState = useSelector( + createClientSideCollectionSelector('interactiveImport') + ); const { isDeleting, deleteError } = useSelector(episodeFilesInfoSelector); const importMode = useSelector(importModeSelector); - const [invalidRowsSelected, setInvalidRowsSelected] = useState([]); + const [invalidRowsSelected, setInvalidRowsSelected] = useState([]); const [ withoutEpisodeFileIdRowsSelected, setWithoutEpisodeFileIdRowsSelected, - ] = useState([]); + ] = useState([]); const [selectModalOpen, setSelectModalOpen] = useState( null ); @@ -253,16 +270,20 @@ function InteractiveImportModalContent( const dispatch = useDispatch(); const columns: Column[] = useMemo(() => { - const result = cloneDeep(COLUMNS); + const result: Column[] = cloneDeep(COLUMNS); if (!showSeries) { - result.find((c) => c.name === 'series').isVisible = false; + const seriesColumn = result.find((c) => c.name === 'series'); + + if (seriesColumn) { + seriesColumn.isVisible = false; + } } return result; }, [showSeries]); - const selectedIds = useMemo(() => { + const selectedIds: number[] = useMemo(() => { return getSelectedIds(selectedState); }, [selectedState]); @@ -317,13 +338,13 @@ function InteractiveImportModalContent( }, [previousIsDeleting, isDeleting, deleteError, onModalClose]); const onSelectAllChange = useCallback( - ({ value }) => { + ({ value }: SelectStateInputProps) => { setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); }, [items, setSelectState] ); - const onSelectedChange = useCallback( + const onSelectedChange = useCallback( ({ id, value, hasEpisodeFileId, shiftKey = false }) => { setSelectState({ type: 'toggleSelected', @@ -365,7 +386,7 @@ function InteractiveImportModalContent( const onConfirmDelete = useCallback(() => { setIsConfirmDeleteModalOpen(false); - const episodeFileIds = items.reduce((acc, item) => { + const episodeFileIds = items.reduce((acc: number[], item) => { if (selectedIds.indexOf(item.id) > -1 && item.episodeFileId) { acc.push(item.episodeFileId); } @@ -381,11 +402,10 @@ function InteractiveImportModalContent( }, [setIsConfirmDeleteModalOpen]); const onImportSelectedPress = useCallback(() => { - const finalImportMode = - downloadId || !showImportMode ? ImportMode.Auto : importMode; + const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode; - const existingFiles = []; - const files = []; + const existingFiles: Partial[] = []; + const files: InteractiveImportCommandOptions[] = []; if (finalImportMode === 'chooseImportMode') { setInteractiveImportErrorMessage('An import mode must be selected'); @@ -511,16 +531,18 @@ function InteractiveImportModalContent( dispatch, ]); - const onSortPress = useCallback( + const onSortPress = useCallback( (sortKey, sortDirection) => { dispatch(setInteractiveImportSort({ sortKey, sortDirection })); }, [dispatch] ); - const onFilterExistingFilesChange = useCallback( + const onFilterExistingFilesChange = useCallback< + (value: FilterExistingFiles) => void + >( (value) => { - const filter = value !== filterExistingFilesOptions.ALL; + const filter = value !== 'all'; setFilterExistingFiles(filter); @@ -536,14 +558,18 @@ function InteractiveImportModalContent( [downloadId, seriesId, folder, setFilterExistingFiles, dispatch] ); - const onImportModeChange = useCallback( + const onImportModeChange = useCallback< + ({ value }: { value: ImportMode }) => void + >( ({ value }) => { dispatch(setInteractiveImportMode({ importMode: value })); }, [dispatch] ); - const onSelectModalSelect = useCallback( + const onSelectModalSelect = useCallback< + ({ value }: { value: SelectType }) => void + >( ({ value }) => { setSelectModalOpen(value); }, @@ -555,7 +581,7 @@ function InteractiveImportModalContent( }, [setSelectModalOpen]); const onSeriesSelect = useCallback( - (series) => { + (series: Series) => { dispatch( updateInteractiveImportItems({ ids: selectedIds, @@ -573,7 +599,7 @@ function InteractiveImportModalContent( ); const onSeasonSelect = useCallback( - (seasonNumber) => { + (seasonNumber: number) => { dispatch( updateInteractiveImportItems({ ids: selectedIds, @@ -590,7 +616,7 @@ function InteractiveImportModalContent( ); const onEpisodesSelect = useCallback( - (episodes) => { + (episodes: SelectedEpisode[]) => { dispatch( updateInteractiveImportItems({ ids: selectedIds, @@ -606,7 +632,7 @@ function InteractiveImportModalContent( ); const onReleaseGroupSelect = useCallback( - (releaseGroup) => { + (releaseGroup: string) => { dispatch( updateInteractiveImportItems({ ids: selectedIds, @@ -622,7 +648,7 @@ function InteractiveImportModalContent( ); const onLanguagesSelect = useCallback( - (newLanguages) => { + (newLanguages: Language[]) => { dispatch( updateInteractiveImportItems({ ids: selectedIds, @@ -638,7 +664,7 @@ function InteractiveImportModalContent( ); const onQualitySelect = useCallback( - (quality) => { + (quality: QualityModel) => { dispatch( updateInteractiveImportItems({ ids: selectedIds, @@ -653,7 +679,7 @@ function InteractiveImportModalContent( [selectedIds, dispatch] ); - const orderedSelectedIds = items.reduce((acc, file) => { + const orderedSelectedIds = items.reduce((acc: number[], file) => { if (selectedIds.includes(file.id)) { acc.push(file.id); } @@ -690,7 +716,7 @@ function InteractiveImportModalContent( @@ -698,7 +724,7 @@ function InteractiveImportModalContent( @@ -733,7 +759,6 @@ function InteractiveImportModalContent( isSelected={selectedState[item.id]} {...item} allowSeriesChange={allowSeriesChange} - autoSelectRow={autoSelectRow} columns={columns} modalTitle={modalTitle} onSelectedChange={onSelectedChange} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 6184af2d8..763d697cb 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -27,6 +27,7 @@ import { reprocessInteractiveImportItems, updateInteractiveImportItem, } from 'Store/Actions/interactiveImportActions'; +import { SelectStateInputProps } from 'typings/props'; import Rejection from 'typings/Rejection'; import formatBytes from 'Utilities/Number/formatBytes'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; @@ -40,6 +41,10 @@ type SelectType = | 'quality' | 'language'; +type SelectedChangeProps = SelectStateInputProps & { + hasEpisodeFileId: boolean; +}; + interface InteractiveImportRowProps { id: number; allowSeriesChange: boolean; @@ -58,7 +63,7 @@ interface InteractiveImportRowProps { isReprocessing?: boolean; isSelected?: boolean; modalTitle: string; - onSelectedChange(...args: unknown[]): void; + onSelectedChange(result: SelectedChangeProps): void; onValidRowChange(id: number, isValid: boolean): void; } @@ -88,7 +93,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { const dispatch = useDispatch(); const isSeriesColumnVisible = useMemo( - () => columns.find((c) => c.name === 'series').isVisible, + () => columns.find((c) => c.name === 'series')?.isVisible ?? false, [columns] ); @@ -110,6 +115,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { id, hasEpisodeFileId: !!episodeFileId, value: true, + shiftKey: false, }); } }, @@ -143,7 +149,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { ]); const onSelectedChangeWrapper = useCallback( - (result) => { + (result: SelectedChangeProps) => { onSelectedChange({ ...result, hasEpisodeFileId: !!episodeFileId, @@ -158,6 +164,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { id, hasEpisodeFileId: !!episodeFileId, value: true, + shiftKey: false, }); } }, [id, episodeFileId, isSelected, onSelectedChange]); @@ -312,9 +319,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { ); }); + const requiresSeasonNumber = isNaN(Number(seasonNumber)); const showSeriesPlaceholder = isSelected && !series; const showSeasonNumberPlaceholder = - isSelected && !!series && isNaN(seasonNumber) && !isReprocessing; + isSelected && !!series && requiresSeasonNumber && !isReprocessing; const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length; const showReleaseGroupPlaceholder = isSelected && !releaseGroup; @@ -364,9 +372,11 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { @@ -456,7 +466,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { { - setFolderPath(f); + (path: string) => { + setFolderPath(path); }, [setFolderPath] ); diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx index a4231e101..844f4daa9 100644 --- a/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import { LanguageSettingsAppState } from 'App/State/SettingsAppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -25,11 +26,12 @@ interface SelectLanguageModalContentProps { function createFilteredLanguagesSelector() { return createSelector(createLanguagesSelector(), (languages) => { - const { isFetching, isPopulated, error, items } = languages; + const { isFetching, isPopulated, error, items } = + languages as LanguageSettingsAppState; const filterItems = ['Any', 'Original']; const filteredLanguages = items.filter( - (lang) => !filterItems.includes(lang.name) + (lang: Language) => !filterItems.includes(lang.name) ); return { @@ -51,7 +53,7 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) { const [languageIds, setLanguageIds] = useState(props.languageIds); const onLanguageChange = useCallback( - ({ value, name }) => { + ({ name, value }: { name: string; value: boolean }) => { const changedId = parseInt(name); let newLanguages = [...languageIds]; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx index 0ddc4af5c..20fc10d67 100644 --- a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx @@ -1,6 +1,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import { Error } from 'App/State/AppSectionState'; +import AppState from 'App/State/AppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -12,22 +14,32 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; -import { QualityModel } from 'Quality/Quality'; +import Quality, { QualityModel } from 'Quality/Quality'; import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import { CheckInputChanged } from 'typings/inputs'; import getQualities from 'Utilities/Quality/getQualities'; -function createQualitySchemeSelctor() { +interface QualitySchemaState { + isFetching: boolean; + isPopulated: boolean; + error: Error; + items: Quality[]; +} + +function createQualitySchemaSelector() { return createSelector( - (state) => state.settings.qualityProfiles, - (qualityProfiles) => { + (state: AppState) => state.settings.qualityProfiles, + (qualityProfiles): QualitySchemaState => { const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = qualityProfiles; + const items = getQualities(schema.items) as Quality[]; + return { isFetching: isSchemaFetching, isPopulated: isSchemaPopulated, error: schemaError, - items: getQualities(schema.items), + items, }; } ); @@ -50,7 +62,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) { const [real, setReal] = useState(props.real); const { isFetching, isPopulated, error, items } = useSelector( - createQualitySchemeSelctor() + createQualitySchemaSelector() ); const dispatch = useDispatch(); @@ -72,28 +84,28 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) { }, [items]); const onQualityChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setQualityId(parseInt(value)); }, [setQualityId] ); const onProperChange = useCallback( - ({ value }) => { + ({ value }: CheckInputChanged) => { setProper(value); }, [setProper] ); const onRealChange = useCallback( - ({ value }) => { + ({ value }: CheckInputChanged) => { setReal(value); }, [setReal] ); const onQualitySelectWrapper = useCallback(() => { - const quality = items.find((item) => item.id === qualityId); + const quality = items.find((item) => item.id === qualityId) as Quality; const revision = { version: proper ? 2 : 1, diff --git a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.tsx b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.tsx index 708433e96..ed44d2030 100644 --- a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.tsx +++ b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.tsx @@ -25,7 +25,7 @@ function SelectReleaseGroupModalContent( const [releaseGroup, setReleaseGroup] = useState(props.releaseGroup); const onReleaseGroupChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setReleaseGroup(value); }, [setReleaseGroup] diff --git a/frontend/src/InteractiveImport/Season/SelectSeasonModal.tsx b/frontend/src/InteractiveImport/Season/SelectSeasonModal.tsx index 5132fd4ec..3a48bf761 100644 --- a/frontend/src/InteractiveImport/Season/SelectSeasonModal.tsx +++ b/frontend/src/InteractiveImport/Season/SelectSeasonModal.tsx @@ -5,8 +5,8 @@ import SelectSeasonModalContent from './SelectSeasonModalContent'; interface SelectSeasonModalProps { isOpen: boolean; modalTitle: string; - seriesId: number; - onSeasonSelect(seasonNumber): void; + seriesId?: number; + onSeasonSelect(seasonNumber: number): void; onModalClose(): void; } diff --git a/frontend/src/InteractiveImport/Season/SelectSeasonModalContent.tsx b/frontend/src/InteractiveImport/Season/SelectSeasonModalContent.tsx index b11da4320..b0e18a6ef 100644 --- a/frontend/src/InteractiveImport/Season/SelectSeasonModalContent.tsx +++ b/frontend/src/InteractiveImport/Season/SelectSeasonModalContent.tsx @@ -5,20 +5,21 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; +import { Season } from 'Series/Series'; import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector'; import SelectSeasonRow from './SelectSeasonRow'; interface SelectSeasonModalContentProps { - seriesId: number; + seriesId?: number; modalTitle: string; - onSeasonSelect(seasonNumber): void; + onSeasonSelect(seasonNumber: number): void; onModalClose(): void; } function SelectSeasonModalContent(props: SelectSeasonModalContentProps) { const { seriesId, modalTitle, onSeasonSelect, onModalClose } = props; const series = useSelector(createSeriesSelectorForHook(seriesId)); - const seasons = useMemo(() => { + const seasons = useMemo(() => { return series.seasons.slice(0).reverse(); }, [series]); diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx index bfa7ce58a..c3c9a92a5 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx @@ -22,11 +22,11 @@ interface SelectSeriesModalContentProps { function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { const { modalTitle, onSeriesSelect, onModalClose } = props; - const allSeries = useSelector(createAllSeriesSelector()); + const allSeries: Series[] = useSelector(createAllSeriesSelector()); const [filter, setFilter] = useState(''); const onFilterChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setFilter(value); }, [setFilter] @@ -34,7 +34,7 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { const onSeriesSelectWrapper = useCallback( (seriesId: number) => { - const series = allSeries.find((s) => s.id === seriesId); + const series = allSeries.find((s) => s.id === seriesId) as Series; onSeriesSelect(series); }, diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx index ecdecbab2..9e1d294fa 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx @@ -15,6 +15,7 @@ import EpisodeQuality from 'Episode/EpisodeQuality'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; import formatDateTime from 'Utilities/Date/formatDateTime'; import formatAge from 'Utilities/Number/formatAge'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -25,7 +26,11 @@ import ReleaseEpisode from './ReleaseEpisode'; import ReleaseSceneIndicator from './ReleaseSceneIndicator'; import styles from './InteractiveSearchRow.css'; -function getDownloadIcon(isGrabbing, isGrabbed, grabError) { +function getDownloadIcon( + isGrabbing: boolean, + isGrabbed: boolean, + grabError?: string +) { if (isGrabbing) { return icons.SPINNER; } else if (isGrabbed) { @@ -37,7 +42,11 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) { return icons.DOWNLOAD; } -function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { +function getDownloadTooltip( + isGrabbing: boolean, + isGrabbed: boolean, + grabError?: string +) { if (isGrabbing) { return ''; } else if (isGrabbed) { @@ -65,7 +74,7 @@ interface InteractiveSearchRowProps { leechers?: number; quality: QualityModel; languages: Language[]; - customFormats?: object[]; + customFormats: CustomFormat[]; customFormatScore: number; sceneMapping?: object; seasonNumber?: number; diff --git a/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx b/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx index 780346498..b07608f11 100644 --- a/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx +++ b/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx @@ -4,7 +4,7 @@ import styles from './SelectDownloadClientRow.css'; interface SelectSeasonRowProps { id: number; - name: number; + name: string; priority: number; onDownloadClientSelect(downloadClientId: number): unknown; } diff --git a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx index 35669289a..1a2e6514b 100644 --- a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx +++ b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModal.tsx @@ -19,7 +19,7 @@ interface OverrideMatchModalProps { quality: QualityModel; protocol: DownloadProtocol; isGrabbing: boolean; - grabError: string; + grabError?: string; onModalClose(): void; } diff --git a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx index 805d417bb..92a0542e2 100644 --- a/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx +++ b/frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.tsx @@ -12,6 +12,7 @@ import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; +import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; @@ -49,7 +50,7 @@ interface OverrideMatchModalContentProps { quality: QualityModel; protocol: DownloadProtocol; isGrabbing: boolean; - grabError: string; + grabError?: string; onModalClose(): void; } @@ -70,7 +71,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { const [episodes, setEpisodes] = useState(props.episodes); const [languages, setLanguages] = useState(props.languages); const [quality, setQuality] = useState(props.quality); - const [downloadClientId, setDownloadClientId] = useState(null); + const [downloadClientId, setDownloadClientId] = useState(null); const [error, setError] = useState(null); const [selectModalOpen, setSelectModalOpen] = useState( null @@ -137,7 +138,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { }, [setSelectModalOpen]); const onEpisodesSelect = useCallback( - (episodeMap) => { + (episodeMap: SelectedEpisode[]) => { setEpisodes(episodeMap[0].episodes); setSelectModalOpen(null); }, @@ -149,7 +150,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { }, [setSelectModalOpen]); const onQualitySelect = useCallback( - (quality) => { + (quality: QualityModel) => { setQuality(quality); setSelectModalOpen(null); }, @@ -161,7 +162,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { }, [setSelectModalOpen]); const onLanguagesSelect = useCallback( - (languages) => { + (languages: Language[]) => { setLanguages(languages); setSelectModalOpen(null); }, @@ -173,7 +174,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { }, [setSelectModalOpen]); const onDownloadClientSelect = useCallback( - (downloadClientId) => { + (downloadClientId: number) => { setDownloadClientId(downloadClientId); setSelectModalOpen(null); }, @@ -264,7 +265,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { data={ } diff --git a/frontend/src/InteractiveSearch/ReleaseSceneIndicator.tsx b/frontend/src/InteractiveSearch/ReleaseSceneIndicator.tsx index 3f7ad6d9c..8101f793c 100644 --- a/frontend/src/InteractiveSearch/ReleaseSceneIndicator.tsx +++ b/frontend/src/InteractiveSearch/ReleaseSceneIndicator.tsx @@ -9,9 +9,9 @@ import { icons, tooltipPositions } from 'Helpers/Props'; import styles from './ReleaseSceneIndicator.css'; function formatReleaseNumber( - seasonNumber, - episodeNumbers, - absoluteEpisodeNumbers + seasonNumber: number | undefined, + episodeNumbers: number[] | undefined, + absoluteEpisodeNumbers: number[] | undefined ) { if (episodeNumbers && episodeNumbers.length) { if (episodeNumbers.length > 1) { diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index 702abafa0..c3c4acb1b 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -652,7 +652,6 @@ class SeriesDetails extends Component { initialSortDirection={sortDirections.DESCENDING} showSeries={false} allowSeriesChange={false} - autoSelectRow={false} showDelete={true} showImportMode={false} modalTitle={'Manage Episodes'} diff --git a/frontend/src/Series/Details/SeriesDetailsSeason.js b/frontend/src/Series/Details/SeriesDetailsSeason.js index f0e290ead..7eeb60c4c 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeason.js +++ b/frontend/src/Series/Details/SeriesDetailsSeason.js @@ -498,7 +498,6 @@ class SeriesDetailsSeason extends Component { initialSortDirection={sortDirections.DESCENDING} showSeries={false} allowSeriesChange={false} - autoSelectRow={false} showDelete={true} showImportMode={false} modalTitle={'Manage Episodes'} diff --git a/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx b/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx index d3b18de37..aebeac134 100644 --- a/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx +++ b/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx @@ -1,10 +1,18 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import { CustomFilter } from 'App/State/AppState'; import FilterMenu from 'Components/Menu/FilterMenu'; import { align } from 'Helpers/Props'; import SeriesIndexFilterModal from 'Series/Index/SeriesIndexFilterModal'; -function SeriesIndexFilterMenu(props) { +interface SeriesIndexFilterMenuProps { + selectedFilterKey: string | number; + filters: object[]; + customFilters: CustomFilter[]; + isDisabled: boolean; + onFilterSelect(filterName: string): unknown; +} + +function SeriesIndexFilterMenu(props: SeriesIndexFilterMenuProps) { const { selectedFilterKey, filters, @@ -26,15 +34,6 @@ function SeriesIndexFilterMenu(props) { ); } -SeriesIndexFilterMenu.propTypes = { - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - .isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - isDisabled: PropTypes.bool.isRequired, - onFilterSelect: PropTypes.func.isRequired, -}; - SeriesIndexFilterMenu.defaultProps = { showCustomFilters: false, }; diff --git a/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx b/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx index f363e8255..28e81247c 100644 --- a/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx +++ b/frontend/src/Series/Index/Menus/SeriesIndexSortMenu.tsx @@ -1,11 +1,18 @@ -import PropTypes from 'prop-types'; import React from 'react'; import MenuContent from 'Components/Menu/MenuContent'; import SortMenu from 'Components/Menu/SortMenu'; import SortMenuItem from 'Components/Menu/SortMenuItem'; -import { align, sortDirections } from 'Helpers/Props'; +import { align } from 'Helpers/Props'; +import SortDirection from 'Helpers/Props/SortDirection'; + +interface SeriesIndexSortMenuProps { + sortKey?: string; + sortDirection?: SortDirection; + isDisabled: boolean; + onSortSelect(sortKey: string): unknown; +} -function SeriesIndexSortMenu(props) { +function SeriesIndexSortMenu(props: SeriesIndexSortMenuProps) { const { sortKey, sortDirection, isDisabled, onSortSelect } = props; return ( @@ -150,11 +157,4 @@ function SeriesIndexSortMenu(props) { ); } -SeriesIndexSortMenu.propTypes = { - sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), - isDisabled: PropTypes.bool.isRequired, - onSortSelect: PropTypes.func.isRequired, -}; - export default SeriesIndexSortMenu; diff --git a/frontend/src/Series/Index/Menus/SeriesIndexViewMenu.tsx b/frontend/src/Series/Index/Menus/SeriesIndexViewMenu.tsx index 3757d6042..5e5ca7708 100644 --- a/frontend/src/Series/Index/Menus/SeriesIndexViewMenu.tsx +++ b/frontend/src/Series/Index/Menus/SeriesIndexViewMenu.tsx @@ -1,11 +1,16 @@ -import PropTypes from 'prop-types'; import React from 'react'; import MenuContent from 'Components/Menu/MenuContent'; import ViewMenu from 'Components/Menu/ViewMenu'; import ViewMenuItem from 'Components/Menu/ViewMenuItem'; import { align } from 'Helpers/Props'; -function SeriesIndexViewMenu(props) { +interface SeriesIndexViewMenuProps { + view: string; + isDisabled: boolean; + onViewSelect(value: string): unknown; +} + +function SeriesIndexViewMenu(props: SeriesIndexViewMenuProps) { const { view, isDisabled, onViewSelect } = props; return ( @@ -31,10 +36,4 @@ function SeriesIndexViewMenu(props) { ); } -SeriesIndexViewMenu.propTypes = { - view: PropTypes.string.isRequired, - isDisabled: PropTypes.bool.isRequired, - onViewSelect: PropTypes.func.isRequired, -}; - export default SeriesIndexViewMenu; diff --git a/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx b/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx index caf822e29..b3cafa700 100644 --- a/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx +++ b/frontend/src/Series/Index/Overview/Options/SeriesIndexOverviewOptionsModalContent.tsx @@ -45,7 +45,7 @@ function SeriesIndexOverviewOptionsModalContent( const dispatch = useDispatch(); const onOverviewOptionChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: unknown }) => { dispatch(setSeriesOverviewOption({ [name]: value })); }, [dispatch] diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx index b29b74655..b5c4a08fd 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx @@ -10,6 +10,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar'; import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect'; +import { Statistics } from 'Series/Series'; import SeriesPoster from 'Series/SeriesPoster'; import { executeCommand } from 'Store/Actions/commandActions'; import dimensions from 'Styles/Variables/dimensions'; @@ -66,7 +67,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) { previousAiring, added, overview, - statistics = {}, + statistics = {} as Statistics, images, network, } = series; diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx index d8cc83c61..13ecc14c6 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx @@ -1,14 +1,50 @@ +import { IconDefinition } from '@fortawesome/free-regular-svg-icons'; import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { icons } from 'Helpers/Props'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import dimensions from 'Styles/Variables/dimensions'; +import { UiSettings } from 'typings/UiSettings'; import formatDateTime from 'Utilities/Date/formatDateTime'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import formatBytes from 'Utilities/Number/formatBytes'; import SeriesIndexOverviewInfoRow from './SeriesIndexOverviewInfoRow'; import styles from './SeriesIndexOverviewInfo.css'; +interface RowProps { + name: string; + showProp: string; + valueProp: string; +} + +interface RowInfoProps { + title: string; + iconName: IconDefinition; + label: string; +} + +interface SeriesIndexOverviewInfoProps { + height: number; + showNetwork: boolean; + showMonitored: boolean; + showQualityProfile: boolean; + showPreviousAiring: boolean; + showAdded: boolean; + showSeasonCount: boolean; + showPath: boolean; + showSizeOnDisk: boolean; + monitored: boolean; + nextAiring?: string; + network?: string; + qualityProfile: object; + previousAiring?: string; + added?: string; + seasonCount: number; + path: string; + sizeOnDisk?: number; + sortKey: string; +} + const infoRowHeight = parseInt(dimensions.seriesIndexOverviewInfoRowHeight); const rows = [ @@ -54,7 +90,11 @@ const rows = [ }, ]; -function getInfoRowProps(row, props, uiSettings) { +function getInfoRowProps( + row: RowProps, + props: SeriesIndexOverviewInfoProps, + uiSettings: UiSettings +): RowInfoProps | null { const { name } = row; if (name === 'monitored') { @@ -71,7 +111,7 @@ function getInfoRowProps(row, props, uiSettings) { return { title: 'Network', iconName: icons.NETWORK, - label: props.network, + label: props.network ?? '', }; } @@ -79,6 +119,9 @@ function getInfoRowProps(row, props, uiSettings) { return { title: 'Quality Profile', iconName: icons.PROFILE, + // TODO: Type QualityProfile + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ts(2339) label: props.qualityProfile.name, }; } @@ -95,15 +138,11 @@ function getInfoRowProps(row, props, uiSettings) { timeFormat )}`, iconName: icons.CALENDAR, - label: getRelativeDate( - previousAiring, - shortDateFormat, - showRelativeDates, - { + label: + getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: true, - } - ), + }) ?? '', }; } @@ -115,10 +154,11 @@ function getInfoRowProps(row, props, uiSettings) { return { title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`, iconName: icons.ADD, - label: getRelativeDate(added, shortDateFormat, showRelativeDates, { - timeFormat, - timeForToday: true, - }), + label: + getRelativeDate(added, shortDateFormat, showRelativeDates, { + timeFormat, + timeForToday: true, + }) ?? '', }; } @@ -154,28 +194,8 @@ function getInfoRowProps(row, props, uiSettings) { label: formatBytes(props.sizeOnDisk), }; } -} -interface SeriesIndexOverviewInfoProps { - height: number; - showNetwork: boolean; - showMonitored: boolean; - showQualityProfile: boolean; - showPreviousAiring: boolean; - showAdded: boolean; - showSeasonCount: boolean; - showPath: boolean; - showSizeOnDisk: boolean; - monitored: boolean; - nextAiring?: string; - network?: string; - qualityProfile: object; - previousAiring?: string; - added?: string; - seasonCount: number; - path: string; - sizeOnDisk?: number; - sortKey: string; + return null; } function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) { @@ -194,6 +214,8 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) { const { name, showProp, valueProp } = row; const isVisible = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ts(7053) props[valueProp] != null && (props[showProp] || props.sortKey === name); return { @@ -234,6 +256,10 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) { const infoRowProps = getInfoRowProps(row, props, uiSettings); + if (infoRowProps == null) { + return null; + } + return ; })} diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx index 5197533bd..11ab1b7f7 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfoRow.tsx @@ -1,11 +1,12 @@ +import { IconDefinition } from '@fortawesome/free-regular-svg-icons'; import React from 'react'; import Icon from 'Components/Icon'; import styles from './SeriesIndexOverviewInfoRow.css'; interface SeriesIndexOverviewInfoRowProps { title?: string; - iconName: object; - label: string; + iconName?: IconDefinition; + label: string | null; } function SeriesIndexOverviewInfoRow(props: SeriesIndexOverviewInfoRowProps) { diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx index 7d473dc94..a1d0b7076 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx @@ -1,5 +1,5 @@ import { throttle } from 'lodash'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import useMeasure from 'Helpers/Hooks/useMeasure'; @@ -33,11 +33,11 @@ interface RowItemData { interface SeriesIndexOverviewsProps { items: Series[]; - sortKey?: string; + sortKey: string; sortDirection?: string; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: React.MutableRefObject; + scrollerRef: RefObject; isSelectMode: boolean; isSmallScreen: boolean; } @@ -79,7 +79,7 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) { const { size: posterSize, detailedProgressBar } = useSelector( selectOverviewOptions ); - const listRef: React.MutableRefObject = useRef(); + const listRef = useRef(null); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -136,8 +136,8 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) { }, [isSmallScreen, scrollerRef, bounds]); useEffect(() => { - const currentScrollListener = isSmallScreen ? window : scrollerRef.current; - const currentScrollerRef = scrollerRef.current; + const currentScrollerRef = scrollerRef.current as HTMLElement; + const currentScrollListener = isSmallScreen ? window : currentScrollerRef; const handleScroll = throttle(() => { const { offsetTop = 0 } = currentScrollerRef; @@ -146,7 +146,7 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - listRef.current.scrollTo(scrollTop); + listRef.current?.scrollTo(scrollTop); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -175,8 +175,8 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) { scrollTop += offset; } - listRef.current.scrollTo(scrollTop); - scrollerRef.current.scrollTo(0, scrollTop); + listRef.current?.scrollTo(scrollTop); + scrollerRef.current?.scrollTo(0, scrollTop); } } }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); diff --git a/frontend/src/Series/Index/Overview/selectOverviewOptions.ts b/frontend/src/Series/Index/Overview/selectOverviewOptions.ts index 27d0e05fe..7819fb559 100644 --- a/frontend/src/Series/Index/Overview/selectOverviewOptions.ts +++ b/frontend/src/Series/Index/Overview/selectOverviewOptions.ts @@ -1,7 +1,8 @@ import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; const selectOverviewOptions = createSelector( - (state) => state.seriesIndex.overviewOptions, + (state: AppState) => state.seriesIndex.overviewOptions, (overviewOptions) => overviewOptions ); diff --git a/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx b/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx index 8a7d7d153..8bf1439d6 100644 --- a/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx +++ b/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx @@ -42,7 +42,7 @@ function SeriesIndexPosterOptionsModalContent( const dispatch = useDispatch(); const onPosterOptionChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: unknown }) => { dispatch(setSeriesPosterOption({ [name]: value })); }, [dispatch] diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx index 43857c2fc..351cab89b 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx @@ -10,6 +10,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar'; import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect'; +import { Statistics } from 'Series/Series'; import SeriesPoster from 'Series/SeriesPoster'; import { executeCommand } from 'Store/Actions/commandActions'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; @@ -52,7 +53,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { path, titleSlug, nextAiring, - statistics = {}, + statistics = {} as Statistics, images, } = series; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index b54645195..622445999 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -1,8 +1,9 @@ import { throttle } from 'lodash'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import useMeasure from 'Helpers/Hooks/useMeasure'; import SortDirection from 'Helpers/Props/SortDirection'; import SeriesIndexPoster from 'Series/Index/Posters/SeriesIndexPoster'; @@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt( const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); -const ADDITIONAL_COLUMN_COUNT = { +const ADDITIONAL_COLUMN_COUNT: Record = { small: 3, medium: 2, large: 1, @@ -41,17 +42,17 @@ interface CellItemData { interface SeriesIndexPostersProps { items: Series[]; - sortKey?: string; + sortKey: string; sortDirection?: SortDirection; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: React.MutableRefObject; + scrollerRef: RefObject; isSelectMode: boolean; isSmallScreen: boolean; } const seriesIndexSelector = createSelector( - (state) => state.seriesIndex.posterOptions, + (state: AppState) => state.seriesIndex.posterOptions, (posterOptions) => { return { posterOptions, @@ -108,7 +109,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { } = props; const { posterOptions } = useSelector(seriesIndexSelector); - const ref: React.MutableRefObject = useRef(); + const ref = useRef(null); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -210,8 +211,8 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { }, [isSmallScreen, scrollerRef, bounds]); useEffect(() => { - const currentScrollListener = isSmallScreen ? window : scrollerRef.current; - const currentScrollerRef = scrollerRef.current; + const currentScrollerRef = scrollerRef.current as HTMLElement; + const currentScrollListener = isSmallScreen ? window : currentScrollerRef; const handleScroll = throttle(() => { const { offsetTop = 0 } = currentScrollerRef; @@ -220,7 +221,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - ref.current.scrollTo({ scrollLeft: 0, scrollTop }); + ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -243,8 +244,8 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) { const scrollTop = rowIndex * rowHeight + padding; - ref.current.scrollTo({ scrollLeft: 0, scrollTop }); - scrollerRef.current.scrollTo(0, scrollTop); + ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); + scrollerRef.current?.scrollTo(0, scrollTop); } } }, [ diff --git a/frontend/src/Series/Index/Posters/selectPosterOptions.ts b/frontend/src/Series/Index/Posters/selectPosterOptions.ts index 62d3700a7..017bcfd88 100644 --- a/frontend/src/Series/Index/Posters/selectPosterOptions.ts +++ b/frontend/src/Series/Index/Posters/selectPosterOptions.ts @@ -1,7 +1,8 @@ import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; const selectPosterOptions = createSelector( - (state) => state.seriesIndex.posterOptions, + (state: AppState) => state.seriesIndex.posterOptions, (posterOptions) => posterOptions ); diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx index 5fc392d21..1df90cb54 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx @@ -2,6 +2,7 @@ import { orderBy } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; @@ -11,8 +12,10 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; +import Series from 'Series/Series'; import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import { CheckInputChanged } from 'typings/inputs'; import styles from './DeleteSeriesModalContent.css'; interface DeleteSeriesModalContentProps { @@ -21,7 +24,7 @@ interface DeleteSeriesModalContentProps { } const selectDeleteOptions = createSelector( - (state) => state.series.deleteOptions, + (state: AppState) => state.series.deleteOptions, (deleteOptions) => deleteOptions ); @@ -29,28 +32,28 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) { const { seriesIds, onModalClose } = props; const { addImportListExclusion } = useSelector(selectDeleteOptions); - const allSeries = useSelector(createAllSeriesSelector()); + const allSeries: Series[] = useSelector(createAllSeriesSelector()); const dispatch = useDispatch(); const [deleteFiles, setDeleteFiles] = useState(false); - const series = useMemo(() => { - const series = seriesIds.map((id) => { + const series = useMemo((): Series[] => { + const seriesList = seriesIds.map((id) => { return allSeries.find((s) => s.id === id); - }); + }) as Series[]; - return orderBy(series, ['sortTitle']); + return orderBy(seriesList, ['sortTitle']); }, [seriesIds, allSeries]); const onDeleteFilesChange = useCallback( - ({ value }) => { + ({ value }: CheckInputChanged) => { setDeleteFiles(value); }, [setDeleteFiles] ); const onDeleteOptionChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: boolean }) => { dispatch( setDeleteOption({ [name]: value, diff --git a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx index fd9f52265..5eb8bcbf6 100644 --- a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx @@ -54,7 +54,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) { const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false); const save = useCallback( - (moveFiles) => { + (moveFiles: boolean) => { let hasChanges = false; const payload: SavePayload = {}; @@ -102,7 +102,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) { ); const onInputChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: string }) => { switch (name) { case 'monitored': setMonitored(value); diff --git a/frontend/src/Series/Index/Select/Organize/OrganizeSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Organize/OrganizeSeriesModalContent.tsx index f706f2032..3b23446dc 100644 --- a/frontend/src/Series/Index/Select/Organize/OrganizeSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Organize/OrganizeSeriesModalContent.tsx @@ -10,6 +10,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { icons, kinds } from 'Helpers/Props'; +import Series from 'Series/Series'; import { executeCommand } from 'Store/Actions/commandActions'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import styles from './OrganizeSeriesModalContent.css'; @@ -22,13 +23,19 @@ interface OrganizeSeriesModalContentProps { function OrganizeSeriesModalContent(props: OrganizeSeriesModalContentProps) { const { seriesIds, onModalClose } = props; - const allSeries = useSelector(createAllSeriesSelector()); + const allSeries: Series[] = useSelector(createAllSeriesSelector()); const dispatch = useDispatch(); const seriesTitles = useMemo(() => { - const series = seriesIds.map((id) => { - return allSeries.find((s) => s.id === id); - }); + const series = seriesIds.reduce((acc: Series[], id) => { + const s = allSeries.find((s) => s.id === id); + + if (s) { + acc.push(s); + } + + return acc; + }, []); const sorted = orderBy(series, ['sortTitle']); diff --git a/frontend/src/Series/Index/Select/SeasonPass/ChangeMonitoringModalContent.tsx b/frontend/src/Series/Index/Select/SeasonPass/ChangeMonitoringModalContent.tsx index 8fce3f4c8..0ec45ae8b 100644 --- a/frontend/src/Series/Index/Select/SeasonPass/ChangeMonitoringModalContent.tsx +++ b/frontend/src/Series/Index/Select/SeasonPass/ChangeMonitoringModalContent.tsx @@ -32,7 +32,7 @@ function ChangeMonitoringModalContent( const [monitor, setMonitor] = useState(NO_CHANGE); const onInputChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setMonitor(value); }, [setMonitor] diff --git a/frontend/src/Series/Index/Select/SeasonPass/SeasonDetails.tsx b/frontend/src/Series/Index/Select/SeasonPass/SeasonDetails.tsx index ee5348d8c..7c43e2840 100644 --- a/frontend/src/Series/Index/Select/SeasonPass/SeasonDetails.tsx +++ b/frontend/src/Series/Index/Select/SeasonPass/SeasonDetails.tsx @@ -18,7 +18,12 @@ function SeasonDetails(props: SeasonDetailsProps) { return (
{latestSeasons.map((season) => { - const { seasonNumber, monitored, statistics, isSaving } = season; + const { + seasonNumber, + monitored, + statistics, + isSaving = false, + } = season; return ( { - const shiftKey = event.nativeEvent.shiftKey; + (event: SyntheticEvent) => { + const nativeEvent = event.nativeEvent as PointerEvent; + const shiftKey = nativeEvent.shiftKey; selectDispatch({ type: 'toggleSelected', diff --git a/frontend/src/Series/Index/Select/SeriesIndexSelectAllButton.tsx b/frontend/src/Series/Index/Select/SeriesIndexSelectAllButton.tsx index 42b5e8001..f7f30899c 100644 --- a/frontend/src/Series/Index/Select/SeriesIndexSelectAllButton.tsx +++ b/frontend/src/Series/Index/Select/SeriesIndexSelectAllButton.tsx @@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props'; interface SeriesIndexSelectAllButtonProps { label: string; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; } function SeriesIndexSelectAllButton(props: SeriesIndexSelectAllButtonProps) { diff --git a/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx b/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx index ad56f3ed5..63bfbe5ec 100644 --- a/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx +++ b/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { useSelect } from 'App/SelectContext'; +import AppState from 'App/State/AppState'; import { RENAME_SERIES } from 'Commands/commandNames'; import SpinnerButton from 'Components/Link/SpinnerButton'; import PageContentFooter from 'Components/Page/PageContentFooter'; @@ -22,7 +23,7 @@ import TagsModal from './Tags/TagsModal'; import styles from './SeriesIndexSelectFooter.css'; const seriesEditorSelector = createSelector( - (state) => state.series, + (state: AppState) => state.series, (series) => { const { isSaving, isDeleting, deleteError } = series; @@ -71,7 +72,7 @@ function SeriesIndexSelectFooter() { }, [setIsEditModalOpen]); const onSavePress = useCallback( - (payload) => { + (payload: any) => { setIsSavingSeries(true); setIsEditModalOpen(false); @@ -102,7 +103,7 @@ function SeriesIndexSelectFooter() { }, [setIsTagsModalOpen]); const onApplyTagsPress = useCallback( - (tags, applyTags) => { + (tags: number[], applyTags: string) => { setIsSavingTags(true); setIsTagsModalOpen(false); @@ -126,7 +127,7 @@ function SeriesIndexSelectFooter() { }, [setIsMonitoringModalOpen]); const onMonitoringSavePress = useCallback( - (monitor) => { + (monitor: string) => { setIsSavingMonitoring(true); setIsMonitoringModalOpen(false); diff --git a/frontend/src/Series/Index/Select/SeriesIndexSelectModeButton.tsx b/frontend/src/Series/Index/Select/SeriesIndexSelectModeButton.tsx index 314c09c50..2b5e1dd85 100644 --- a/frontend/src/Series/Index/Select/SeriesIndexSelectModeButton.tsx +++ b/frontend/src/Series/Index/Select/SeriesIndexSelectModeButton.tsx @@ -7,7 +7,7 @@ interface SeriesIndexSelectModeButtonProps { label: string; iconName: IconDefinition; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; onPress: () => void; } diff --git a/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx index 6e116c06b..f0f6d28ae 100644 --- a/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx +++ b/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx @@ -1,6 +1,7 @@ -import { concat, uniq } from 'lodash'; +import { uniq } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; +import { Tag } from 'App/State/TagsAppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -12,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import Series from 'Series/Series'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; import styles from './TagsModalContent.css'; @@ -25,29 +27,35 @@ interface TagsModalContentProps { function TagsModalContent(props: TagsModalContentProps) { const { seriesIds, onModalClose, onApplyTagsPress } = props; - const allSeries = useSelector(createAllSeriesSelector()); - const tagList = useSelector(createTagsSelector()); + const allSeries: Series[] = useSelector(createAllSeriesSelector()); + const tagList: Tag[] = useSelector(createTagsSelector()); const [tags, setTags] = useState([]); const [applyTags, setApplyTags] = useState('add'); const seriesTags = useMemo(() => { - const series = seriesIds.map((id) => { - return allSeries.find((s) => s.id === id); - }); + const tags = seriesIds.reduce((acc: number[], id) => { + const s = allSeries.find((s) => s.id === id); - return uniq(concat(...series.map((s) => s.tags))); + if (s) { + acc.push(...s.tags); + } + + return acc; + }, []); + + return uniq(tags); }, [seriesIds, allSeries]); const onTagsChange = useCallback( - ({ value }) => { + ({ value }: { value: number[] }) => { setTags(value); }, [setTags] ); const onApplyTagsChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setApplyTags(value); }, [setApplyTags] diff --git a/frontend/src/Series/Index/SeriesIndex.tsx b/frontend/src/Series/Index/SeriesIndex.tsx index 93657e140..6149b1e92 100644 --- a/frontend/src/Series/Index/SeriesIndex.tsx +++ b/frontend/src/Series/Index/SeriesIndex.tsx @@ -7,6 +7,8 @@ import React, { } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { SelectProvider } from 'App/SelectContext'; +import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; +import SeriesAppState, { SeriesIndexAppState } from 'App/State/SeriesAppState'; import { REFRESH_SERIES, RSS_SYNC } from 'Commands/commandNames'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; @@ -51,7 +53,7 @@ import SeriesIndexTable from './Table/SeriesIndexTable'; import SeriesIndexTableOptions from './Table/SeriesIndexTableOptions'; import styles from './SeriesIndex.css'; -function getViewComponent(view) { +function getViewComponent(view: string) { if (view === 'posters') { return SeriesIndexPosters; } @@ -81,7 +83,8 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { sortKey, sortDirection, view, - } = useSelector(createSeriesClientSideCollectionItemsSelector('seriesIndex')); + }: SeriesAppState & SeriesIndexAppState & ClientSideCollectionAppState = + useSelector(createSeriesClientSideCollectionItemsSelector('seriesIndex')); const isRefreshingSeries = useSelector( createCommandExecutingSelector(REFRESH_SERIES) @@ -91,9 +94,11 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { ); const { isSmallScreen } = useSelector(createDimensionsSelector()); const dispatch = useDispatch(); - const scrollerRef = useRef(); + const scrollerRef = useRef(null); const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false); - const [jumpToCharacter, setJumpToCharacter] = useState(null); + const [jumpToCharacter, setJumpToCharacter] = useState( + undefined + ); const [isSelectMode, setIsSelectMode] = useState(false); useEffect(() => { @@ -122,14 +127,14 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { }, [isSelectMode, setIsSelectMode]); const onTableOptionChange = useCallback( - (payload) => { + (payload: unknown) => { dispatch(setSeriesTableOption(payload)); }, [dispatch] ); const onViewSelect = useCallback( - (value) => { + (value: string) => { dispatch(setSeriesView({ view: value })); if (scrollerRef.current) { @@ -140,14 +145,14 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { ); const onSortSelect = useCallback( - (value) => { + (value: string) => { dispatch(setSeriesSort({ sortKey: value })); }, [dispatch] ); const onFilterSelect = useCallback( - (value) => { + (value: string) => { dispatch(setSeriesFilter({ selectedFilterKey: value })); }, [dispatch] @@ -162,15 +167,15 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { }, [setIsOptionsModalOpen]); const onJumpBarItemPress = useCallback( - (character) => { + (character: string) => { setJumpToCharacter(character); }, [setJumpToCharacter] ); const onScroll = useCallback( - ({ scrollTop }) => { - setJumpToCharacter(null); + ({ scrollTop }: { scrollTop: number }) => { + setJumpToCharacter(undefined); scrollPositions.seriesIndex = scrollTop; }, [setJumpToCharacter] @@ -184,10 +189,10 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { }; } - const characters = items.reduce((acc, item) => { + const characters = items.reduce((acc: Record, item) => { let char = item.sortTitle.charAt(0); - if (!isNaN(char)) { + if (!isNaN(Number(char))) { char = '#'; } @@ -305,6 +310,8 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { state.series.items, + (state: AppState) => state.series.items, (series) => { return series; } @@ -15,14 +16,20 @@ function createSeriesSelector() { function createFilterBuilderPropsSelector() { return createSelector( - (state) => state.seriesIndex.filterBuilderProps, + (state: AppState) => state.seriesIndex.filterBuilderProps, (filterBuilderProps) => { return filterBuilderProps; } ); } -export default function SeriesIndexFilterModal(props) { +interface SeriesIndexFilterModalProps { + isOpen: boolean; +} + +export default function SeriesIndexFilterModal( + props: SeriesIndexFilterModalProps +) { const sectionItems = useSelector(createSeriesSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); const customFilterType = 'series'; @@ -30,7 +37,7 @@ export default function SeriesIndexFilterModal(props) { const dispatch = useDispatch(); const dispatchSetFilter = useCallback( - (payload) => { + (payload: unknown) => { dispatch(setSeriesFilter(payload)); }, [dispatch] @@ -38,6 +45,7 @@ export default function SeriesIndexFilterModal(props) { return ( { + (series: SeriesAppState) => { return series.items.map((s) => { const { monitored, status, statistics } = s; @@ -45,7 +46,9 @@ export default function SeriesIndexFooter() { let totalFileSize = 0; series.forEach((s) => { - const { statistics = {} } = s; + const { + statistics = { episodeCount: 0, episodeFileCount: 0, sizeOnDisk: 0 }, + } = s; const { episodeCount = 0, diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx index 753add771..29ccbe043 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx @@ -17,9 +17,11 @@ import { icons } from 'Helpers/Props'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector'; import createSeriesIndexItemSelector from 'Series/Index/createSeriesIndexItemSelector'; +import { Statistics } from 'Series/Series'; import SeriesBanner from 'Series/SeriesBanner'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import { executeCommand } from 'Store/Actions/commandActions'; +import { SelectStateInputProps } from 'typings/props'; import formatBytes from 'Utilities/Number/formatBytes'; import titleCase from 'Utilities/String/titleCase'; import SeriesIndexProgressBar from '../ProgressBar/SeriesIndexProgressBar'; @@ -58,7 +60,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { nextAiring, previousAiring, added, - statistics = {}, + statistics = {} as Statistics, seasonFolder, images, seriesType, @@ -137,7 +139,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { }, []); const onSelectedChange = useCallback( - ({ id, value, shiftKey }) => { + ({ id, value, shiftKey }: SelectStateInputProps) => { selectDispatch({ type: 'toggleSelected', id, @@ -247,6 +249,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { if (name === 'nextAiring') { return ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ts(2739) ; + scrollerRef: RefObject; isSelectMode: boolean; isSmallScreen: boolean; } const columnsSelector = createSelector( - (state) => state.seriesIndex.columns, + (state: AppState) => state.seriesIndex.columns, (columns) => columns ); @@ -92,7 +93,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) { const columns = useSelector(columnsSelector); const { showBanners } = useSelector(selectTableOptions); - const listRef: React.MutableRefObject = useRef(); + const listRef = useRef>(null); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); const windowWidth = window.innerWidth; @@ -103,7 +104,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) { }, [showBanners]); useEffect(() => { - const current = scrollerRef.current as HTMLElement; + const current = scrollerRef?.current as HTMLElement; if (isSmallScreen) { setSize({ @@ -127,8 +128,8 @@ function SeriesIndexTable(props: SeriesIndexTableProps) { }, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]); useEffect(() => { - const currentScrollListener = isSmallScreen ? window : scrollerRef.current; - const currentScrollerRef = scrollerRef.current; + const currentScrollerRef = scrollerRef.current as HTMLElement; + const currentScrollListener = isSmallScreen ? window : currentScrollerRef; const handleScroll = throttle(() => { const { offsetTop = 0 } = currentScrollerRef; @@ -137,7 +138,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - listRef.current.scrollTo(scrollTop); + listRef.current?.scrollTo(scrollTop); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -166,8 +167,8 @@ function SeriesIndexTable(props: SeriesIndexTableProps) { scrollTop += offset; } - listRef.current.scrollTo(scrollTop); - scrollerRef.current.scrollTo(0, scrollTop); + listRef.current?.scrollTo(scrollTop); + scrollerRef?.current?.scrollTo(0, scrollTop); } } }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx index e1fb295ab..77bfd860a 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx @@ -14,6 +14,7 @@ import { setSeriesSort, setSeriesTableOption, } from 'Store/Actions/seriesIndexActions'; +import { CheckInputChanged } from 'typings/inputs'; import hasGrowableColumns from './hasGrowableColumns'; import SeriesIndexTableOptions from './SeriesIndexTableOptions'; import styles from './SeriesIndexTableHeader.css'; @@ -32,21 +33,21 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) { const [selectState, selectDispatch] = useSelect(); const onSortPress = useCallback( - (value) => { + (value: string) => { dispatch(setSeriesSort({ sortKey: value })); }, [dispatch] ); const onTableOptionChange = useCallback( - (payload) => { + (payload: unknown) => { dispatch(setSeriesTableOption(payload)); }, [dispatch] ); const onSelectAllChange = useCallback( - ({ value }) => { + ({ value }: CheckInputChanged) => { selectDispatch({ type: value ? 'selectAll' : 'unselectAll', }); @@ -94,6 +95,8 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) { { + ({ name, value }: CheckInputChanged) => { onTableOptionChange({ tableOptions: { ...tableOptions, diff --git a/frontend/src/Series/Index/Table/hasGrowableColumns.ts b/frontend/src/Series/Index/Table/hasGrowableColumns.ts index 489f32211..7837122e1 100644 --- a/frontend/src/Series/Index/Table/hasGrowableColumns.ts +++ b/frontend/src/Series/Index/Table/hasGrowableColumns.ts @@ -1,6 +1,8 @@ +import Column from 'Components/Table/Column'; + const growableColumns = ['network', 'qualityProfileId', 'path', 'tags']; -export default function hasGrowableColumns(columns) { +export default function hasGrowableColumns(columns: Column[]) { return columns.some((column) => { const { name, isVisible } = column; diff --git a/frontend/src/Series/Index/Table/selectTableOptions.ts b/frontend/src/Series/Index/Table/selectTableOptions.ts index 47a1517f4..3e9fb428b 100644 --- a/frontend/src/Series/Index/Table/selectTableOptions.ts +++ b/frontend/src/Series/Index/Table/selectTableOptions.ts @@ -1,7 +1,8 @@ import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; const selectTableOptions = createSelector( - (state) => state.seriesIndex.tableOptions, + (state: AppState) => state.seriesIndex.tableOptions, (tableOptions) => tableOptions ); diff --git a/frontend/src/Series/Index/createSeriesIndexItemSelector.ts b/frontend/src/Series/Index/createSeriesIndexItemSelector.ts index 790067ccd..1da42864c 100644 --- a/frontend/src/Series/Index/createSeriesIndexItemSelector.ts +++ b/frontend/src/Series/Index/createSeriesIndexItemSelector.ts @@ -1,6 +1,8 @@ import { maxBy } from 'lodash'; import { createSelector } from 'reselect'; +import Command from 'Commands/Command'; import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames'; +import Series from 'Series/Series'; import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; import createSeriesQualityProfileSelector from 'Store/Selectors/createSeriesQualityProfileSelector'; import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector'; @@ -10,25 +12,16 @@ function createSeriesIndexItemSelector(seriesId: number) { createSeriesSelectorForHook(seriesId), createSeriesQualityProfileSelector(seriesId), createExecutingCommandsSelector(), - (series, qualityProfile, executingCommands) => { - // If a series is deleted this selector may fire before the parent - // selectors, which will result in an undefined series, if that happens - // we want to return early here and again in the render function to avoid - // trying to show a series that has no information available. - - if (!series) { - return {}; - } - + (series: Series, qualityProfile, executingCommands: Command[]) => { const isRefreshingSeries = executingCommands.some((command) => { return ( - command.name === REFRESH_SERIES && command.body.seriesId === series.id + command.name === REFRESH_SERIES && command.body.seriesId === seriesId ); }); const isSearchingSeries = executingCommands.some((command) => { return ( - command.name === SERIES_SEARCH && command.body.seriesId === series.id + command.name === SERIES_SEARCH && command.body.seriesId === seriesId ); }); diff --git a/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts b/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts index 23905e7b2..66143ad2c 100644 --- a/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts +++ b/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; export interface SeriesQueueDetails { count: number; @@ -10,7 +11,7 @@ function createSeriesQueueDetailsSelector( seasonNumber?: number ) { return createSelector( - (state) => state.queue.details.items, + (state: AppState) => state.queue.details.items, (queueItems) => { return queueItems.reduce( (acc: SeriesQueueDetails, item) => { diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index cecc8086f..32b584cc0 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -14,6 +14,7 @@ export interface Language { } export interface Statistics { + seasonCount: number; episodeCount: number; episodeFileCount: number; percentOfEpisodes: number; @@ -41,11 +42,12 @@ export interface AlternateTitle { } interface Series extends ModelBase { - added: Date; + added: string; alternateTitles: AlternateTitle[]; + certification: string; cleanTitle: string; ended: boolean; - firstAired: Date; + firstAired: string; genres: string[]; images: Image[]; imdbId: string; @@ -54,7 +56,8 @@ interface Series extends ModelBase { originalLanguage: Language; overview: string; path: string; - previousAiring: Date; + previousAiring?: string; + nextAiring?: string; qualityProfileId: number; ratings: Ratings; rootFolderPath: string; @@ -73,6 +76,7 @@ interface Series extends ModelBase { tvRageId: number; useSceneNumbering: boolean; year: number; + isSaving?: boolean; } export default Series; diff --git a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts index fc832643c..ac31e5210 100644 --- a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts +++ b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect'; +import { DownloadClientAppState } from 'App/State/SettingsAppState'; import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import sortByName from 'Utilities/Array/sortByName'; @@ -8,7 +9,7 @@ export default function createEnabledDownloadClientsSelector( ) { return createSelector( createSortedSectionSelector('settings.downloadClients', sortByName), - (downloadClients) => { + (downloadClients: DownloadClientAppState) => { const { isFetching, isPopulated, error, items } = downloadClients; const clients = items.filter( diff --git a/frontend/src/Store/Selectors/createSeriesSelector.js b/frontend/src/Store/Selectors/createSeriesSelector.js index c9f0f6e43..5cdf8becd 100644 --- a/frontend/src/Store/Selectors/createSeriesSelector.js +++ b/frontend/src/Store/Selectors/createSeriesSelector.js @@ -5,6 +5,7 @@ export function createSeriesSelectorForHook(seriesId) { (state) => state.series.itemMap, (state) => state.series.items, (itemMap, allSeries) => { + return seriesId ? allSeries[itemMap[seriesId]]: undefined; } ); diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js deleted file mode 100644 index 39648c008..000000000 --- a/frontend/src/Store/scrollPositions.js +++ /dev/null @@ -1,5 +0,0 @@ -const scrollPositions = { - seriesIndex: 0 -}; - -export default scrollPositions; diff --git a/frontend/src/Store/scrollPositions.ts b/frontend/src/Store/scrollPositions.ts new file mode 100644 index 000000000..9b7dd57b6 --- /dev/null +++ b/frontend/src/Store/scrollPositions.ts @@ -0,0 +1,5 @@ +const scrollPositions: Record = { + seriesIndex: 0, +}; + +export default scrollPositions; diff --git a/frontend/src/Utilities/Table/getSelectedIds.js b/frontend/src/Utilities/Table/getSelectedIds.js deleted file mode 100644 index 705f13a5d..000000000 --- a/frontend/src/Utilities/Table/getSelectedIds.js +++ /dev/null @@ -1,15 +0,0 @@ -import _ from 'lodash'; - -function getSelectedIds(selectedState, { parseIds = true } = {}) { - return _.reduce(selectedState, (result, value, id) => { - if (value) { - const parsedId = parseIds ? parseInt(id) : id; - - result.push(parsedId); - } - - return result; - }, []); -} - -export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/getSelectedIds.ts b/frontend/src/Utilities/Table/getSelectedIds.ts new file mode 100644 index 000000000..13815b105 --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.ts @@ -0,0 +1,24 @@ +import { reduce } from 'lodash'; +import { SelectedState } from 'Helpers/Hooks/useSelectState'; + +// TODO: This needs to handle string IDs as well +function getSelectedIds( + selectedState: SelectedState, + { parseIds = true } = {} +): number[] { + return reduce( + selectedState, + (result: any[], value, id) => { + if (value) { + const parsedId = parseIds ? parseInt(id) : id; + + result.push(parsedId); + } + + return result; + }, + [] + ); +} + +export default getSelectedIds; diff --git a/frontend/src/typings/CustomFormat.ts b/frontend/src/typings/CustomFormat.ts new file mode 100644 index 000000000..7cef9f6ef --- /dev/null +++ b/frontend/src/typings/CustomFormat.ts @@ -0,0 +1,12 @@ +export interface QualityProfileFormatItem { + format: number; + name: string; + score: number; +} + +interface CustomFormat { + id: number; + name: string; +} + +export default CustomFormat; diff --git a/frontend/src/typings/MediaInfo.ts b/frontend/src/typings/MediaInfo.ts new file mode 100644 index 000000000..55b27de4a --- /dev/null +++ b/frontend/src/typings/MediaInfo.ts @@ -0,0 +1,19 @@ +interface MediaInfo { + audioBitrate: number; + audioChannels: number; + audioCodec: string; + audioLanguages: string; + audioStreamCount: number; + videoBitDepth: number; + videoBitrate: number; + videoCodec: string; + videoFps: number; + videoDynamicRange: string; + videoDynamicRangeType: string; + resolution: string; + runTime: string; + scanType: string; + subtitles: string; +} + +export default MediaInfo; diff --git a/frontend/src/typings/QualityProfile.ts b/frontend/src/typings/QualityProfile.ts new file mode 100644 index 000000000..ec4e46648 --- /dev/null +++ b/frontend/src/typings/QualityProfile.ts @@ -0,0 +1,23 @@ +import Quality from 'Quality/Quality'; +import { QualityProfileFormatItem } from './CustomFormat'; + +export interface QualityProfileQualityItem { + id?: number; + quality?: Quality; + items: QualityProfileQualityItem[]; + allowed: boolean; + name?: string; +} + +interface QualityProfile { + name: string; + upgradeAllowed: boolean; + cutoff: number; + items: QualityProfileQualityItem[]; + minFormatScore: number; + cutoffFormatScore: number; + formatItems: QualityProfileFormatItem[]; + id: number; +} + +export default QualityProfile; diff --git a/frontend/src/typings/UiSettings.ts b/frontend/src/typings/UiSettings.ts new file mode 100644 index 000000000..79cb0f333 --- /dev/null +++ b/frontend/src/typings/UiSettings.ts @@ -0,0 +1,6 @@ +export interface UiSettings { + showRelativeDates: boolean; + shortDateFormat: string; + longDateFormat: string; + timeFormat: string; +} diff --git a/frontend/src/typings/callbacks.ts b/frontend/src/typings/callbacks.ts new file mode 100644 index 000000000..0114efeb0 --- /dev/null +++ b/frontend/src/typings/callbacks.ts @@ -0,0 +1,6 @@ +import SortDirection from 'Helpers/Props/SortDirection'; + +export type SortCallback = ( + sortKey: string, + sortDirection: SortDirection +) => void; diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts new file mode 100644 index 000000000..c0fda305c --- /dev/null +++ b/frontend/src/typings/inputs.ts @@ -0,0 +1,4 @@ +export type CheckInputChanged = { + name: string; + value: boolean; +}; diff --git a/frontend/src/typings/props.ts b/frontend/src/typings/props.ts new file mode 100644 index 000000000..5b87e36b3 --- /dev/null +++ b/frontend/src/typings/props.ts @@ -0,0 +1,5 @@ +export interface SelectStateInputProps { + id: number; + value: boolean; + shiftKey: boolean; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index dfddb15a3..354e2a5aa 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -7,7 +7,15 @@ "jsx": "react", "module": "commonjs", "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "strict": true, "esModuleInterop": true, "typeRoots": ["node_modules/@types", "typings"], "paths": { diff --git a/package.json b/package.json index 3d6dd5068..e6df75dac 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,9 @@ "@babel/preset-env": "7.18.0", "@babel/preset-react": "7.17.12", "@babel/preset-typescript": "7.18.6", + "@types/lodash": "4.14.192", + "@types/react-router-dom": "5.3.3", + "@types/react-text-truncate": "0.14.1", "@types/react-window": "1.8.5", "@typescript-eslint/eslint-plugin": "5.48.1", "@typescript-eslint/parser": "5.48.0", diff --git a/yarn.lock b/yarn.lock index 76090bb5a..ccf6c4c33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1422,6 +1422,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -1472,6 +1477,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@4.14.192": + version "4.14.192" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285" + integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A== + "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -1524,6 +1534,30 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" +"@types/react-router-dom@5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + +"@types/react-text-truncate@0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28" + integrity sha512-yCtOOOJzrsfWF6TbnTDZz0gM5JYOxJmewExaTJTv01E7yrmpkNcmVny2fAtsNgSFCp8k2VgCePBoIvFBpKyEOw== + dependencies: + "@types/react" "*" + "@types/react-window@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"