mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-16 10:03:51 -07:00
Extract useSelectState from SelectContext
(cherry picked from commit 032d9a720c89286dc8c1931775144f0a65a6149e)
This commit is contained in:
parent
552c70ec6f
commit
74d2b4e0dc
9 changed files with 51 additions and 135 deletions
|
@ -1,58 +1,28 @@
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import areAllSelected from 'Utilities/Table/areAllSelected';
|
import useSelectState, { SelectState } from 'Helpers/Hooks/useSelectState';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import ModelBase from './ModelBase';
|
import ModelBase from './ModelBase';
|
||||||
|
|
||||||
export enum SelectActionType {
|
export type SelectContextAction =
|
||||||
Reset,
|
| { type: 'reset' }
|
||||||
SelectAll,
|
| { type: 'selectAll' }
|
||||||
UnselectAll,
|
| { type: '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;
|
type: 'toggleSelected';
|
||||||
id: number;
|
id: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
shiftKey: boolean;
|
shiftKey: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: SelectActionType.RemoveItem;
|
type: 'removeItem';
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: SelectActionType.UpdateItems;
|
type: 'updateItems';
|
||||||
items: ModelBase[];
|
items: ModelBase[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Dispatch = (action: SelectAction) => void;
|
export type SelectDispatch = (action: SelectContextAction) => void;
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
selectedState: {},
|
|
||||||
lastToggled: null,
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: true,
|
|
||||||
items: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SelectProviderOptions<T extends ModelBase> {
|
interface SelectProviderOptions<T extends ModelBase> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -60,90 +30,40 @@ interface SelectProviderOptions<T extends ModelBase> {
|
||||||
items: Array<T>;
|
items: Array<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
|
const SelectContext = React.createContext<
|
||||||
return items.reduce((acc: SelectedState, item) => {
|
[SelectState, SelectDispatch] | undefined
|
||||||
const id = item.id;
|
>(cloneDeep(undefined));
|
||||||
|
|
||||||
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: {
|
|
||||||
const 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: action.items,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(`Unhandled action type: ${action.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SelectProvider<T extends ModelBase>(
|
export function SelectProvider<T extends ModelBase>(
|
||||||
props: SelectProviderOptions<T>
|
props: SelectProviderOptions<T>
|
||||||
) {
|
) {
|
||||||
const { items } = props;
|
const { items } = props;
|
||||||
const selectedState = getSelectedState(items, {});
|
const [state, dispatch] = useSelectState();
|
||||||
|
|
||||||
const [state, dispatch] = React.useReducer(selectReducer, {
|
const dispatchWrapper = useCallback(
|
||||||
selectedState,
|
(action: SelectContextAction) => {
|
||||||
lastToggled: null,
|
switch (action.type) {
|
||||||
allSelected: false,
|
case 'reset':
|
||||||
allUnselected: true,
|
case 'removeItem':
|
||||||
items,
|
dispatch(action);
|
||||||
});
|
break;
|
||||||
|
|
||||||
const value: [SelectState, Dispatch] = [state, dispatch];
|
default:
|
||||||
|
dispatch({
|
||||||
|
...action,
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value: [SelectState, SelectDispatch] = [state, dispatchWrapper];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({ type: SelectActionType.UpdateItems, items });
|
dispatch({ type: 'updateItems', items });
|
||||||
}, [items]);
|
}, [items, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectContext.Provider value={value}>
|
<SelectContext.Provider value={value}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import styles from './ArtistIndexPosterSelect.css';
|
import styles from './ArtistIndexPosterSelect.css';
|
||||||
|
@ -18,7 +18,7 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: SelectActionType.ToggleSelected,
|
type: 'toggleSelected',
|
||||||
id: artistId,
|
id: artistId,
|
||||||
isSelected: !isSelected,
|
isSelected: !isSelected,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
|
||||||
|
@ -24,9 +24,7 @@ function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: allSelected
|
type: allSelected ? 'unselectAll' : 'selectAll',
|
||||||
? SelectActionType.UnselectAll
|
|
||||||
: SelectActionType.SelectAll,
|
|
||||||
});
|
});
|
||||||
}, [allSelected, selectDispatch]);
|
}, [allSelected, selectDispatch]);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
|
||||||
|
@ -25,9 +25,7 @@ function ArtistIndexSelectAllMenuItem(
|
||||||
|
|
||||||
const onPressWrapper = useCallback(() => {
|
const onPressWrapper = useCallback(() => {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: allSelected
|
type: allSelected ? 'unselectAll' : 'selectAll',
|
||||||
? SelectActionType.UnselectAll
|
|
||||||
: SelectActionType.SelectAll,
|
|
||||||
});
|
});
|
||||||
}, [allSelected, selectDispatch]);
|
}, [allSelected, selectDispatch]);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import { RENAME_ARTIST, RETAG_ARTIST } from 'Commands/commandNames';
|
import { RENAME_ARTIST, RETAG_ARTIST } from 'Commands/commandNames';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
@ -172,7 +172,7 @@ function ArtistIndexSelectFooter() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDeleting && !deleteError) {
|
if (!isDeleting && !deleteError) {
|
||||||
selectDispatch({ type: SelectActionType.UnselectAll });
|
selectDispatch({ type: 'unselectAll' });
|
||||||
}
|
}
|
||||||
}, [isDeleting, deleteError, selectDispatch]);
|
}, [isDeleting, deleteError, selectDispatch]);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
|
||||||
interface ArtistIndexSelectModeButtonProps {
|
interface ArtistIndexSelectModeButtonProps {
|
||||||
|
@ -18,7 +18,7 @@ function ArtistIndexSelectModeButton(props: ArtistIndexSelectModeButtonProps) {
|
||||||
const onPressWrapper = useCallback(() => {
|
const onPressWrapper = useCallback(() => {
|
||||||
if (isSelectMode) {
|
if (isSelectMode) {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: SelectActionType.Reset,
|
type: 'reset',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||||
|
|
||||||
interface ArtistIndexSelectModeMenuItemProps {
|
interface ArtistIndexSelectModeMenuItemProps {
|
||||||
|
@ -19,7 +19,7 @@ function ArtistIndexSelectModeMenuItem(
|
||||||
const onPressWrapper = useCallback(() => {
|
const onPressWrapper = useCallback(() => {
|
||||||
if (isSelectMode) {
|
if (isSelectMode) {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: SelectActionType.Reset,
|
type: 'reset',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +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 { 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';
|
||||||
|
@ -129,7 +129,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
const onSelectedChange = useCallback(
|
const onSelectedChange = useCallback(
|
||||||
({ id, value, shiftKey }) => {
|
({ id, value, shiftKey }) => {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: SelectActionType.ToggleSelected,
|
type: 'toggleSelected',
|
||||||
id,
|
id,
|
||||||
isSelected: value,
|
isSelected: value,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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 { 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';
|
||||||
|
@ -48,7 +48,7 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }) => {
|
({ value }) => {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll,
|
type: value ? 'selectAll' : 'unselectAll',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[selectDispatch]
|
[selectDispatch]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue