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'; } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext'; import { SelectProvider } from 'App/SelectContext';
import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import NoArtist from 'Artist/NoArtist'; import NoArtist from 'Artist/NoArtist';
import { RSS_SYNC } from 'Commands/commandNames'; import { RSS_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -89,16 +91,19 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
sortKey, sortKey,
sortDirection, sortDirection,
view, view,
} = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex')); }: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState =
useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
const isRssSyncExecuting = useSelector( const isRssSyncExecuting = useSelector(
createCommandExecutingSelector(RSS_SYNC) createCommandExecutingSelector(RSS_SYNC)
); );
const { isSmallScreen } = useSelector(createDimensionsSelector()); const { isSmallScreen } = useSelector(createDimensionsSelector());
const dispatch = useDispatch(); const dispatch = useDispatch();
const scrollerRef = useRef<HTMLDivElement>(); const scrollerRef = useRef<HTMLDivElement>(null);
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false); const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null); const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
undefined
);
const [isSelectMode, setIsSelectMode] = useState(false); const [isSelectMode, setIsSelectMode] = useState(false);
useEffect(() => { useEffect(() => {
@ -118,14 +123,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
}, [isSelectMode, setIsSelectMode]); }, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback( const onTableOptionChange = useCallback(
(payload) => { (payload: unknown) => {
dispatch(setArtistTableOption(payload)); dispatch(setArtistTableOption(payload));
}, },
[dispatch] [dispatch]
); );
const onViewSelect = useCallback( const onViewSelect = useCallback(
(value) => { (value: string) => {
dispatch(setArtistView({ view: value })); dispatch(setArtistView({ view: value }));
if (scrollerRef.current) { if (scrollerRef.current) {
@ -136,14 +141,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
); );
const onSortSelect = useCallback( const onSortSelect = useCallback(
(value) => { (value: string) => {
dispatch(setArtistSort({ sortKey: value })); dispatch(setArtistSort({ sortKey: value }));
}, },
[dispatch] [dispatch]
); );
const onFilterSelect = useCallback( const onFilterSelect = useCallback(
(value) => { (value: string) => {
dispatch(setArtistFilter({ selectedFilterKey: value })); dispatch(setArtistFilter({ selectedFilterKey: value }));
}, },
[dispatch] [dispatch]
@ -158,15 +163,15 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
}, [setIsOptionsModalOpen]); }, [setIsOptionsModalOpen]);
const onJumpBarItemPress = useCallback( const onJumpBarItemPress = useCallback(
(character) => { (character: string) => {
setJumpToCharacter(character); setJumpToCharacter(character);
}, },
[setJumpToCharacter] [setJumpToCharacter]
); );
const onScroll = useCallback( const onScroll = useCallback(
({ scrollTop }) => { ({ scrollTop }: { scrollTop: number }) => {
setJumpToCharacter(null); setJumpToCharacter(undefined);
scrollPositions.artistIndex = scrollTop; scrollPositions.artistIndex = scrollTop;
}, },
[setJumpToCharacter] [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); let char = item.sortName.charAt(0);
if (!isNaN(char)) { if (!isNaN(Number(char))) {
char = '#'; char = '#';
} }
@ -300,6 +305,8 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
<PageContentBody <PageContentBody
ref={scrollerRef} ref={scrollerRef}
className={styles.contentBody} className={styles.contentBody}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
innerClassName={styles[`${view}InnerContentBody`]} innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop} initialScrollTop={props.initialScrollTop}
onScroll={onScroll} 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 sectionItems = useSelector(createArtistSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'artist'; const customFilterType = 'artist';
@ -31,7 +37,7 @@ export default function ArtistIndexFilterModal(props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const dispatchSetFilter = useCallback( const dispatchSetFilter = useCallback(
(payload) => { (payload: unknown) => {
dispatch(setArtistFilter(payload)); dispatch(setArtistFilter(payload));
}, },
[dispatch] [dispatch]
@ -39,6 +45,7 @@ export default function ArtistIndexFilterModal(props) {
return ( return (
<FilterModal <FilterModal
// TODO: Don't spread all the props
{...props} {...props}
sectionItems={sectionItems} sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps} filterBuilderProps={filterBuilderProps}

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

@ -1,8 +1,9 @@
import { throttle } from 'lodash'; 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 { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist'; import Artist from 'Artist/Artist';
import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner'; import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner';
import useMeasure from 'Helpers/Hooks/useMeasure'; import useMeasure from 'Helpers/Hooks/useMeasure';
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT = { const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
small: 3, small: 3,
medium: 2, medium: 2,
large: 1, large: 1,
@ -41,17 +42,17 @@ interface CellItemData {
interface ArtistIndexBannersProps { interface ArtistIndexBannersProps {
items: Artist[]; items: Artist[];
sortKey?: string; sortKey: string;
sortDirection?: SortDirection; sortDirection?: SortDirection;
jumpToCharacter?: string; jumpToCharacter?: string;
scrollTop?: number; scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>; scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean; isSelectMode: boolean;
isSmallScreen: boolean; isSmallScreen: boolean;
} }
const artistIndexSelector = createSelector( const artistIndexSelector = createSelector(
(state) => state.artistIndex.bannerOptions, (state: AppState) => state.artistIndex.bannerOptions,
(bannerOptions) => { (bannerOptions) => {
return { return {
bannerOptions, bannerOptions,
@ -108,7 +109,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
} = props; } = props;
const { bannerOptions } = useSelector(artistIndexSelector); const { bannerOptions } = useSelector(artistIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef(); const ref = useRef<Grid>(null);
const [measureRef, bounds] = useMeasure(); const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 }); const [size, setSize] = useState({ width: 0, height: 0 });
@ -222,8 +223,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
}, [isSmallScreen, scrollerRef, bounds]); }, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => { useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current; const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollerRef = scrollerRef.current; const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => { const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef; const { offsetTop = 0 } = currentScrollerRef;
@ -232,7 +233,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
? getWindowScrollTopPosition() ? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop; : currentScrollerRef.scrollTop) - offsetTop;
ref.current.scrollTo({ scrollLeft: 0, scrollTop }); ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
}, 10); }, 10);
currentScrollListener.addEventListener('scroll', handleScroll); currentScrollListener.addEventListener('scroll', handleScroll);
@ -255,8 +256,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
const scrollTop = rowIndex * rowHeight + padding; const scrollTop = rowIndex * rowHeight + padding;
ref.current.scrollTo({ scrollLeft: 0, scrollTop }); ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current.scrollTo(0, scrollTop); scrollerRef.current?.scrollTo(0, scrollTop);
} }
} }
}, [ }, [

@ -59,7 +59,7 @@ function ArtistIndexBannerOptionsModalContent(
const dispatch = useDispatch(); const dispatch = useDispatch();
const onBannerOptionChange = useCallback( const onBannerOptionChange = useCallback(
({ name, value }) => { ({ name, value }: { name: string; value: unknown }) => {
dispatch(setArtistBannerOption({ [name]: value })); dispatch(setArtistBannerOption({ [name]: value }));
}, },
[dispatch] [dispatch]

@ -1,10 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { CustomFilter } from 'App/State/AppState';
import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal'; import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal';
import FilterMenu from 'Components/Menu/FilterMenu'; import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props'; 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 { const {
selectedFilterKey, selectedFilterKey,
filters, 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 = { ArtistIndexFilterMenu.defaultProps = {
showCustomFilters: false, showCustomFilters: false,
}; };

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

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MenuContent from 'Components/Menu/MenuContent'; import MenuContent from 'Components/Menu/MenuContent';
import ViewMenu from 'Components/Menu/ViewMenu'; import ViewMenu from 'Components/Menu/ViewMenu';
@ -6,7 +5,13 @@ import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; 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; const { view, isDisabled, onViewSelect } = props;
return ( 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; export default ArtistIndexViewMenu;

@ -1,15 +1,51 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Album from 'Album/Album'; import Album from 'Album/Album';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow'; import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
import styles from './ArtistIndexOverviewInfo.css'; 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 infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
const rows = [ 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; const { name } = row;
if (name === 'monitored') { if (name === 'monitored') {
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored'; const monitoredText = props.monitored
? translate('Monitored')
: translate('Unmonitored');
return { return {
title: monitoredText, title: monitoredText,
@ -63,9 +105,9 @@ function getInfoRowProps(row, props, uiSettings) {
}; };
} }
if (name === 'qualityProfileId') { if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
return { return {
title: 'Quality Profile', title: translate('QualityProfile'),
iconName: icons.PROFILE, iconName: icons.PROFILE,
label: props.qualityProfile.name, label: props.qualityProfile.name,
}; };
@ -78,15 +120,16 @@ function getInfoRowProps(row, props, uiSettings) {
return { return {
title: `Last Album: ${lastAlbum.title}`, title: `Last Album: ${lastAlbum.title}`,
iconName: icons.CALENDAR, iconName: icons.CALENDAR,
label: getRelativeDate( label:
lastAlbum.releaseDate, getRelativeDate(
shortDateFormat, lastAlbum.releaseDate,
showRelativeDates, shortDateFormat,
{ showRelativeDates,
timeFormat, {
timeForToday: true, timeFormat,
} timeForToday: true,
), }
) ?? '',
}; };
} }
@ -98,10 +141,11 @@ function getInfoRowProps(row, props, uiSettings) {
return { return {
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`, title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
iconName: icons.ADD, iconName: icons.ADD,
label: getRelativeDate(added, shortDateFormat, showRelativeDates, { label:
timeFormat, getRelativeDate(added, shortDateFormat, showRelativeDates, {
timeForToday: true, timeFormat,
}), timeForToday: true,
}) ?? '',
}; };
} }
@ -116,7 +160,7 @@ function getInfoRowProps(row, props, uiSettings) {
} }
return { return {
title: 'Album Count', title: translate('AlbumCount'),
iconName: icons.CIRCLE, iconName: icons.CIRCLE,
label: albums, label: albums,
}; };
@ -124,7 +168,7 @@ function getInfoRowProps(row, props, uiSettings) {
if (name === 'path') { if (name === 'path') {
return { return {
title: 'Path', title: translate('Path'),
iconName: icons.FOLDER, iconName: icons.FOLDER,
label: props.path, label: props.path,
}; };
@ -132,31 +176,13 @@ function getInfoRowProps(row, props, uiSettings) {
if (name === 'sizeOnDisk') { if (name === 'sizeOnDisk') {
return { return {
title: 'Size on Disk', title: translate('SizeOnDisk'),
iconName: icons.DRIVE, iconName: icons.DRIVE,
label: formatBytes(props.sizeOnDisk), label: formatBytes(props.sizeOnDisk),
}; };
} }
}
interface ArtistIndexOverviewInfoProps { return null;
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;
} }
function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) { function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
@ -175,6 +201,8 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
const { name, showProp, valueProp } = row; const { name, showProp, valueProp } = row;
const isVisible = const isVisible =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(7053)
props[valueProp] != null && (props[showProp] || props.sortKey === name); props[valueProp] != null && (props[showProp] || props.sortKey === name);
return { return {
@ -219,6 +247,10 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
const infoRowProps = getInfoRowProps(row, props, uiSettings); const infoRowProps = getInfoRowProps(row, props, uiSettings);
if (infoRowProps == null) {
return null;
}
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />; return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
})} })}
</div> </div>

@ -1,11 +1,12 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import styles from './ArtistIndexOverviewInfoRow.css'; import styles from './ArtistIndexOverviewInfoRow.css';
interface ArtistIndexOverviewInfoRowProps { interface ArtistIndexOverviewInfoRowProps {
title?: string; title?: string;
iconName: object; iconName?: IconDefinition;
label: string; label: string | null;
} }
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) { function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {

@ -1,5 +1,5 @@
import { throttle } from 'lodash'; 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 { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import Artist from 'Artist/Artist'; import Artist from 'Artist/Artist';
@ -33,11 +33,11 @@ interface RowItemData {
interface ArtistIndexOverviewsProps { interface ArtistIndexOverviewsProps {
items: Artist[]; items: Artist[];
sortKey?: string; sortKey: string;
sortDirection?: string; sortDirection?: string;
jumpToCharacter?: string; jumpToCharacter?: string;
scrollTop?: number; scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>; scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean; isSelectMode: boolean;
isSmallScreen: boolean; isSmallScreen: boolean;
} }
@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
const { size: posterSize, detailedProgressBar } = useSelector( const { size: posterSize, detailedProgressBar } = useSelector(
selectOverviewOptions selectOverviewOptions
); );
const listRef: React.MutableRefObject<List> = useRef(); const listRef = useRef<List>(null);
const [measureRef, bounds] = useMeasure(); const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 }); const [size, setSize] = useState({ width: 0, height: 0 });
@ -136,8 +136,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
}, [isSmallScreen, scrollerRef, bounds]); }, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => { useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current; const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollerRef = scrollerRef.current; const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => { const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef; const { offsetTop = 0 } = currentScrollerRef;
@ -146,7 +146,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
? getWindowScrollTopPosition() ? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop; : currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop); listRef.current?.scrollTo(scrollTop);
}, 10); }, 10);
currentScrollListener.addEventListener('scroll', handleScroll); currentScrollListener.addEventListener('scroll', handleScroll);
@ -175,8 +175,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
scrollTop += offset; scrollTop += offset;
} }
listRef.current.scrollTo(scrollTop); listRef.current?.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop); scrollerRef.current?.scrollTo(0, scrollTop);
} }
} }
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

@ -60,7 +60,7 @@ function ArtistIndexOverviewOptionsModalContent(
const dispatch = useDispatch(); const dispatch = useDispatch();
const onOverviewOptionChange = useCallback( const onOverviewOptionChange = useCallback(
({ name, value }) => { ({ name, value }: { name: string; value: unknown }) => {
dispatch(setArtistOverviewOption({ [name]: value })); dispatch(setArtistOverviewOption({ [name]: value }));
}, },
[dispatch] [dispatch]

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

@ -1,8 +1,9 @@
import { throttle } from 'lodash'; 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 { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist'; import Artist from 'Artist/Artist';
import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster'; import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster';
import useMeasure from 'Helpers/Hooks/useMeasure'; import useMeasure from 'Helpers/Hooks/useMeasure';
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT = { const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
small: 3, small: 3,
medium: 2, medium: 2,
large: 1, large: 1,
@ -41,17 +42,17 @@ interface CellItemData {
interface ArtistIndexPostersProps { interface ArtistIndexPostersProps {
items: Artist[]; items: Artist[];
sortKey?: string; sortKey: string;
sortDirection?: SortDirection; sortDirection?: SortDirection;
jumpToCharacter?: string; jumpToCharacter?: string;
scrollTop?: number; scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>; scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean; isSelectMode: boolean;
isSmallScreen: boolean; isSmallScreen: boolean;
} }
const artistIndexSelector = createSelector( const artistIndexSelector = createSelector(
(state) => state.artistIndex.posterOptions, (state: AppState) => state.artistIndex.posterOptions,
(posterOptions) => { (posterOptions) => {
return { return {
posterOptions, posterOptions,
@ -108,7 +109,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
} = props; } = props;
const { posterOptions } = useSelector(artistIndexSelector); const { posterOptions } = useSelector(artistIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef(); const ref = useRef<Grid>(null);
const [measureRef, bounds] = useMeasure(); const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 }); const [size, setSize] = useState({ width: 0, height: 0 });
@ -231,8 +232,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
}, [isSmallScreen, size, scrollerRef, bounds]); }, [isSmallScreen, size, scrollerRef, bounds]);
useEffect(() => { useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current; const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollerRef = scrollerRef.current; const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => { const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef; const { offsetTop = 0 } = currentScrollerRef;
@ -241,7 +242,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
? getWindowScrollTopPosition() ? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop; : currentScrollerRef.scrollTop) - offsetTop;
ref.current.scrollTo({ scrollLeft: 0, scrollTop }); ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
}, 10); }, 10);
currentScrollListener.addEventListener('scroll', handleScroll); currentScrollListener.addEventListener('scroll', handleScroll);
@ -264,8 +265,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
const scrollTop = rowIndex * rowHeight + padding; const scrollTop = rowIndex * rowHeight + padding;
ref.current.scrollTo({ scrollLeft: 0, scrollTop }); ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current.scrollTo(0, scrollTop); scrollerRef.current?.scrollTo(0, scrollTop);
} }
} }
}, [ }, [

@ -59,7 +59,7 @@ function ArtistIndexPosterOptionsModalContent(
const dispatch = useDispatch(); const dispatch = useDispatch();
const onPosterOptionChange = useCallback( const onPosterOptionChange = useCallback(
({ name, value }) => { ({ name, value }: { name: string; value: unknown }) => {
dispatch(setArtistPosterOption({ [name]: value })); dispatch(setArtistPosterOption({ [name]: value }));
}, },
[dispatch] [dispatch]

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

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

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

@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React, { SyntheticEvent, useCallback } from 'react';
import { useSelect } from 'App/SelectContext'; import { useSelect } from 'App/SelectContext';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
@ -15,8 +15,9 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
const isSelected = selectState.selectedState[artistId]; const isSelected = selectState.selectedState[artistId];
const onSelectPress = useCallback( const onSelectPress = useCallback(
(event) => { (event: SyntheticEvent) => {
const shiftKey = event.nativeEvent.shiftKey; const nativeEvent = event.nativeEvent as PointerEvent;
const shiftKey = nativeEvent.shiftKey;
selectDispatch({ selectDispatch({
type: 'toggleSelected', type: 'toggleSelected',

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

@ -24,6 +24,14 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal';
import TagsModal from './Tags/TagsModal'; import TagsModal from './Tags/TagsModal';
import styles from './ArtistIndexSelectFooter.css'; import styles from './ArtistIndexSelectFooter.css';
interface SavePayload {
monitored?: boolean;
qualityProfileId?: number;
metadataProfileId?: number;
rootFolderPath?: string;
moveFiles?: boolean;
}
const artistEditorSelector = createSelector( const artistEditorSelector = createSelector(
(state: AppState) => state.artist, (state: AppState) => state.artist,
(artist) => { (artist) => {
@ -79,7 +87,7 @@ function ArtistIndexSelectFooter() {
}, [setIsEditModalOpen]); }, [setIsEditModalOpen]);
const onSavePress = useCallback( const onSavePress = useCallback(
(payload) => { (payload: SavePayload) => {
setIsSavingArtist(true); setIsSavingArtist(true);
setIsEditModalOpen(false); setIsEditModalOpen(false);
@ -118,7 +126,7 @@ function ArtistIndexSelectFooter() {
}, [setIsTagsModalOpen]); }, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback( const onApplyTagsPress = useCallback(
(tags, applyTags) => { (tags: number[], applyTags: string) => {
setIsSavingTags(true); setIsSavingTags(true);
setIsTagsModalOpen(false); setIsTagsModalOpen(false);

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

@ -28,9 +28,15 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const artistNames = useMemo(() => { const artistNames = useMemo(() => {
const artists = artistIds.map((id) => { const artists = artistIds.reduce((acc: Artist[], id) => {
return allArtists.find((a) => a.id === id); const a = allArtists.find((a) => a.id === id);
});
if (a) {
acc.push(a);
}
return acc;
}, []);
const sorted = orderBy(artists, ['sortName']); const sorted = orderBy(artists, ['sortName']);

@ -15,6 +15,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions'; import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './DeleteArtistModalContent.css'; import styles from './DeleteArtistModalContent.css';
@ -37,16 +38,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) {
const [deleteFiles, setDeleteFiles] = useState(false); const [deleteFiles, setDeleteFiles] = useState(false);
const artists = useMemo(() => { const artists = useMemo((): Artist[] => {
const artists = artistIds.map((id) => { const artistList = artistIds.map((id) => {
return allArtists.find((a) => a.id === id); return allArtists.find((a) => a.id === id);
}); }) as Artist[];
return orderBy(artists, ['sortName']); return orderBy(artistList, ['sortName']);
}, [artistIds, allArtists]); }, [artistIds, allArtists]);
const onDeleteFilesChange = useCallback( const onDeleteFilesChange = useCallback(
({ value }) => { ({ value }: CheckInputChanged) => {
setDeleteFiles(value); setDeleteFiles(value);
}, },
[setDeleteFiles] [setDeleteFiles]

@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false); const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const save = useCallback( const save = useCallback(
(moveFiles) => { (moveFiles: boolean) => {
let hasChanges = false; let hasChanges = false;
const payload: SavePayload = {}; const payload: SavePayload = {};
@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
); );
const onInputChange = useCallback( const onInputChange = useCallback(
({ name, value }) => { ({ name, value }: { name: string; value: string }) => {
switch (name) { switch (name) {
case 'monitored': case 'monitored':
setMonitored(value); setMonitored(value);

@ -1,6 +1,7 @@
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Tag } from 'App/State/TagsAppState';
import Artist from 'Artist/Artist'; import Artist from 'Artist/Artist';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
@ -28,7 +29,7 @@ function TagsModalContent(props: TagsModalContentProps) {
const { artistIds, onModalClose, onApplyTagsPress } = props; const { artistIds, onModalClose, onApplyTagsPress } = props;
const allArtists: Artist[] = useSelector(createAllArtistSelector()); const allArtists: Artist[] = useSelector(createAllArtistSelector());
const tagList = useSelector(createTagsSelector()); const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]); const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add'); const [applyTags, setApplyTags] = useState('add');
@ -48,14 +49,14 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [artistIds, allArtists]); }, [artistIds, allArtists]);
const onTagsChange = useCallback( const onTagsChange = useCallback(
({ value }) => { ({ value }: { value: number[] }) => {
setTags(value); setTags(value);
}, },
[setTags] [setTags]
); );
const onApplyTagsChange = useCallback( const onApplyTagsChange = useCallback(
({ value }) => { ({ value }: { value: string }) => {
setApplyTags(value); setApplyTags(value);
}, },
[setApplyTags] [setApplyTags]

@ -23,6 +23,7 @@ import Column from 'Components/Table/Column';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { SelectStateInputProps } from 'typings/props';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
@ -128,7 +129,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
}, [setIsDeleteArtistModalOpen]); }, [setIsDeleteArtistModalOpen]);
const onSelectedChange = useCallback( const onSelectedChange = useCallback(
({ id, value, shiftKey }) => { ({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({ selectDispatch({
type: 'toggleSelected', type: 'toggleSelected',
id, id,
@ -219,7 +220,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'qualityProfileId') { if (name === 'qualityProfileId') {
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
{qualityProfile.name} {qualityProfile?.name ?? ''}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@ -227,7 +228,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'metadataProfileId') { if (name === 'metadataProfileId') {
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
{metadataProfile.name} {metadataProfile?.name ?? ''}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@ -280,6 +281,8 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'added') { if (name === 'added') {
return ( return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector <RelativeDateCellConnector
key={name} key={name}
className={styles[name]} className={styles[name]}

@ -1,8 +1,9 @@
import { throttle } from 'lodash'; 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 { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist'; import Artist from 'Artist/Artist';
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow'; import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader'; import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
@ -30,17 +31,17 @@ interface RowItemData {
interface ArtistIndexTableProps { interface ArtistIndexTableProps {
items: Artist[]; items: Artist[];
sortKey?: string; sortKey: string;
sortDirection?: SortDirection; sortDirection?: SortDirection;
jumpToCharacter?: string; jumpToCharacter?: string;
scrollTop?: number; scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>; scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean; isSelectMode: boolean;
isSmallScreen: boolean; isSmallScreen: boolean;
} }
const columnsSelector = createSelector( const columnsSelector = createSelector(
(state) => state.artistIndex.columns, (state: AppState) => state.artistIndex.columns,
(columns) => columns (columns) => columns
); );
@ -93,7 +94,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
const columns = useSelector(columnsSelector); const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions); const { showBanners } = useSelector(selectTableOptions);
const listRef: React.MutableRefObject<List> = useRef(); const listRef = useRef<List<RowItemData>>(null);
const [measureRef, bounds] = useMeasure(); const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 }); const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth; const windowWidth = window.innerWidth;
@ -104,7 +105,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
}, [showBanners]); }, [showBanners]);
useEffect(() => { useEffect(() => {
const current = scrollerRef.current as HTMLElement; const current = scrollerRef?.current as HTMLElement;
if (isSmallScreen) { if (isSmallScreen) {
setSize({ setSize({
@ -128,8 +129,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]); }, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
useEffect(() => { useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current; const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollerRef = scrollerRef.current; const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => { const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef; const { offsetTop = 0 } = currentScrollerRef;
@ -138,7 +139,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
? getWindowScrollTopPosition() ? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop; : currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop); listRef.current?.scrollTo(scrollTop);
}, 10); }, 10);
currentScrollListener.addEventListener('scroll', handleScroll); currentScrollListener.addEventListener('scroll', handleScroll);
@ -167,8 +168,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
scrollTop += offset; scrollTop += offset;
} }
listRef.current.scrollTo(scrollTop); listRef.current?.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop); scrollerRef?.current?.scrollTo(0, scrollTop);
} }
} }
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

@ -15,6 +15,7 @@ import {
setArtistSort, setArtistSort,
setArtistTableOption, setArtistTableOption,
} from 'Store/Actions/artistIndexActions'; } from 'Store/Actions/artistIndexActions';
import { CheckInputChanged } from 'typings/inputs';
import hasGrowableColumns from './hasGrowableColumns'; import hasGrowableColumns from './hasGrowableColumns';
import styles from './ArtistIndexTableHeader.css'; import styles from './ArtistIndexTableHeader.css';
@ -32,21 +33,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
const [selectState, selectDispatch] = useSelect(); const [selectState, selectDispatch] = useSelect();
const onSortPress = useCallback( const onSortPress = useCallback(
(value) => { (value: string) => {
dispatch(setArtistSort({ sortKey: value })); dispatch(setArtistSort({ sortKey: value }));
}, },
[dispatch] [dispatch]
); );
const onTableOptionChange = useCallback( const onTableOptionChange = useCallback(
(payload) => { (payload: unknown) => {
dispatch(setArtistTableOption(payload)); dispatch(setArtistTableOption(payload));
}, },
[dispatch] [dispatch]
); );
const onSelectAllChange = useCallback( const onSelectAllChange = useCallback(
({ value }) => { ({ value }: CheckInputChanged) => {
selectDispatch({ selectDispatch({
type: value ? 'selectAll' : 'unselectAll', type: value ? 'selectAll' : 'unselectAll',
}); });
@ -94,6 +95,8 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
<VirtualTableHeaderCell <VirtualTableHeaderCell
key={name} key={name}
className={classNames( className={classNames(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
styles[name], styles[name],
name === 'sortName' && showBanners && styles.banner, name === 'sortName' && showBanners && styles.banner,
name === 'sortName' && name === 'sortName' &&

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

@ -1,5 +1,6 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import Artist from 'Artist/Artist'; import Artist from 'Artist/Artist';
import Command from 'Commands/Command';
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames'; import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector'; import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector'; import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
@ -12,25 +13,21 @@ function createArtistIndexItemSelector(artistId: number) {
createArtistQualityProfileSelector(artistId), createArtistQualityProfileSelector(artistId),
createArtistMetadataProfileSelector(artistId), createArtistMetadataProfileSelector(artistId),
createExecutingCommandsSelector(), createExecutingCommandsSelector(),
(artist: Artist, qualityProfile, metadataProfile, executingCommands) => { (
// If an artist is deleted this selector may fire before the parent artist: Artist,
// selectors, which will result in an undefined artist, if that happens qualityProfile,
// we want to return early here and again in the render function to avoid metadataProfile,
// trying to show an artist that has no information available. executingCommands: Command[]
) => {
if (!artist) {
return {};
}
const isRefreshingArtist = executingCommands.some((command) => { const isRefreshingArtist = executingCommands.some((command) => {
return ( return (
command.name === REFRESH_ARTIST && command.body.artistId === artist.id command.name === REFRESH_ARTIST && command.body.artistId === artistId
); );
}); });
const isSearchingArtist = executingCommands.some((command) => { const isSearchingArtist = executingCommands.some((command) => {
return ( 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 React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
import Scroller from 'Components/Scroller/Scroller'; import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
import ScrollDirection from 'Helpers/Props/ScrollDirection'; import ScrollDirection from 'Helpers/Props/ScrollDirection';
import { isLocked } from 'Utilities/scrollLock'; import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css'; import styles from './PageContentBody.css';
@ -9,14 +9,11 @@ interface PageContentBodyProps {
innerClassName?: string; innerClassName?: string;
children: ReactNode; children: ReactNode;
initialScrollTop?: number; initialScrollTop?: number;
onScroll?: (payload) => void; onScroll?: (payload: OnScroll) => void;
} }
const PageContentBody = forwardRef( const PageContentBody = forwardRef(
( (props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
props: PageContentBodyProps,
ref: React.MutableRefObject<HTMLDivElement>
) => {
const { const {
className = styles.contentBody, className = styles.contentBody,
innerClassName = styles.innerContentBody, innerClassName = styles.innerContentBody,
@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
} = props; } = props;
const onScrollWrapper = useCallback( const onScrollWrapper = useCallback(
(payload) => { (payload: OnScroll) => {
if (onScroll && !isLocked()) { if (onScroll && !isLocked()) {
onScroll(payload); onScroll(payload);
} }

@ -1,9 +1,21 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { throttle } from 'lodash'; 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 ScrollDirection from 'Helpers/Props/ScrollDirection';
import styles from './Scroller.css'; import styles from './Scroller.css';
export interface OnScroll {
scrollLeft: number;
scrollTop: number;
}
interface ScrollerProps { interface ScrollerProps {
className?: string; className?: string;
scrollDirection?: ScrollDirection; scrollDirection?: ScrollDirection;
@ -12,11 +24,11 @@ interface ScrollerProps {
scrollTop?: number; scrollTop?: number;
initialScrollTop?: number; initialScrollTop?: number;
children?: ReactNode; children?: ReactNode;
onScroll?: (payload) => void; onScroll?: (payload: OnScroll) => void;
} }
const Scroller = forwardRef( const Scroller = forwardRef(
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => { (props: ScrollerProps, ref: ForwardedRef<HTMLDivElement>) => {
const { const {
className, className,
autoFocus = false, autoFocus = false,
@ -30,7 +42,7 @@ const Scroller = forwardRef(
} = props; } = props;
const internalRef = useRef(); const internalRef = useRef();
const currentRef = ref ?? internalRef; const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
useEffect( useEffect(
() => { () => {

@ -7,6 +7,8 @@ import { scrollDirections } from 'Helpers/Props';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import styles from './VirtualTable.css'; import styles from './VirtualTable.css';
const ROW_HEIGHT = 38;
function overscanIndicesGetter(options) { function overscanIndicesGetter(options) {
const { const {
cellCount, cellCount,
@ -48,8 +50,7 @@ class VirtualTable extends Component {
const { const {
items, items,
scrollIndex, scrollIndex,
scrollTop, scrollTop
onRecompute
} = this.props; } = this.props;
const { const {
@ -57,10 +58,7 @@ class VirtualTable extends Component {
scrollRestored scrollRestored
} = this.state; } = this.state;
if (this._grid && if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
(prevState.width !== width ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
onRecompute(width);
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
} }
@ -103,7 +101,6 @@ class VirtualTable extends Component {
className, className,
items, items,
scroller, scroller,
scrollTop: ignored,
header, header,
headerHeight, headerHeight,
rowHeight, rowHeight,
@ -149,6 +146,7 @@ class VirtualTable extends Component {
{header} {header}
<div ref={registerChild}> <div ref={registerChild}>
<Grid <Grid
{...otherProps}
ref={this.setGridRef} ref={this.setGridRef}
autoContainerWidth={true} autoContainerWidth={true}
autoHeight={true} autoHeight={true}
@ -170,7 +168,6 @@ class VirtualTable extends Component {
className={styles.tableBodyContainer} className={styles.tableBodyContainer}
style={gridStyle} style={gridStyle}
containerStyle={containerStyle} containerStyle={containerStyle}
{...otherProps}
/> />
</div> </div>
</Scroller> </Scroller>
@ -192,16 +189,14 @@ VirtualTable.propTypes = {
scroller: PropTypes.instanceOf(Element).isRequired, scroller: PropTypes.instanceOf(Element).isRequired,
header: PropTypes.node.isRequired, header: PropTypes.node.isRequired,
headerHeight: PropTypes.number.isRequired, headerHeight: PropTypes.number.isRequired,
rowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]).isRequired,
rowRenderer: PropTypes.func.isRequired, rowRenderer: PropTypes.func.isRequired,
onRecompute: PropTypes.func.isRequired rowHeight: PropTypes.number.isRequired
}; };
VirtualTable.defaultProps = { VirtualTable.defaultProps = {
className: styles.tableContainer, className: styles.tableContainer,
headerHeight: 38, headerHeight: 38,
rowHeight: 38, rowHeight: ROW_HEIGHT
onRecompute: () => {}
}; };
export default VirtualTable; export default VirtualTable;

@ -1,24 +1,30 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
function withScrollPosition(WrappedComponent, scrollPositionKey) { interface WrappedComponentProps {
function ScrollPosition(props) { 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 { history } = props;
const initialScrollTop = const initialScrollTop =
history.action === 'POP' || history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
(history.location.state && history.location.state.restoreScrollPosition)
? scrollPositions[scrollPositionKey]
: 0;
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />; return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
} }
ScrollPosition.propTypes = {
history: PropTypes.object.isRequired,
};
return ScrollPosition; 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", "jsx": "react",
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"typeRoots": ["node_modules/@types", "typings"], "typeRoots": ["node_modules/@types", "typings"],
"paths": { "paths": {

@ -680,6 +680,7 @@
"Monitored": "Monitored", "Monitored": "Monitored",
"MonitoredHelpText": "Download monitored albums from this artist", "MonitoredHelpText": "Download monitored albums from this artist",
"MonitoredOnly": "Monitored Only", "MonitoredOnly": "Monitored Only",
"MonitoredStatus": "Monitored/Status",
"Monitoring": "Monitoring", "Monitoring": "Monitoring",
"MonitoringOptions": "Monitoring Options", "MonitoringOptions": "Monitoring Options",
"MonitoringOptionsHelpText": "Which albums should be monitored after the artist is added (one-time adjustment)", "MonitoringOptionsHelpText": "Which albums should be monitored after the artist is added (one-time adjustment)",

Loading…
Cancel
Save