From 9fd3eb4d6bfa59e88a394c27e689b329f48e639b Mon Sep 17 00:00:00 2001 From: Qstick Date: Wed, 31 May 2023 19:41:55 -0500 Subject: [PATCH] Extract useSelectState from SelectContext Co-Authored-By: Mark McDowall --- frontend/src/App/SelectContext.tsx | 152 +++++------------- frontend/src/Helpers/Hooks/useSelectState.tsx | 113 +++++++++++++ .../Select/IndexerIndexSelectAllButton.tsx | 6 +- .../Select/IndexerIndexSelectAllMenuItem.tsx | 6 +- .../Index/Select/IndexerIndexSelectFooter.tsx | 4 +- .../Select/IndexerIndexSelectModeButton.tsx | 4 +- .../Select/IndexerIndexSelectModeMenuItem.tsx | 4 +- .../Indexer/Index/Table/IndexerIndexRow.tsx | 4 +- .../Index/Table/IndexerIndexTableHeader.tsx | 4 +- 9 files changed, 163 insertions(+), 134 deletions(-) create mode 100644 frontend/src/Helpers/Hooks/useSelectState.tsx diff --git a/frontend/src/App/SelectContext.tsx b/frontend/src/App/SelectContext.tsx index 6980129c1..66be388ce 100644 --- a/frontend/src/App/SelectContext.tsx +++ b/frontend/src/App/SelectContext.tsx @@ -1,58 +1,28 @@ 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 React, { useCallback, useEffect } from 'react'; +import useSelectState, { SelectState } from 'Helpers/Hooks/useSelectState'; import ModelBase from './ModelBase'; -export enum SelectActionType { - Reset, - SelectAll, - UnselectAll, - ToggleSelected, - RemoveItem, - UpdateItems, -} - -type SelectedState = Record; - -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 } +export type SelectContextAction = + | { type: 'reset' } + | { type: 'selectAll' } + | { type: 'unselectAll' } | { - type: SelectActionType.ToggleSelected; + type: 'toggleSelected'; id: number; isSelected: boolean; shiftKey: boolean; } | { - type: SelectActionType.RemoveItem; + type: 'removeItem'; id: number; } | { - type: SelectActionType.UpdateItems; + type: 'updateItems'; items: ModelBase[]; }; -type Dispatch = (action: SelectAction) => void; - -const initialState = { - selectedState: {}, - lastToggled: null, - allSelected: false, - allUnselected: true, - items: [], -}; +export type SelectDispatch = (action: SelectContextAction) => void; interface SelectProviderOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -60,90 +30,40 @@ interface SelectProviderOptions { items: Array; } -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: { - 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}`); - } - } -} +const SelectContext = React.createContext< + [SelectState, SelectDispatch] | undefined +>(cloneDeep(undefined)); export function SelectProvider( props: SelectProviderOptions ) { const { items } = props; - const selectedState = getSelectedState(items, {}); - - const [state, dispatch] = React.useReducer(selectReducer, { - selectedState, - lastToggled: null, - allSelected: false, - allUnselected: true, - items, - }); + const [state, dispatch] = useSelectState(); + + const dispatchWrapper = useCallback( + (action: SelectContextAction) => { + switch (action.type) { + case 'reset': + case 'removeItem': + dispatch(action); + break; + + default: + dispatch({ + ...action, + items, + }); + break; + } + }, + [items, dispatch] + ); - const value: [SelectState, Dispatch] = [state, dispatch]; + const value: [SelectState, SelectDispatch] = [state, dispatchWrapper]; useEffect(() => { - dispatch({ type: SelectActionType.UpdateItems, items }); - }, [items]); + dispatch({ type: 'updateItems', items }); + }, [items, dispatch]); return ( diff --git a/frontend/src/Helpers/Hooks/useSelectState.tsx b/frontend/src/Helpers/Hooks/useSelectState.tsx new file mode 100644 index 000000000..8fb96e42a --- /dev/null +++ b/frontend/src/Helpers/Hooks/useSelectState.tsx @@ -0,0 +1,113 @@ +import { cloneDeep } from 'lodash'; +import { useReducer } from 'react'; +import ModelBase from 'App/ModelBase'; +import areAllSelected from 'Utilities/Table/areAllSelected'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; + +export type SelectedState = Record; + +export interface SelectState { + selectedState: SelectedState; + lastToggled: number | null; + allSelected: boolean; + allUnselected: boolean; +} + +export type SelectAction = + | { type: 'reset' } + | { type: 'selectAll'; items: ModelBase[] } + | { type: 'unselectAll'; items: ModelBase[] } + | { + type: 'toggleSelected'; + id: number; + isSelected: boolean; + shiftKey: boolean; + items: ModelBase[]; + } + | { + type: 'removeItem'; + id: number; + } + | { + type: 'updateItems'; + items: ModelBase[]; + }; + +export type Dispatch = (action: SelectAction) => void; + +const initialState = { + selectedState: {}, + lastToggled: null, + allSelected: false, + allUnselected: true, + items: [], +}; + +function getSelectedState(items: ModelBase[], existingState: SelectedState) { + return items.reduce((acc: SelectedState, item) => { + const id = item.id; + + acc[id] = existingState[id] ?? false; + + return acc; + }, {}); +} + +function selectReducer(state: SelectState, action: SelectAction): SelectState { + const { selectedState } = state; + + switch (action.type) { + case 'reset': { + return cloneDeep(initialState); + } + case 'selectAll': { + return { + ...selectAll(selectedState, true), + }; + } + case 'unselectAll': { + return { + ...selectAll(selectedState, false), + }; + } + case 'toggleSelected': { + const result = { + ...toggleSelected( + state, + action.items, + action.id, + action.isSelected, + action.shiftKey + ), + }; + + return result; + } + case 'updateItems': { + const nextSelectedState = getSelectedState(action.items, selectedState); + + return { + ...state, + ...areAllSelected(nextSelectedState), + selectedState: nextSelectedState, + }; + } + default: { + throw new Error(`Unhandled action type: ${action.type}`); + } + } +} + +export default function useSelectState(): [SelectState, Dispatch] { + const selectedState = getSelectedState([], {}); + + const [state, dispatch] = useReducer(selectReducer, { + selectedState, + lastToggled: null, + allSelected: false, + allUnselected: true, + }); + + return [state, dispatch]; +} diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx index 746317dd1..bd7682018 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { SelectActionType, useSelect } from 'App/SelectContext'; +import { useSelect } from 'App/SelectContext'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import { icons } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; @@ -25,9 +25,7 @@ function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) { const onPress = useCallback(() => { selectDispatch({ - type: allSelected - ? SelectActionType.UnselectAll - : SelectActionType.SelectAll, + type: allSelected ? 'unselectAll' : 'selectAll', }); }, [allSelected, selectDispatch]); diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx index c5dc6981d..f9a52ed30 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { SelectActionType, useSelect } from 'App/SelectContext'; +import { useSelect } from 'App/SelectContext'; import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem'; import { icons } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; @@ -26,9 +26,7 @@ function IndexerIndexSelectAllMenuItem( const onPressWrapper = useCallback(() => { selectDispatch({ - type: allSelected - ? SelectActionType.UnselectAll - : SelectActionType.SelectAll, + type: allSelected ? 'unselectAll' : 'selectAll', }); }, [allSelected, selectDispatch]); diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx index 5d9317859..37828f8f4 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; -import { SelectActionType, useSelect } from 'App/SelectContext'; +import { useSelect } from 'App/SelectContext'; import SpinnerButton from 'Components/Link/SpinnerButton'; import PageContentFooter from 'Components/Page/PageContentFooter'; import { kinds } from 'Helpers/Props'; @@ -111,7 +111,7 @@ function IndexerIndexSelectFooter() { useEffect(() => { if (!isDeleting && !deleteError) { - selectDispatch({ type: SelectActionType.UnselectAll }); + selectDispatch({ type: 'unselectAll' }); } }, [isDeleting, deleteError, selectDispatch]); diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx index f95a5fa91..31fa1d041 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx @@ -1,6 +1,6 @@ import { IconDefinition } from '@fortawesome/fontawesome-common-types'; import React, { useCallback } from 'react'; -import { SelectActionType, useSelect } from 'App/SelectContext'; +import { useSelect } from 'App/SelectContext'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; interface IndexerIndexSelectModeButtonProps { @@ -20,7 +20,7 @@ function IndexerIndexSelectModeButton( const onPressWrapper = useCallback(() => { if (isSelectMode) { selectDispatch({ - type: SelectActionType.Reset, + type: 'reset', }); } diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx index f7d63950f..8f1c0623f 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx @@ -1,6 +1,6 @@ import { IconDefinition } from '@fortawesome/fontawesome-common-types'; import React, { useCallback } from 'react'; -import { SelectActionType, useSelect } from 'App/SelectContext'; +import { useSelect } from 'App/SelectContext'; import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem'; interface IndexerIndexSelectModeMenuItemProps { @@ -19,7 +19,7 @@ function IndexerIndexSelectModeMenuItem( const onPressWrapper = useCallback(() => { if (isSelectMode) { selectDispatch({ - type: SelectActionType.Reset, + type: 'reset', }); } diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx index 7bda8a287..fbb5a88ca 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; -import { SelectActionType, useSelect } from 'App/SelectContext'; +import { useSelect } from 'App/SelectContext'; import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; @@ -90,7 +90,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { const onSelectedChange = useCallback( ({ id, value, shiftKey }) => { selectDispatch({ - type: SelectActionType.ToggleSelected, + type: 'toggleSelected', id, isSelected: value, shiftKey, diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx index aa231533c..10fa849cd 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { SelectActionType, useSelect } from 'App/SelectContext'; +import { useSelect } from 'App/SelectContext'; import IconButton from 'Components/Link/IconButton'; import Column from 'Components/Table/Column'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; @@ -47,7 +47,7 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) { const onSelectAllChange = useCallback( ({ value }) => { selectDispatch({ - type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll, + type: value ? 'selectAll' : 'unselectAll', }); }, [selectDispatch]