Typings cleanup and improvements

Appease linter

(cherry picked from commit b2c43fb2a67965d68d3d35b72302b0cddb5aca23)
(cherry picked from commit 3b5e83670b844cf7c20bf7d744d9fbc96fde6902)

Closes #3516
Closes #3510
Closes #2778
pull/4553/head
Mark McDowall 1 year ago committed by Bogdan
parent 8e5942d5c5
commit efe0a3d283

@ -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<HTMLDivElement>();
const scrollerRef = useRef<HTMLDivElement>(null);
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
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<string, number>, item) => {
let char = item.sortName.charAt(0);
if (!isNaN(char)) {
if (!isNaN(Number(char))) {
char = '#';
}
@ -300,6 +305,8 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}

@ -23,7 +23,13 @@ function createFilterBuilderPropsSelector() {
);
}
export default function ArtistIndexFilterModal(props) {
interface ArtistIndexFilterModalProps {
isOpen: boolean;
}
export default function ArtistIndexFilterModal(
props: ArtistIndexFilterModalProps
) {
const sectionItems = useSelector(createArtistSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'artist';
@ -31,7 +37,7 @@ export default function ArtistIndexFilterModal(props) {
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setArtistFilter(payload));
},
[dispatch]
@ -39,6 +45,7 @@ export default function ArtistIndexFilterModal(props) {
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}

@ -206,7 +206,7 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
</div>
) : null}
{showQualityProfile ? (
{showQualityProfile && !!qualityProfile?.name ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>

@ -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<string, number> = {
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<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
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<Grid> = useRef();
const ref = useRef<Grid>(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);
}
}
}, [

@ -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]

@ -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,
};

@ -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')}
</SortMenuItem>
<SortMenuItem
@ -26,7 +34,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Name
{translate('Name')}
</SortMenuItem>
<SortMenuItem
@ -35,7 +43,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Type
{translate('Type')}
</SortMenuItem>
<SortMenuItem
@ -44,7 +52,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Quality Profile
{translate('QualityProfile')}
</SortMenuItem>
<SortMenuItem
@ -53,7 +61,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Metadata Profile
{translate('MetadataProfile')}
</SortMenuItem>
<SortMenuItem
@ -62,7 +70,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Next Album
{translate('NextAlbum')}
</SortMenuItem>
<SortMenuItem
@ -71,7 +79,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Last Album
{translate('Last Album')}
</SortMenuItem>
<SortMenuItem
@ -80,7 +88,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Added
{translate('Added')}
</SortMenuItem>
<SortMenuItem
@ -89,7 +97,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Albums
{translate('Albums')}
</SortMenuItem>
<SortMenuItem
@ -98,7 +106,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Tracks
{translate('Tracks')}
</SortMenuItem>
<SortMenuItem
@ -107,7 +115,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Track Count
{translate('TrackCount')}
</SortMenuItem>
<SortMenuItem
@ -116,7 +124,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Path
{translate('Path')}
</SortMenuItem>
<SortMenuItem
@ -125,7 +133,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Size on Disk
{translate('SizeOnDisk')}
</SortMenuItem>
<SortMenuItem
@ -134,18 +142,11 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Tags
{translate('Tags')}
</SortMenuItem>
</MenuContent>
</SortMenu>
);
}
ArtistIndexSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
};
export default ArtistIndexSortMenu;

@ -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;

@ -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 <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
})}
</div>

@ -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) {

@ -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<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
const { size: posterSize, detailedProgressBar } = useSelector(
selectOverviewOptions
);
const listRef: React.MutableRefObject<List> = useRef();
const listRef = useRef<List>(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]);

@ -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]

@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
</div>
) : null}
{showQualityProfile ? (
{showQualityProfile && !!qualityProfile?.name ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>

@ -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<string, number> = {
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<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
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<Grid> = useRef();
const ref = useRef<Grid>(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);
}
}
}, [

@ -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]

@ -57,7 +57,7 @@ function AlbumDetails(props: AlbumDetailsProps) {
albumType,
monitored,
statistics,
isSaving,
isSaving = false,
} = album;
return (

@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps {
artistId: number;
albumId: number;
title: string;
disambiguation: string;
disambiguation?: string;
albumType: string;
monitored: boolean;
statistics: Statistics;

@ -33,7 +33,7 @@ function ChangeMonitoringModalContent(
const [monitor, setMonitor] = useState(NO_CHANGE);
const onInputChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setMonitor(value);
},
[setMonitor]

@ -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',

@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
interface ArtistIndexSelectAllButtonProps {
label: string;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
}
function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {

@ -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);

@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps {
label: string;
iconName: IconDefinition;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
onPress: () => void;
}

@ -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']);

@ -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]

@ -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);

@ -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<number[]>([]);
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]

@ -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 (
<VirtualTableRowCell key={name} className={styles[name]}>
{qualityProfile.name}
{qualityProfile?.name ?? ''}
</VirtualTableRowCell>
);
}
@ -227,7 +228,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'metadataProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{metadataProfile.name}
{metadataProfile?.name ?? ''}
</VirtualTableRowCell>
);
}
@ -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)
<RelativeDateCellConnector
key={name}
className={styles[name]}

@ -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 { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
@ -30,17 +31,17 @@ interface RowItemData {
interface ArtistIndexTableProps {
items: Artist[];
sortKey?: string;
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
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<List> = useRef();
const listRef = useRef<List<RowItemData>>(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]);

@ -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) {
<VirtualTableHeaderCell
key={name}
className={classNames(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
styles[name],
name === 'sortName' && showBanners && styles.banner,
name === 'sortName' &&

@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import selectTableOptions from './selectTableOptions';
@ -19,7 +20,7 @@ function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) {
const { showBanners, showSearchAction } = tableOptions;
const onTableOptionChangeWrapper = useCallback(
({ name, value }) => {
({ name, value }: CheckInputChanged) => {
onTableOptionChange({
tableOptions: {
...tableOptions,

@ -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
);
});

@ -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;

@ -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<HTMLDivElement>
) => {
(props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
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);
}

@ -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<HTMLDivElement>) => {
(props: ScrollerProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
className,
autoFocus = false,
@ -30,7 +42,7 @@ const Scroller = forwardRef(
} = props;
const internalRef = useRef();
const currentRef = ref ?? internalRef;
const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
useEffect(
() => {

@ -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}
<div ref={registerChild}>
<Grid
{...otherProps}
ref={this.setGridRef}
autoContainerWidth={true}
autoHeight={true}
@ -170,7 +168,6 @@ class VirtualTable extends Component {
className={styles.tableBodyContainer}
style={gridStyle}
containerStyle={containerStyle}
{...otherProps}
/>
</div>
</Scroller>
@ -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;

@ -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<WrappedComponentProps>,
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 <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
}
ScrollPosition.propTypes = {
history: PropTypes.object.isRequired,
};
return ScrollPosition;
}

@ -1,5 +0,0 @@
const scrollPositions = {
artistIndex: 0
};
export default scrollPositions;

@ -0,0 +1,5 @@
const scrollPositions: Record<string, number> = {
artistIndex: 0,
};
export default scrollPositions;

@ -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];
});
}

@ -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<string, Thunk> = {};
function identity<T, TResult>(payload: T): TResult {
return payload as unknown as TResult;
}
export function createThunk(type: string, identityFunction = identity) {
return function <T>(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<string, Thunk>) {
const types = Object.keys(handlers);
types.forEach((type) => {
thunks[type] = handlers[type];
});
}

@ -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;

@ -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;

@ -0,0 +1,6 @@
import SortDirection from 'Helpers/Props/SortDirection';
export type SortCallback = (
sortKey: string,
sortDirection: SortDirection
) => void;

@ -0,0 +1,4 @@
export type CheckInputChanged = {
name: string;
value: boolean;
};

@ -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": {

@ -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)",

Loading…
Cancel
Save