mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-15 01:23:53 -07:00
Added artists index selection
(cherry picked from commit 815a16d5cfced17ca4db7f1b66991c5cc9f3b719)
This commit is contained in:
parent
d31c323f3c
commit
84d5f2bcee
17 changed files with 553 additions and 135 deletions
170
frontend/src/App/SelectContext.tsx
Normal file
170
frontend/src/App/SelectContext.tsx
Normal file
|
@ -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}
|
||||||
|
|
36
frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css
Normal file
36
frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
10
frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css.d.ts
vendored
Normal file
10
frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css.d.ts
vendored
Normal file
|
@ -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;
|
41
frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx
Normal file
41
frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue