Extract useSelectState from SelectContext

(cherry picked from commit 032d9a720c89286dc8c1931775144f0a65a6149e)
pull/4254/head
Mark McDowall 1 year ago committed by Bogdan
parent 552c70ec6f
commit 74d2b4e0dc

@ -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;
default:
dispatch({
...action,
items,
});
break;
}
},
[items, dispatch]
);
const value: [SelectState, Dispatch] = [state, 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…
Cancel
Save