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 React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import NoArtist from 'Artist/NoArtist'; import NoArtist from 'Artist/NoArtist';
import { REFRESH_ARTIST, RSS_SYNC } from 'Commands/commandNames'; import { REFRESH_ARTIST, RSS_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -37,6 +38,7 @@ import ArtistIndexOverviews from './Overview/ArtistIndexOverviews';
import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal'; import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal';
import ArtistIndexPosters from './Posters/ArtistIndexPosters'; import ArtistIndexPosters from './Posters/ArtistIndexPosters';
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal'; import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
import ArtistIndexSelectAllButton from './Select/ArtistIndexSelectAllButton';
import ArtistIndexTable from './Table/ArtistIndexTable'; import ArtistIndexTable from './Table/ArtistIndexTable';
import ArtistIndexTableOptions from './Table/ArtistIndexTableOptions'; import ArtistIndexTableOptions from './Table/ArtistIndexTableOptions';
import styles from './ArtistIndex.css'; import styles from './ArtistIndex.css';
@ -88,6 +90,7 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
const scrollerRef = useRef<HTMLDivElement>(); const scrollerRef = useRef<HTMLDivElement>();
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false); const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null); const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [isSelectMode, setIsSelectMode] = useState(false);
const onRefreshArtistPress = useCallback(() => { const onRefreshArtistPress = useCallback(() => {
dispatch( dispatch(
@ -105,6 +108,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
); );
}, [dispatch]); }, [dispatch]);
const onSelectModePress = useCallback(() => {
setIsSelectMode(!isSelectMode);
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback( const onTableOptionChange = useCallback(
(payload) => { (payload) => {
dispatch(setArtistTableOption(payload)); dispatch(setArtistTableOption(payload));
@ -202,6 +209,7 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
const hasNoArtist = !totalItems; const hasNoArtist = !totalItems;
return ( return (
<SelectProvider isSelectMode={isSelectMode} items={items}>
<PageContent> <PageContent>
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
@ -221,9 +229,22 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
isDisabled={hasNoArtist} isDisabled={hasNoArtist}
onPress={onRssSyncPress} onPress={onRssSyncPress}
/> />
<PageToolbarSeparator />
<PageToolbarButton
label={isSelectMode ? 'Stop Selecting' : 'Select Artists'}
iconName={isSelectMode ? icons.ARTIST_ENDED : icons.CHECK}
onPress={onSelectModePress}
/>
{isSelectMode ? <ArtistIndexSelectAllButton /> : null}
</PageToolbarSection> </PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}> <PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
{view === 'table' ? ( {view === 'table' ? (
<TableOptionsModalWrapper <TableOptionsModalWrapper
columns={columns} columns={columns}
@ -292,6 +313,7 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter} jumpToCharacter={jumpToCharacter}
isSelectMode={isSelectMode}
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
/> />
@ -305,7 +327,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
</PageContentBody> </PageContentBody>
{isLoaded && !!jumpBarItems.order.length ? ( {isLoaded && !!jumpBarItems.order.length ? (
<PageJumpBar items={jumpBarItems} onItemPress={onJumpBarItemPress} /> <PageJumpBar
items={jumpBarItems}
onItemPress={onJumpBarItemPress}
/>
) : null} ) : null}
</div> </div>
{view === 'posters' ? ( {view === 'posters' ? (
@ -327,6 +352,7 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
/> />
) : null} ) : null}
</PageContent> </PageContent>
</SelectProvider>
); );
}, 'artistIndex'); }, 'artistIndex');

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

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

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

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

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

@ -36,6 +36,7 @@ interface CellItemData {
}; };
items: Artist[]; items: Artist[];
sortKey: string; sortKey: string;
isSelectMode: boolean;
} }
interface ArtistIndexPostersProps { interface ArtistIndexPostersProps {
@ -45,6 +46,7 @@ interface ArtistIndexPostersProps {
jumpToCharacter?: string; jumpToCharacter?: string;
scrollTop?: number; scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>; scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean; isSmallScreen: boolean;
} }
@ -63,10 +65,8 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
style, style,
data, data,
}) => { }) => {
const { layout, items, sortKey } = data; const { layout, items, sortKey, isSelectMode } = data;
const { columnCount, padding, posterWidth, posterHeight } = layout; const { columnCount, padding, posterWidth, posterHeight } = layout;
const index = rowIndex * columnCount + columnIndex; const index = rowIndex * columnCount + columnIndex;
if (index >= items.length) { if (index >= items.length) {
@ -85,6 +85,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
<ArtistIndexPoster <ArtistIndexPoster
artistId={artist.id} artistId={artist.id}
sortKey={sortKey} sortKey={sortKey}
isSelectMode={isSelectMode}
posterWidth={posterWidth} posterWidth={posterWidth}
posterHeight={posterHeight} posterHeight={posterHeight}
/> />
@ -97,7 +98,14 @@ function getWindowScrollTopPosition() {
} }
export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { 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 { posterOptions } = useSelector(artistIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef(); const ref: React.MutableRefObject<Grid> = useRef();
@ -285,6 +293,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
}, },
items, items,
sortKey, sortKey,
isSelectMode,
}} }}
> >
{Cell} {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 React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import AlbumTitleLink from 'Album/AlbumTitleLink'; import AlbumTitleLink from 'Album/AlbumTitleLink';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { Statistics } from 'Artist/Artist'; import { Statistics } from 'Artist/Artist';
import ArtistBanner from 'Artist/ArtistBanner'; import ArtistBanner from 'Artist/ArtistBanner';
import ArtistNameLink from 'Artist/ArtistNameLink'; import ArtistNameLink from 'Artist/ArtistNameLink';
@ -17,6 +18,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column'; 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';
@ -32,10 +34,11 @@ interface ArtistIndexRowProps {
artistId: number; artistId: number;
sortKey: string; sortKey: string;
columns: Column[]; columns: Column[];
isSelectMode: boolean;
} }
function ArtistIndexRow(props: ArtistIndexRowProps) { function ArtistIndexRow(props: ArtistIndexRowProps) {
const { artistId, columns } = props; const { artistId, columns, isSelectMode } = props;
const { const {
artist, artist,
@ -77,6 +80,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
const [hasBannerError, setHasBannerError] = useState(false); const [hasBannerError, setHasBannerError] = useState(false);
const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false); const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false);
const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false); const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false);
const [selectState, selectDispatch] = useSelect();
const onRefreshPress = useCallback(() => { const onRefreshPress = useCallback(() => {
dispatch( dispatch(
@ -121,8 +125,29 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
setIsDeleteArtistModalOpen(false); setIsDeleteArtistModalOpen(false);
}, [setIsDeleteArtistModalOpen]); }, [setIsDeleteArtistModalOpen]);
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
selectDispatch({
type: SelectActionType.ToggleSelected,
id,
isSelected: value,
shiftKey,
});
},
[selectDispatch]
);
return ( return (
<> <>
{isSelectMode ? (
<VirtualTableSelectCell
id={artistId}
isSelected={selectState.selectedState[artistId]}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
) : null}
{columns.map((column) => { {columns.map((column) => {
const { name, isVisible } = column; const { name, isVisible } = column;

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

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

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

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

Loading…
Cancel
Save