Added artists index selection

(cherry picked from commit 815a16d5cfced17ca4db7f1b66991c5cc9f3b719)
pull/4254/head
Mark McDowall 1 year ago committed by Bogdan
parent d31c323f3c
commit 84d5f2bcee

@ -0,0 +1,170 @@
import { cloneDeep } from 'lodash';
import React, { useEffect } from 'react';
import areAllSelected from 'Utilities/Table/areAllSelected';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import ModelBase from './ModelBase';
export enum SelectActionType {
Reset,
SelectAll,
UnselectAll,
ToggleSelected,
RemoveItem,
UpdateItems,
}
type SelectedState = Record<number, boolean>;
interface SelectState {
selectedState: SelectedState;
lastToggled: number | null;
allSelected: boolean;
allUnselected: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items: any[];
}
type SelectAction =
| { type: SelectActionType.Reset }
| { type: SelectActionType.SelectAll }
| { type: SelectActionType.UnselectAll }
| {
type: SelectActionType.ToggleSelected;
id: number;
isSelected: boolean;
shiftKey: boolean;
}
| {
type: SelectActionType.RemoveItem;
id: number;
}
| {
type: SelectActionType.UpdateItems;
items: ModelBase[];
};
type Dispatch = (action: SelectAction) => void;
const initialState = {
selectedState: {},
lastToggled: null,
allSelected: false,
allUnselected: true,
items: [],
};
interface SelectProviderOptions<T extends ModelBase> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children: any;
isSelectMode: boolean;
items: Array<T>;
}
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
return items.reduce((acc: SelectedState, item) => {
const id = item.id;
acc[id] = existingState[id] ?? false;
return acc;
}, {});
}
// TODO: Can this be reused?
const SelectContext = React.createContext<[SelectState, Dispatch] | undefined>(
cloneDeep(undefined)
);
function selectReducer(state: SelectState, action: SelectAction): SelectState {
const { items, selectedState } = state;
switch (action.type) {
case SelectActionType.Reset: {
return cloneDeep(initialState);
}
case SelectActionType.SelectAll: {
return {
items,
...selectAll(selectedState, true),
};
}
case SelectActionType.UnselectAll: {
return {
items,
...selectAll(selectedState, false),
};
}
case SelectActionType.ToggleSelected: {
var result = {
items,
...toggleSelected(
state,
items,
action.id,
action.isSelected,
action.shiftKey
),
};
return result;
}
case SelectActionType.UpdateItems: {
const nextSelectedState = getSelectedState(action.items, selectedState);
return {
...state,
...areAllSelected(nextSelectedState),
selectedState: nextSelectedState,
items,
};
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
export function SelectProvider<T extends ModelBase>(
props: SelectProviderOptions<T>
) {
const { isSelectMode, items } = props;
const selectedState = getSelectedState(items, {});
const [state, dispatch] = React.useReducer(selectReducer, {
selectedState,
lastToggled: null,
allSelected: false,
allUnselected: true,
items,
});
const value: [SelectState, Dispatch] = [state, dispatch];
useEffect(() => {
if (!isSelectMode) {
dispatch({ type: SelectActionType.Reset });
}
}, [isSelectMode]);
useEffect(() => {
dispatch({ type: SelectActionType.UpdateItems, items });
}, [items]);
return (
<SelectContext.Provider value={value}>
{props.children}
</SelectContext.Provider>
);
}
export function useSelect() {
const context = React.useContext(SelectContext);
if (context === undefined) {
throw new Error('useSelect must be used within a SelectProvider');
}
return context;
}

@ -1,5 +1,6 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import NoArtist from 'Artist/NoArtist';
import { REFRESH_ARTIST, RSS_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -37,6 +38,7 @@ import ArtistIndexOverviews from './Overview/ArtistIndexOverviews';
import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal';
import ArtistIndexPosters from './Posters/ArtistIndexPosters';
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
import ArtistIndexSelectAllButton from './Select/ArtistIndexSelectAllButton';
import ArtistIndexTable from './Table/ArtistIndexTable';
import ArtistIndexTableOptions from './Table/ArtistIndexTableOptions';
import styles from './ArtistIndex.css';
@ -88,6 +90,7 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
const scrollerRef = useRef<HTMLDivElement>();
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [isSelectMode, setIsSelectMode] = useState(false);
const onRefreshArtistPress = useCallback(() => {
dispatch(
@ -105,6 +108,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
);
}, [dispatch]);
const onSelectModePress = useCallback(() => {
setIsSelectMode(!isSelectMode);
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback(
(payload) => {
dispatch(setArtistTableOption(payload));
@ -202,131 +209,150 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
const hasNoArtist = !totalItems;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('UpdateAll')}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isRefreshingArtist}
isDisabled={hasNoArtist}
onPress={onRefreshArtistPress}
/>
<SelectProvider isSelectMode={isSelectMode} items={items}>
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('UpdateAll')}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isRefreshingArtist}
isDisabled={hasNoArtist}
onPress={onRefreshArtistPress}
/>
<PageToolbarButton
label={translate('RSSSync')}
iconName={icons.RSS}
isSpinning={isRssSyncExecuting}
isDisabled={hasNoArtist}
onPress={onRssSyncPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
{view === 'table' ? (
<TableOptionsModalWrapper
columns={columns}
optionsComponent={ArtistIndexTableOptions}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
) : (
<PageToolbarButton
label={translate('Options')}
iconName={view === 'posters' ? icons.POSTER : icons.OVERVIEW}
label={translate('RSSSync')}
iconName={icons.RSS}
isSpinning={isRssSyncExecuting}
isDisabled={hasNoArtist}
onPress={onOptionsPress}
onPress={onRssSyncPress}
/>
)}
<PageToolbarSeparator />
<PageToolbarSeparator />
<ArtistIndexViewMenu
view={view}
isDisabled={hasNoArtist}
onViewSelect={onViewSelect}
/>
<PageToolbarButton
label={isSelectMode ? 'Stop Selecting' : 'Select Artists'}
iconName={isSelectMode ? icons.ARTIST_ENDED : icons.CHECK}
onPress={onSelectModePress}
/>
<ArtistIndexSortMenu
sortKey={sortKey}
sortDirection={sortDirection}
isDisabled={hasNoArtist}
onSortSelect={onSortSelect}
/>
{isSelectMode ? <ArtistIndexSelectAllButton /> : null}
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
{view === 'table' ? (
<TableOptionsModalWrapper
columns={columns}
optionsComponent={ArtistIndexTableOptions}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
) : (
<PageToolbarButton
label={translate('Options')}
iconName={view === 'posters' ? icons.POSTER : icons.OVERVIEW}
isDisabled={hasNoArtist}
onPress={onOptionsPress}
/>
)}
<ArtistIndexFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
isDisabled={hasNoArtist}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}
>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<div className={styles.errorMessage}>
{getErrorMessage(error, 'Failed to load artist from API')}
</div>
) : null}
<PageToolbarSeparator />
{isLoaded ? (
<div className={styles.contentBodyContainer}>
<ViewComponent
scrollerRef={scrollerRef}
items={items}
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
isSmallScreen={isSmallScreen}
/>
<ArtistIndexViewMenu
view={view}
isDisabled={hasNoArtist}
onViewSelect={onViewSelect}
/>
<ArtistIndexFooter />
</div>
) : null}
<ArtistIndexSortMenu
sortKey={sortKey}
sortDirection={sortDirection}
isDisabled={hasNoArtist}
onSortSelect={onSortSelect}
/>
{!error && isPopulated && !items.length ? (
<NoArtist totalItems={totalItems} />
<ArtistIndexFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
isDisabled={hasNoArtist}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}
>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<div className={styles.errorMessage}>
{getErrorMessage(error, 'Failed to load artist from API')}
</div>
) : null}
{isLoaded ? (
<div className={styles.contentBodyContainer}>
<ViewComponent
scrollerRef={scrollerRef}
items={items}
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
isSelectMode={isSelectMode}
isSmallScreen={isSmallScreen}
/>
<ArtistIndexFooter />
</div>
) : null}
{!error && isPopulated && !items.length ? (
<NoArtist totalItems={totalItems} />
) : null}
</PageContentBody>
{isLoaded && !!jumpBarItems.order.length ? (
<PageJumpBar
items={jumpBarItems}
onItemPress={onJumpBarItemPress}
/>
) : null}
</PageContentBody>
{isLoaded && !!jumpBarItems.order.length ? (
<PageJumpBar items={jumpBarItems} onItemPress={onJumpBarItemPress} />
</div>
{view === 'posters' ? (
<ArtistIndexPosterOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={onOptionsModalClose}
/>
) : null}
{view === 'banners' ? (
<ArtistIndexBannerOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={onOptionsModalClose}
/>
) : null}
{view === 'overview' ? (
<ArtistIndexOverviewOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={onOptionsModalClose}
/>
) : null}
</div>
{view === 'posters' ? (
<ArtistIndexPosterOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={onOptionsModalClose}
/>
) : null}
{view === 'banners' ? (
<ArtistIndexBannerOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={onOptionsModalClose}
/>
) : null}
{view === 'overview' ? (
<ArtistIndexOverviewOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={onOptionsModalClose}
/>
) : null}
</PageContent>
</PageContent>
</SelectProvider>
);
}, 'artistIndex');

@ -7,6 +7,7 @@ import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import ArtistIndexBannerInfo from 'Artist/Index/Banners/ArtistIndexBannerInfo';
import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector';
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
import ArtistIndexPosterSelect from 'Artist/Index/Select/ArtistIndexPosterSelect';
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
@ -23,12 +24,13 @@ import styles from './ArtistIndexBanner.css';
interface ArtistIndexBannerProps {
artistId: number;
sortKey: string;
isSelectMode: boolean;
bannerWidth: number;
bannerHeight: number;
}
function ArtistIndexBanner(props: ArtistIndexBannerProps) {
const { artistId, sortKey, bannerWidth, bannerHeight } = props;
const { artistId, sortKey, isSelectMode, bannerWidth, bannerHeight } = props;
const {
artist,
@ -130,6 +132,8 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
return (
<div className={styles.content}>
<div className={styles.bannerContainer}>
{isSelectMode ? <ArtistIndexPosterSelect artistId={artistId} /> : null}
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}

@ -36,6 +36,7 @@ interface CellItemData {
};
items: Artist[];
sortKey: string;
isSelectMode: boolean;
}
interface ArtistIndexBannersProps {
@ -45,6 +46,7 @@ interface ArtistIndexBannersProps {
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -63,10 +65,8 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
style,
data,
}) => {
const { layout, items, sortKey } = data;
const { layout, items, sortKey, isSelectMode } = data;
const { columnCount, padding, bannerWidth, bannerHeight } = layout;
const index = rowIndex * columnCount + columnIndex;
if (index >= items.length) {
@ -85,6 +85,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
<ArtistIndexBanner
artistId={artist.id}
sortKey={sortKey}
isSelectMode={isSelectMode}
bannerWidth={bannerWidth}
bannerHeight={bannerHeight}
/>
@ -97,7 +98,14 @@ function getWindowScrollTopPosition() {
}
export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
const { scrollerRef, items, sortKey, jumpToCharacter, isSmallScreen } = props;
const {
scrollerRef,
items,
sortKey,
jumpToCharacter,
isSelectMode,
isSmallScreen,
} = props;
const { bannerOptions } = useSelector(artistIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef();
@ -285,6 +293,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
},
items,
sortKey,
isSelectMode,
}}
>
{Cell}

@ -6,6 +6,7 @@ import ArtistPoster from 'Artist/ArtistPoster';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
import ArtistIndexPosterSelect from 'Artist/Index/Select/ArtistIndexPosterSelect';
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
@ -37,6 +38,7 @@ interface ArtistIndexOverviewProps {
posterWidth: number;
posterHeight: number;
rowHeight: number;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -47,6 +49,7 @@ function ArtistIndexOverview(props: ArtistIndexOverviewProps) {
posterWidth,
posterHeight,
rowHeight,
isSelectMode,
isSmallScreen,
} = props;
@ -136,6 +139,10 @@ function ArtistIndexOverview(props: ArtistIndexOverviewProps) {
<div className={styles.content}>
<div className={styles.poster}>
<div className={styles.posterContainer}>
{isSelectMode ? (
<ArtistIndexPosterSelect artistId={artistId} />
) : null}
{status === 'ended' && (
<div className={styles.ended} title={translate('Inactive')} />
)}

@ -27,6 +27,7 @@ interface RowItemData {
posterWidth: number;
posterHeight: number;
rowHeight: number;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -37,6 +38,7 @@ interface ArtistIndexOverviewsProps {
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -65,7 +67,14 @@ function getWindowScrollTopPosition() {
}
function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
const { items, sortKey, jumpToCharacter, isSmallScreen, scrollerRef } = props;
const {
items,
sortKey,
jumpToCharacter,
scrollerRef,
isSelectMode,
isSmallScreen,
} = props;
const { size: posterSize, detailedProgressBar } = useSelector(
selectOverviewOptions
@ -191,6 +200,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
posterWidth,
posterHeight,
rowHeight,
isSelectMode,
isSmallScreen,
}}
>

@ -7,6 +7,7 @@ import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector';
import ArtistIndexPosterInfo from 'Artist/Index/Posters/ArtistIndexPosterInfo';
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
import ArtistIndexPosterSelect from 'Artist/Index/Select/ArtistIndexPosterSelect';
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
@ -23,12 +24,13 @@ import styles from './ArtistIndexPoster.css';
interface ArtistIndexPosterProps {
artistId: number;
sortKey: string;
isSelectMode: boolean;
posterWidth: number;
posterHeight: number;
}
function ArtistIndexPoster(props: ArtistIndexPosterProps) {
const { artistId, sortKey, posterWidth, posterHeight } = props;
const { artistId, sortKey, isSelectMode, posterWidth, posterHeight } = props;
const {
artist,
@ -130,6 +132,8 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
{isSelectMode ? <ArtistIndexPosterSelect artistId={artistId} /> : null}
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}

@ -36,6 +36,7 @@ interface CellItemData {
};
items: Artist[];
sortKey: string;
isSelectMode: boolean;
}
interface ArtistIndexPostersProps {
@ -45,6 +46,7 @@ interface ArtistIndexPostersProps {
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -63,10 +65,8 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
style,
data,
}) => {
const { layout, items, sortKey } = data;
const { layout, items, sortKey, isSelectMode } = data;
const { columnCount, padding, posterWidth, posterHeight } = layout;
const index = rowIndex * columnCount + columnIndex;
if (index >= items.length) {
@ -85,6 +85,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
<ArtistIndexPoster
artistId={artist.id}
sortKey={sortKey}
isSelectMode={isSelectMode}
posterWidth={posterWidth}
posterHeight={posterHeight}
/>
@ -97,7 +98,14 @@ function getWindowScrollTopPosition() {
}
export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
const { scrollerRef, items, sortKey, jumpToCharacter, isSmallScreen } = props;
const {
scrollerRef,
items,
sortKey,
jumpToCharacter,
isSelectMode,
isSmallScreen,
} = props;
const { posterOptions } = useSelector(artistIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef();
@ -285,6 +293,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
},
items,
sortKey,
isSelectMode,
}}
>
{Cell}

@ -0,0 +1,36 @@
.checkContainer {
position: absolute;
top: 10px;
left: 10px;
z-index: 3;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: var(--defaultColor);
}
.icon {
position: absolute;
top: -1px;
left: -1px;
}
.selected {
composes: icon;
color: var(--lidarrGreen);
&:hover {
color: var(--white);
}
}
.unselected {
composes: icon;
color: var(--white);
&:hover {
color: var(--lidarrGreen);
}
}

@ -0,0 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'checkContainer': string;
'icon': string;
'selected': string;
'unselected': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -0,0 +1,41 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import styles from './ArtistIndexPosterSelect.css';
interface ArtistIndexPosterSelectProps {
artistId: number;
}
function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
const { artistId } = props;
const [selectState, selectDispatch] = useSelect();
const isSelected = selectState.selectedState[artistId];
const onSelectPress = useCallback(
(event) => {
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: SelectActionType.ToggleSelected,
id: artistId,
isSelected: !isSelected,
shiftKey,
});
},
[artistId, isSelected, selectDispatch]
);
return (
<IconButton
className={styles.checkContainer}
iconClassName={isSelected ? styles.selected : styles.unselected}
name={isSelected ? icons.CHECK_CIRCLE : icons.CIRCLE_OUTLINE}
size={20}
onPress={onSelectPress}
/>
);
}
export default ArtistIndexPosterSelect;

@ -0,0 +1,35 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
function ArtistIndexSelectAllButton() {
const [selectState, selectDispatch] = useSelect();
const { allSelected, allUnselected } = selectState;
let icon = icons.SQUARE_MINUS;
if (allSelected) {
icon = icons.CHECK_SQUARE;
} else if (allUnselected) {
icon = icons.SQUARE;
}
const onPress = useCallback(() => {
selectDispatch({
type: allSelected
? SelectActionType.UnselectAll
: SelectActionType.SelectAll,
});
}, [allSelected, selectDispatch]);
return (
<PageToolbarButton
label={allSelected ? 'Unselect All' : 'Select All'}
iconName={icon}
onPress={onPress}
/>
);
}
export default ArtistIndexSelectAllButton;

@ -2,6 +2,7 @@ import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AlbumTitleLink from 'Album/AlbumTitleLink';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { Statistics } from 'Artist/Artist';
import ArtistBanner from 'Artist/ArtistBanner';
import ArtistNameLink from 'Artist/ArtistNameLink';
@ -17,6 +18,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
@ -32,10 +34,11 @@ interface ArtistIndexRowProps {
artistId: number;
sortKey: string;
columns: Column[];
isSelectMode: boolean;
}
function ArtistIndexRow(props: ArtistIndexRowProps) {
const { artistId, columns } = props;
const { artistId, columns, isSelectMode } = props;
const {
artist,
@ -77,6 +80,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
const [hasBannerError, setHasBannerError] = useState(false);
const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false);
const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false);
const [selectState, selectDispatch] = useSelect();
const onRefreshPress = useCallback(() => {
dispatch(
@ -121,8 +125,29 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
setIsDeleteArtistModalOpen(false);
}, [setIsDeleteArtistModalOpen]);
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
selectDispatch({
type: SelectActionType.ToggleSelected,
id,
isSelected: value,
shiftKey,
});
},
[selectDispatch]
);
return (
<>
{isSelectMode ? (
<VirtualTableSelectCell
id={artistId}
isSelected={selectState.selectedState[artistId]}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
) : null}
{columns.map((column) => {
const { name, isVisible } = column;

@ -25,6 +25,7 @@ interface RowItemData {
items: Artist[];
sortKey: string;
columns: Column[];
isSelectMode: boolean;
}
interface ArtistIndexTableProps {
@ -34,6 +35,7 @@ interface ArtistIndexTableProps {
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -47,7 +49,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
style,
data,
}) => {
const { items, sortKey, columns } = data;
const { items, sortKey, columns, isSelectMode } = data;
if (index >= items.length) {
return null;
@ -67,6 +69,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
artistId={artist.id}
sortKey={sortKey}
columns={columns}
isSelectMode={isSelectMode}
/>
</div>
);
@ -82,6 +85,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
sortKey,
sortDirection,
jumpToCharacter,
isSelectMode,
isSmallScreen,
scrollerRef,
} = props;
@ -177,6 +181,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
isSelectMode={isSelectMode}
/>
<List<RowItemData>
ref={listRef}
@ -193,6 +198,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
items,
sortKey,
columns,
isSelectMode,
}}
>
{Row}

@ -1,12 +1,14 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import ArtistIndexTableOptions from 'Artist/Index/Table/ArtistIndexTableOptions';
import IconButton from 'Components/Link/IconButton';
import Column from 'Components/Table/Column';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
@ -21,12 +23,13 @@ interface ArtistIndexTableHeaderProps {
columns: Column[];
sortKey?: string;
sortDirection?: SortDirection;
isSelectMode: boolean;
}
function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
const { showBanners, columns, sortKey, sortDirection } = props;
const { showBanners, columns, sortKey, sortDirection, isSelectMode } = props;
const dispatch = useDispatch();
const [selectState, selectDispatch] = useSelect();
const onSortPress = useCallback(
(value) => {
@ -42,8 +45,25 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
[dispatch]
);
const onSelectAllChange = useCallback(
({ value }) => {
selectDispatch({
type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll,
});
},
[selectDispatch]
);
return (
<VirtualTableHeader>
{isSelectMode ? (
<VirtualTableSelectAllHeaderCell
allSelected={selectState.allSelected}
allUnselected={selectState.allUnselected}
onSelectAllChange={onSelectAllChange}
/>
) : null}
{columns.map((column) => {
const { name, label, isSortable, isVisible } = column;

@ -15,7 +15,8 @@ import {
faHdd as farHdd,
faKeyboard as farKeyboard,
faObjectGroup as farObjectGroup,
faObjectUngroup as farObjectUngroup
faObjectUngroup as farObjectUngroup,
faSquare as farSquare
} from '@fortawesome/free-regular-svg-icons';
//
// Solid
@ -90,6 +91,8 @@ import {
faSortDown as fasSortDown,
faSortUp as fasSortUp,
faSpinner as fasSpinner,
faSquareCheck as fasSquareCheck,
faSquareMinus as fasSquareMinus,
faStar as fasStar,
faStop as fasStop,
faSync as fasSync,
@ -128,6 +131,7 @@ export const CARET_DOWN = fasCaretDown;
export const CHECK = fasCheck;
export const CHECK_INDETERMINATE = fasMinus;
export const CHECK_CIRCLE = fasCheckCircle;
export const CHECK_SQUARE = fasSquareCheck;
export const CIRCLE = fasCircle;
export const CIRCLE_OUTLINE = farCircle;
export const CLEAR = fasTrashAlt;
@ -205,6 +209,8 @@ export const SORT = fasSort;
export const SORT_ASCENDING = fasSortUp;
export const SORT_DESCENDING = fasSortDown;
export const SPINNER = fasSpinner;
export const SQUARE = farSquare;
export const SQUARE_MINUS = fasSquareMinus;
export const STAR_FULL = fasStar;
export const SUBTRACT = fasMinus;
export const SYSTEM = fasLaptop;

@ -1,29 +1,29 @@
import areAllSelected from './areAllSelected';
import getToggledRange from './getToggledRange';
function toggleSelected(state, items, id, selected, shiftKey) {
const lastToggled = state.lastToggled;
const selectedState = {
...state.selectedState,
function toggleSelected(selectedState, items, id, selected, shiftKey) {
const lastToggled = selectedState.lastToggled;
const nextSelectedState = {
...selectedState.selectedState,
[id]: selected
};
if (selected == null) {
delete selectedState[id];
delete nextSelectedState[id];
}
if (shiftKey && lastToggled) {
const { lower, upper } = getToggledRange(items, id, lastToggled);
for (let i = lower; i < upper; i++) {
selectedState[items[i].id] = selected;
nextSelectedState[items[i].id] = selected;
}
}
return {
...areAllSelected(selectedState),
...areAllSelected(nextSelectedState),
lastToggled: id,
selectedState
selectedState: nextSelectedState
};
}

Loading…
Cancel
Save