From efe0a3d283e93cb1d14c5408b940b9a7e84081b0 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 4 Apr 2023 09:21:34 -0700 Subject: [PATCH] Typings cleanup and improvements Appease linter (cherry picked from commit b2c43fb2a67965d68d3d35b72302b0cddb5aca23) (cherry picked from commit 3b5e83670b844cf7c20bf7d744d9fbc96fde6902) Closes #3516 Closes #3510 Closes #2778 --- frontend/src/Artist/Index/ArtistIndex.tsx | 31 +++-- .../Artist/Index/ArtistIndexFilterModal.tsx | 11 +- .../Index/Banners/ArtistIndexBanner.tsx | 2 +- .../Index/Banners/ArtistIndexBanners.tsx | 23 ++-- .../ArtistIndexBannerOptionsModalContent.tsx | 2 +- .../Index/Menus/ArtistIndexFilterMenu.tsx | 21 ++-- .../Index/Menus/ArtistIndexSortMenu.tsx | 49 ++++---- .../Index/Menus/ArtistIndexViewMenu.tsx | 15 ++- .../Overview/ArtistIndexOverviewInfo.tsx | 110 +++++++++++------- .../Overview/ArtistIndexOverviewInfoRow.tsx | 5 +- .../Index/Overview/ArtistIndexOverviews.tsx | 18 +-- ...ArtistIndexOverviewOptionsModalContent.tsx | 2 +- .../Index/Posters/ArtistIndexPoster.tsx | 2 +- .../Index/Posters/ArtistIndexPosters.tsx | 23 ++-- .../ArtistIndexPosterOptionsModalContent.tsx | 2 +- .../Index/Select/AlbumStudio/AlbumDetails.tsx | 2 +- .../Select/AlbumStudio/AlbumStudioAlbum.tsx | 2 +- .../ChangeMonitoringModalContent.tsx | 2 +- .../Index/Select/ArtistIndexPosterSelect.tsx | 7 +- .../Select/ArtistIndexSelectAllButton.tsx | 2 +- .../Index/Select/ArtistIndexSelectFooter.tsx | 12 +- .../Select/ArtistIndexSelectModeButton.tsx | 2 +- .../AudioTags/RetagArtistModalContent.tsx | 12 +- .../Delete/DeleteArtistModalContent.tsx | 11 +- .../Select/Edit/EditArtistModalContent.tsx | 4 +- .../Index/Select/Tags/TagsModalContent.tsx | 7 +- .../src/Artist/Index/Table/ArtistIndexRow.tsx | 9 +- .../Artist/Index/Table/ArtistIndexTable.tsx | 23 ++-- .../Index/Table/ArtistIndexTableHeader.tsx | 9 +- .../Index/Table/ArtistIndexTableOptions.tsx | 3 +- .../Index/createArtistIndexItemSelector.ts | 21 ++-- frontend/src/Commands/Command.ts | 37 ++++++ .../src/Components/Page/PageContentBody.tsx | 13 +-- frontend/src/Components/Scroller/Scroller.tsx | 20 +++- frontend/src/Components/Table/VirtualTable.js | 19 ++- .../src/Components/withScrollPosition.tsx | 28 +++-- frontend/src/Store/scrollPositions.js | 5 - frontend/src/Store/scrollPositions.ts | 5 + frontend/src/Store/thunks.js | 27 ----- frontend/src/Store/thunks.ts | 39 +++++++ .../src/Utilities/Table/getSelectedIds.js | 15 --- .../src/Utilities/Table/getSelectedIds.ts | 18 +++ frontend/src/typings/callbacks.ts | 6 + frontend/src/typings/inputs.ts | 4 + frontend/tsconfig.json | 8 ++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 46 files changed, 420 insertions(+), 269 deletions(-) create mode 100644 frontend/src/Commands/Command.ts delete mode 100644 frontend/src/Store/scrollPositions.js create mode 100644 frontend/src/Store/scrollPositions.ts delete mode 100644 frontend/src/Store/thunks.js create mode 100644 frontend/src/Store/thunks.ts delete mode 100644 frontend/src/Utilities/Table/getSelectedIds.js create mode 100644 frontend/src/Utilities/Table/getSelectedIds.ts create mode 100644 frontend/src/typings/callbacks.ts create mode 100644 frontend/src/typings/inputs.ts diff --git a/frontend/src/Artist/Index/ArtistIndex.tsx b/frontend/src/Artist/Index/ArtistIndex.tsx index 3143ff725..2fcc0fadf 100644 --- a/frontend/src/Artist/Index/ArtistIndex.tsx +++ b/frontend/src/Artist/Index/ArtistIndex.tsx @@ -7,6 +7,8 @@ import React, { } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { SelectProvider } from 'App/SelectContext'; +import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState'; +import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; import NoArtist from 'Artist/NoArtist'; import { RSS_SYNC } from 'Commands/commandNames'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -89,16 +91,19 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => { sortKey, sortDirection, view, - } = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex')); + }: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState = + useSelector(createArtistClientSideCollectionItemsSelector('artistIndex')); const isRssSyncExecuting = useSelector( createCommandExecutingSelector(RSS_SYNC) ); 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(() => { @@ -118,14 +123,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => { }, [isSelectMode, setIsSelectMode]); const onTableOptionChange = useCallback( - (payload) => { + (payload: unknown) => { dispatch(setArtistTableOption(payload)); }, [dispatch] ); const onViewSelect = useCallback( - (value) => { + (value: string) => { dispatch(setArtistView({ view: value })); if (scrollerRef.current) { @@ -136,14 +141,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => { ); const onSortSelect = useCallback( - (value) => { + (value: string) => { dispatch(setArtistSort({ sortKey: value })); }, [dispatch] ); const onFilterSelect = useCallback( - (value) => { + (value: string) => { dispatch(setArtistFilter({ selectedFilterKey: value })); }, [dispatch] @@ -158,15 +163,15 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => { }, [setIsOptionsModalOpen]); const onJumpBarItemPress = useCallback( - (character) => { + (character: string) => { setJumpToCharacter(character); }, [setJumpToCharacter] ); const onScroll = useCallback( - ({ scrollTop }) => { - setJumpToCharacter(null); + ({ scrollTop }: { scrollTop: number }) => { + setJumpToCharacter(undefined); scrollPositions.artistIndex = scrollTop; }, [setJumpToCharacter] @@ -180,10 +185,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => { }; } - const characters = items.reduce((acc, item) => { + const characters = items.reduce((acc: Record, item) => { let char = item.sortName.charAt(0); - if (!isNaN(char)) { + if (!isNaN(Number(char))) { char = '#'; } @@ -300,6 +305,8 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => { { + (payload: unknown) => { dispatch(setArtistFilter(payload)); }, [dispatch] @@ -39,6 +45,7 @@ export default function ArtistIndexFilterModal(props) { return ( ) : null} - {showQualityProfile ? ( + {showQualityProfile && !!qualityProfile?.name ? (
{qualityProfile.name}
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.tsx b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.tsx index 50c0f3f9a..3582da097 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.tsx +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.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 Artist from 'Artist/Artist'; import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner'; import useMeasure from 'Helpers/Hooks/useMeasure'; @@ -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 ArtistIndexBannersProps { items: Artist[]; - sortKey?: string; + sortKey: string; sortDirection?: SortDirection; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: React.MutableRefObject; + scrollerRef: RefObject; isSelectMode: boolean; isSmallScreen: boolean; } const artistIndexSelector = createSelector( - (state) => state.artistIndex.bannerOptions, + (state: AppState) => state.artistIndex.bannerOptions, (bannerOptions) => { return { bannerOptions, @@ -108,7 +109,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) { } = props; const { bannerOptions } = useSelector(artistIndexSelector); - const ref: React.MutableRefObject = useRef(); + const ref = useRef(null); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -222,8 +223,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) { }, [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; @@ -232,7 +233,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - ref.current.scrollTo({ scrollLeft: 0, scrollTop }); + ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -255,8 +256,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) { 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/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.tsx b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.tsx index f75311bca..f889ea450 100644 --- a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.tsx +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.tsx @@ -59,7 +59,7 @@ function ArtistIndexBannerOptionsModalContent( const dispatch = useDispatch(); const onBannerOptionChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: unknown }) => { dispatch(setArtistBannerOption({ [name]: value })); }, [dispatch] diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.tsx b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.tsx index 19be069e5..91ebbef2d 100644 --- a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.tsx +++ b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.tsx @@ -1,10 +1,18 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import { CustomFilter } from 'App/State/AppState'; import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal'; import FilterMenu from 'Components/Menu/FilterMenu'; import { align } from 'Helpers/Props'; -function ArtistIndexFilterMenu(props) { +interface ArtistIndexFilterMenuProps { + selectedFilterKey: string | number; + filters: object[]; + customFilters: CustomFilter[]; + isDisabled: boolean; + onFilterSelect(filterName: string): unknown; +} + +function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) { const { selectedFilterKey, filters, @@ -26,15 +34,6 @@ function ArtistIndexFilterMenu(props) { ); } -ArtistIndexFilterMenu.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, -}; - ArtistIndexFilterMenu.defaultProps = { showCustomFilters: false, }; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx index 3d4b8ded0..9bae4dabf 100644 --- a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx +++ b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx @@ -1,11 +1,19 @@ -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'; +import translate from 'Utilities/String/translate'; + +interface SeriesIndexSortMenuProps { + sortKey?: string; + sortDirection?: SortDirection; + isDisabled: boolean; + onSortSelect(sortKey: string): unknown; +} -function ArtistIndexSortMenu(props) { +function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) { const { sortKey, sortDirection, isDisabled, onSortSelect } = props; return ( @@ -17,7 +25,7 @@ function ArtistIndexSortMenu(props) { sortDirection={sortDirection} onPress={onSortSelect} > - Monitored/Status + {translate('MonitoredStatus')} - Name + {translate('Name')} - Type + {translate('Type')} - Quality Profile + {translate('QualityProfile')} - Metadata Profile + {translate('MetadataProfile')} - Next Album + {translate('NextAlbum')} - Last Album + {translate('Last Album')} - Added + {translate('Added')} - Albums + {translate('Albums')} - Tracks + {translate('Tracks')} - Track Count + {translate('TrackCount')} - Path + {translate('Path')} - Size on Disk + {translate('SizeOnDisk')} - Tags + {translate('Tags')} ); } -ArtistIndexSortMenu.propTypes = { - sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), - isDisabled: PropTypes.bool.isRequired, - onSortSelect: PropTypes.func.isRequired, -}; - export default ArtistIndexSortMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.tsx b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.tsx index f4be32db3..bb88d9149 100644 --- a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.tsx +++ b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import MenuContent from 'Components/Menu/MenuContent'; import ViewMenu from 'Components/Menu/ViewMenu'; @@ -6,7 +5,13 @@ import ViewMenuItem from 'Components/Menu/ViewMenuItem'; import { align } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; -function ArtistIndexViewMenu(props) { +interface ArtistIndexViewMenuProps { + view: string; + isDisabled: boolean; + onViewSelect(value: string): unknown; +} + +function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) { const { view, isDisabled, onViewSelect } = props; return ( @@ -36,10 +41,4 @@ function ArtistIndexViewMenu(props) { ); } -ArtistIndexViewMenu.propTypes = { - view: PropTypes.string.isRequired, - isDisabled: PropTypes.bool.isRequired, - onViewSelect: PropTypes.func.isRequired, -}; - export default ArtistIndexViewMenu; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.tsx index 33c4ad2a9..fd3263893 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.tsx +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.tsx @@ -1,15 +1,51 @@ +import { IconDefinition } from '@fortawesome/free-regular-svg-icons'; import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import Album from 'Album/Album'; import { icons } from 'Helpers/Props'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import dimensions from 'Styles/Variables/dimensions'; +import QualityProfile from 'typings/QualityProfile'; +import { UiSettings } from 'typings/UiSettings'; import formatDateTime from 'Utilities/Date/formatDateTime'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow'; import styles from './ArtistIndexOverviewInfo.css'; +interface RowProps { + name: string; + showProp: string; + valueProp: string; +} + +interface RowInfoProps { + title: string; + iconName: IconDefinition; + label: string; +} + +interface ArtistIndexOverviewInfoProps { + height: number; + showMonitored: boolean; + showQualityProfile: boolean; + showLastAlbum: boolean; + showAdded: boolean; + showAlbumCount: boolean; + showPath: boolean; + showSizeOnDisk: boolean; + monitored: boolean; + nextAlbum?: Album; + qualityProfile?: QualityProfile; + lastAlbum?: Album; + added?: string; + albumCount: number; + path: string; + sizeOnDisk?: number; + sortKey: string; +} + const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight); const rows = [ @@ -50,11 +86,17 @@ const rows = [ }, ]; -function getInfoRowProps(row, props, uiSettings) { +function getInfoRowProps( + row: RowProps, + props: ArtistIndexOverviewInfoProps, + uiSettings: UiSettings +): RowInfoProps | null { const { name } = row; if (name === 'monitored') { - const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored'; + const monitoredText = props.monitored + ? translate('Monitored') + : translate('Unmonitored'); return { title: monitoredText, @@ -63,9 +105,9 @@ function getInfoRowProps(row, props, uiSettings) { }; } - if (name === 'qualityProfileId') { + if (name === 'qualityProfileId' && !!props.qualityProfile?.name) { return { - title: 'Quality Profile', + title: translate('QualityProfile'), iconName: icons.PROFILE, label: props.qualityProfile.name, }; @@ -78,15 +120,16 @@ function getInfoRowProps(row, props, uiSettings) { return { title: `Last Album: ${lastAlbum.title}`, iconName: icons.CALENDAR, - label: getRelativeDate( - lastAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - ), + label: + getRelativeDate( + lastAlbum.releaseDate, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true, + } + ) ?? '', }; } @@ -98,10 +141,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, + }) ?? '', }; } @@ -116,7 +160,7 @@ function getInfoRowProps(row, props, uiSettings) { } return { - title: 'Album Count', + title: translate('AlbumCount'), iconName: icons.CIRCLE, label: albums, }; @@ -124,7 +168,7 @@ function getInfoRowProps(row, props, uiSettings) { if (name === 'path') { return { - title: 'Path', + title: translate('Path'), iconName: icons.FOLDER, label: props.path, }; @@ -132,31 +176,13 @@ function getInfoRowProps(row, props, uiSettings) { if (name === 'sizeOnDisk') { return { - title: 'Size on Disk', + title: translate('SizeOnDisk'), iconName: icons.DRIVE, label: formatBytes(props.sizeOnDisk), }; } -} -interface ArtistIndexOverviewInfoProps { - height: number; - showMonitored: boolean; - showQualityProfile: boolean; - showLastAlbum: boolean; - showAdded: boolean; - showAlbumCount: boolean; - showPath: boolean; - showSizeOnDisk: boolean; - monitored: boolean; - nextAlbum?: Album; - qualityProfile: object; - lastAlbum?: Album; - added?: string; - albumCount: number; - path: string; - sizeOnDisk?: number; - sortKey: string; + return null; } function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) { @@ -175,6 +201,8 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) { 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 { @@ -219,6 +247,10 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) { const infoRowProps = getInfoRowProps(row, props, uiSettings); + if (infoRowProps == null) { + return null; + } + return ; })} diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx index 931d7053c..5d9b4a069 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.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 './ArtistIndexOverviewInfoRow.css'; interface ArtistIndexOverviewInfoRowProps { title?: string; - iconName: object; - label: string; + iconName?: IconDefinition; + label: string | null; } function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) { diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx index 3f1197ef4..11285c1b3 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.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 Artist from 'Artist/Artist'; @@ -33,11 +33,11 @@ interface RowItemData { interface ArtistIndexOverviewsProps { items: Artist[]; - sortKey?: string; + sortKey: string; sortDirection?: string; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: React.MutableRefObject; + scrollerRef: RefObject; isSelectMode: boolean; isSmallScreen: boolean; } @@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { 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 ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { }, [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 ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - listRef.current.scrollTo(scrollTop); + listRef.current?.scrollTo(scrollTop); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -175,8 +175,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { 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/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx index e19692e41..4ab9391e3 100644 --- a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx @@ -60,7 +60,7 @@ function ArtistIndexOverviewOptionsModalContent( const dispatch = useDispatch(); const onOverviewOptionChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: unknown }) => { dispatch(setArtistOverviewOption({ [name]: value })); }, [dispatch] diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx index 156368d9b..67c37c00d 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx @@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) { ) : null} - {showQualityProfile ? ( + {showQualityProfile && !!qualityProfile?.name ? (
{qualityProfile.name}
diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx index 9e5c3e885..c478ac1ae 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.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 Artist from 'Artist/Artist'; import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster'; import useMeasure from 'Helpers/Hooks/useMeasure'; @@ -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 ArtistIndexPostersProps { items: Artist[]; - sortKey?: string; + sortKey: string; sortDirection?: SortDirection; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: React.MutableRefObject; + scrollerRef: RefObject; isSelectMode: boolean; isSmallScreen: boolean; } const artistIndexSelector = createSelector( - (state) => state.artistIndex.posterOptions, + (state: AppState) => state.artistIndex.posterOptions, (posterOptions) => { return { posterOptions, @@ -108,7 +109,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { } = props; const { posterOptions } = useSelector(artistIndexSelector); - const ref: React.MutableRefObject = useRef(); + const ref = useRef(null); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -231,8 +232,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { }, [isSmallScreen, size, 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; @@ -241,7 +242,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - ref.current.scrollTo({ scrollLeft: 0, scrollTop }); + ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -264,8 +265,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { 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/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx index e1e60801c..2560d855a 100644 --- a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx @@ -59,7 +59,7 @@ function ArtistIndexPosterOptionsModalContent( const dispatch = useDispatch(); const onPosterOptionChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: unknown }) => { dispatch(setArtistPosterOption({ [name]: value })); }, [dispatch] diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx index bd6a01e10..02c5f9ee9 100644 --- a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx @@ -57,7 +57,7 @@ function AlbumDetails(props: AlbumDetailsProps) { albumType, monitored, statistics, - isSaving, + isSaving = false, } = album; return ( diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx index ec4ef9074..3e7e0578f 100644 --- a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx @@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps { artistId: number; albumId: number; title: string; - disambiguation: string; + disambiguation?: string; albumType: string; monitored: boolean; statistics: Statistics; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx index c21c9a8af..b3c2abbbe 100644 --- a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx +++ b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx @@ -33,7 +33,7 @@ function ChangeMonitoringModalContent( const [monitor, setMonitor] = useState(NO_CHANGE); const onInputChange = useCallback( - ({ value }) => { + ({ value }: { value: string }) => { setMonitor(value); }, [setMonitor] diff --git a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx b/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx index cc072f41a..86b41e8ba 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { SyntheticEvent, useCallback } from 'react'; import { useSelect } from 'App/SelectContext'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; @@ -15,8 +15,9 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) { const isSelected = selectState.selectedState[artistId]; const onSelectPress = useCallback( - (event) => { - 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/Artist/Index/Select/ArtistIndexSelectAllButton.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx index 7229dbdc0..2b3e9c01c 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx @@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props'; interface ArtistIndexSelectAllButtonProps { label: string; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; } function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) { diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx index ffa017a78..f0569d607 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx @@ -24,6 +24,14 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal'; import TagsModal from './Tags/TagsModal'; import styles from './ArtistIndexSelectFooter.css'; +interface SavePayload { + monitored?: boolean; + qualityProfileId?: number; + metadataProfileId?: number; + rootFolderPath?: string; + moveFiles?: boolean; +} + const artistEditorSelector = createSelector( (state: AppState) => state.artist, (artist) => { @@ -79,7 +87,7 @@ function ArtistIndexSelectFooter() { }, [setIsEditModalOpen]); const onSavePress = useCallback( - (payload) => { + (payload: SavePayload) => { setIsSavingArtist(true); setIsEditModalOpen(false); @@ -118,7 +126,7 @@ function ArtistIndexSelectFooter() { }, [setIsTagsModalOpen]); const onApplyTagsPress = useCallback( - (tags, applyTags) => { + (tags: number[], applyTags: string) => { setIsSavingTags(true); setIsTagsModalOpen(false); diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx index 45fe78536..8679bba99 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx @@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps { label: string; iconName: IconDefinition; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; onPress: () => void; } diff --git a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx b/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx index 5e7d1f1ff..b67ee60aa 100644 --- a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx +++ b/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx @@ -28,9 +28,15 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) { const dispatch = useDispatch(); const artistNames = useMemo(() => { - const artists = artistIds.map((id) => { - return allArtists.find((a) => a.id === id); - }); + const artists = artistIds.reduce((acc: Artist[], id) => { + const a = allArtists.find((a) => a.id === id); + + if (a) { + acc.push(a); + } + + return acc; + }, []); const sorted = orderBy(artists, ['sortName']); diff --git a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx b/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx index c367a4550..4accc9f0e 100644 --- a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx +++ b/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx @@ -15,6 +15,7 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions'; import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import { CheckInputChanged } from 'typings/inputs'; import translate from 'Utilities/String/translate'; import styles from './DeleteArtistModalContent.css'; @@ -37,16 +38,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) { const [deleteFiles, setDeleteFiles] = useState(false); - const artists = useMemo(() => { - const artists = artistIds.map((id) => { + const artists = useMemo((): Artist[] => { + const artistList = artistIds.map((id) => { return allArtists.find((a) => a.id === id); - }); + }) as Artist[]; - return orderBy(artists, ['sortName']); + return orderBy(artistList, ['sortName']); }, [artistIds, allArtists]); const onDeleteFilesChange = useCallback( - ({ value }) => { + ({ value }: CheckInputChanged) => { setDeleteFiles(value); }, [setDeleteFiles] diff --git a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx b/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx index 94d1e87d2..f6f733f5f 100644 --- a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx +++ b/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx @@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) { const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false); const save = useCallback( - (moveFiles) => { + (moveFiles: boolean) => { let hasChanges = false; const payload: SavePayload = {}; @@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) { ); const onInputChange = useCallback( - ({ name, value }) => { + ({ name, value }: { name: string; value: string }) => { switch (name) { case 'monitored': setMonitored(value); diff --git a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx index c41c0c896..95a7eaae2 100644 --- a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx +++ b/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx @@ -1,6 +1,7 @@ import { uniq } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; +import { Tag } from 'App/State/TagsAppState'; import Artist from 'Artist/Artist'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; @@ -28,7 +29,7 @@ function TagsModalContent(props: TagsModalContentProps) { const { artistIds, onModalClose, onApplyTagsPress } = props; const allArtists: Artist[] = useSelector(createAllArtistSelector()); - const tagList = useSelector(createTagsSelector()); + const tagList: Tag[] = useSelector(createTagsSelector()); const [tags, setTags] = useState([]); const [applyTags, setApplyTags] = useState('add'); @@ -48,14 +49,14 @@ function TagsModalContent(props: TagsModalContentProps) { }, [artistIds, allArtists]); 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/Artist/Index/Table/ArtistIndexRow.tsx b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx index 376fdb359..0398f5502 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx @@ -23,6 +23,7 @@ import Column from 'Components/Table/Column'; import TagListConnector from 'Components/TagListConnector'; import { icons } from 'Helpers/Props'; import { executeCommand } from 'Store/Actions/commandActions'; +import { SelectStateInputProps } from 'typings/props'; import formatBytes from 'Utilities/Number/formatBytes'; import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; @@ -128,7 +129,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { }, [setIsDeleteArtistModalOpen]); const onSelectedChange = useCallback( - ({ id, value, shiftKey }) => { + ({ id, value, shiftKey }: SelectStateInputProps) => { selectDispatch({ type: 'toggleSelected', id, @@ -219,7 +220,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { if (name === 'qualityProfileId') { return ( - {qualityProfile.name} + {qualityProfile?.name ?? ''} ); } @@ -227,7 +228,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { if (name === 'metadataProfileId') { return ( - {metadataProfile.name} + {metadataProfile?.name ?? ''} ); } @@ -280,6 +281,8 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { if (name === 'added') { 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.artistIndex.columns, + (state: AppState) => state.artistIndex.columns, (columns) => columns ); @@ -93,7 +94,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { 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; @@ -104,7 +105,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { }, [showBanners]); useEffect(() => { - const current = scrollerRef.current as HTMLElement; + const current = scrollerRef?.current as HTMLElement; if (isSmallScreen) { setSize({ @@ -128,8 +129,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { }, [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; @@ -138,7 +139,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - listRef.current.scrollTo(scrollTop); + listRef.current?.scrollTo(scrollTop); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -167,8 +168,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { 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/Artist/Index/Table/ArtistIndexTableHeader.tsx b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx index 2e903574d..1b325c225 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx @@ -15,6 +15,7 @@ import { setArtistSort, setArtistTableOption, } from 'Store/Actions/artistIndexActions'; +import { CheckInputChanged } from 'typings/inputs'; import hasGrowableColumns from './hasGrowableColumns'; import styles from './ArtistIndexTableHeader.css'; @@ -32,21 +33,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) { const [selectState, selectDispatch] = useSelect(); const onSortPress = useCallback( - (value) => { + (value: string) => { dispatch(setArtistSort({ sortKey: value })); }, [dispatch] ); const onTableOptionChange = useCallback( - (payload) => { + (payload: unknown) => { dispatch(setArtistTableOption(payload)); }, [dispatch] ); const onSelectAllChange = useCallback( - ({ value }) => { + ({ value }: CheckInputChanged) => { selectDispatch({ type: value ? 'selectAll' : 'unselectAll', }); @@ -94,6 +95,8 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) { { + ({ name, value }: CheckInputChanged) => { onTableOptionChange({ tableOptions: { ...tableOptions, diff --git a/frontend/src/Artist/Index/createArtistIndexItemSelector.ts b/frontend/src/Artist/Index/createArtistIndexItemSelector.ts index 86ee8a560..4388a3aeb 100644 --- a/frontend/src/Artist/Index/createArtistIndexItemSelector.ts +++ b/frontend/src/Artist/Index/createArtistIndexItemSelector.ts @@ -1,5 +1,6 @@ import { createSelector } from 'reselect'; import Artist from 'Artist/Artist'; +import Command from 'Commands/Command'; import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames'; import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector'; import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector'; @@ -12,25 +13,21 @@ function createArtistIndexItemSelector(artistId: number) { createArtistQualityProfileSelector(artistId), createArtistMetadataProfileSelector(artistId), createExecutingCommandsSelector(), - (artist: Artist, qualityProfile, metadataProfile, executingCommands) => { - // If an artist is deleted this selector may fire before the parent - // selectors, which will result in an undefined artist, if that happens - // we want to return early here and again in the render function to avoid - // trying to show an artist that has no information available. - - if (!artist) { - return {}; - } - + ( + artist: Artist, + qualityProfile, + metadataProfile, + executingCommands: Command[] + ) => { const isRefreshingArtist = executingCommands.some((command) => { return ( - command.name === REFRESH_ARTIST && command.body.artistId === artist.id + command.name === REFRESH_ARTIST && command.body.artistId === artistId ); }); const isSearchingArtist = executingCommands.some((command) => { return ( - command.name === ARTIST_SEARCH && command.body.artistId === artist.id + command.name === ARTIST_SEARCH && command.body.artistId === artistId ); }); diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts new file mode 100644 index 000000000..8b781355f --- /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; + artistId?: 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/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index 972a9bade..ce9b0e7e4 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/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js index 4a597e795..5473413cb 100644 --- a/frontend/src/Components/Table/VirtualTable.js +++ b/frontend/src/Components/Table/VirtualTable.js @@ -7,6 +7,8 @@ import { scrollDirections } from 'Helpers/Props'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import styles from './VirtualTable.css'; +const ROW_HEIGHT = 38; + function overscanIndicesGetter(options) { const { cellCount, @@ -48,8 +50,7 @@ class VirtualTable extends Component { const { items, scrollIndex, - scrollTop, - onRecompute + scrollTop } = this.props; const { @@ -57,10 +58,7 @@ class VirtualTable extends Component { scrollRestored } = this.state; - if (this._grid && - (prevState.width !== width || - hasDifferentItemsOrOrder(prevProps.items, items))) { - onRecompute(width); + if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) { // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } @@ -103,7 +101,6 @@ class VirtualTable extends Component { className, items, scroller, - scrollTop: ignored, header, headerHeight, rowHeight, @@ -149,6 +146,7 @@ class VirtualTable extends Component { {header}
@@ -192,16 +189,14 @@ VirtualTable.propTypes = { scroller: PropTypes.instanceOf(Element).isRequired, header: PropTypes.node.isRequired, headerHeight: PropTypes.number.isRequired, - rowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]).isRequired, rowRenderer: PropTypes.func.isRequired, - onRecompute: PropTypes.func.isRequired + rowHeight: PropTypes.number.isRequired }; VirtualTable.defaultProps = { className: styles.tableContainer, headerHeight: 38, - rowHeight: 38, - onRecompute: () => {} + rowHeight: ROW_HEIGHT }; export default VirtualTable; 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/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js deleted file mode 100644 index 287a58593..000000000 --- a/frontend/src/Store/scrollPositions.js +++ /dev/null @@ -1,5 +0,0 @@ -const scrollPositions = { - artistIndex: 0 -}; - -export default scrollPositions; diff --git a/frontend/src/Store/scrollPositions.ts b/frontend/src/Store/scrollPositions.ts new file mode 100644 index 000000000..199bfa84c --- /dev/null +++ b/frontend/src/Store/scrollPositions.ts @@ -0,0 +1,5 @@ +const scrollPositions: Record = { + artistIndex: 0, +}; + +export default scrollPositions; diff --git a/frontend/src/Store/thunks.js b/frontend/src/Store/thunks.js deleted file mode 100644 index 6daa843f4..000000000 --- a/frontend/src/Store/thunks.js +++ /dev/null @@ -1,27 +0,0 @@ -const thunks = {}; - -function identity(payload) { - return payload; -} - -export function createThunk(type, identityFunction = identity) { - return function(payload = {}) { - return function(dispatch, getState) { - const thunk = thunks[type]; - - if (thunk) { - return thunk(getState, identityFunction(payload), dispatch); - } - - throw Error(`Thunk handler has not been registered for ${type}`); - }; - }; -} - -export function handleThunks(handlers) { - const types = Object.keys(handlers); - - types.forEach((type) => { - thunks[type] = handlers[type]; - }); -} diff --git a/frontend/src/Store/thunks.ts b/frontend/src/Store/thunks.ts new file mode 100644 index 000000000..fd277211e --- /dev/null +++ b/frontend/src/Store/thunks.ts @@ -0,0 +1,39 @@ +import { Dispatch } from 'redux'; +import AppState from 'App/State/AppState'; + +type GetState = () => AppState; +type Thunk = ( + getState: GetState, + identityFn: never, + dispatch: Dispatch +) => unknown; + +const thunks: Record = {}; + +function identity(payload: T): TResult { + return payload as unknown as TResult; +} + +export function createThunk(type: string, identityFunction = identity) { + return function (payload?: T) { + return function (dispatch: Dispatch, getState: GetState) { + const thunk = thunks[type]; + + if (thunk) { + const finalPayload = payload ?? {}; + + return thunk(getState, identityFunction(finalPayload), dispatch); + } + + throw Error(`Thunk handler has not been registered for ${type}`); + }; + }; +} + +export function handleThunks(handlers: Record) { + const types = Object.keys(handlers); + + types.forEach((type) => { + thunks[type] = handlers[type]; + }); +} 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..b84db6245 --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.ts @@ -0,0 +1,18 @@ +import { reduce } from 'lodash'; +import { SelectedState } from 'Helpers/Hooks/useSelectState'; + +function getSelectedIds(selectedState: SelectedState): number[] { + return reduce( + selectedState, + (result: number[], value, id) => { + if (value) { + result.push(parseInt(id)); + } + + return result; + }, + [] + ); +} + +export default getSelectedIds; 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/tsconfig.json b/frontend/tsconfig.json index 4ff9d4e87..611c872ed 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -7,7 +7,15 @@ "jsx": "react", "module": "esnext", "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/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a05bb0b65..4ce0670c3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -680,6 +680,7 @@ "Monitored": "Monitored", "MonitoredHelpText": "Download monitored albums from this artist", "MonitoredOnly": "Monitored Only", + "MonitoredStatus": "Monitored/Status", "Monitoring": "Monitoring", "MonitoringOptions": "Monitoring Options", "MonitoringOptionsHelpText": "Which albums should be monitored after the artist is added (one-time adjustment)",