mirror of
https://github.com/Radarr/Radarr.git
synced 2024-08-17 07:49:45 +02:00
parent
1336743aca
commit
3e643644cd
@ -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}>
|
||||||
|
113
frontend/src/Helpers/Hooks/useSelectState.tsx
Normal file
113
frontend/src/Helpers/Hooks/useSelectState.tsx
Normal file
@ -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<number, boolean>;
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import React, { SyntheticEvent, useCallback } from 'react';
|
import React, { SyntheticEvent, useCallback } from 'react';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@ -20,7 +20,7 @@ function MovieIndexPosterSelect(props: MovieIndexPosterSelectProps) {
|
|||||||
const shiftKey = nativeEvent.shiftKey;
|
const shiftKey = nativeEvent.shiftKey;
|
||||||
|
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: SelectActionType.ToggleSelected,
|
type: 'toggleSelected',
|
||||||
id: movieId,
|
id: movieId,
|
||||||
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 MovieIndexSelectAllButton(props: MovieIndexSelectAllButtonProps) {
|
|||||||
|
|
||||||
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';
|
||||||
|
|
||||||
@ -23,9 +23,7 @@ function MovieIndexSelectAllMenuItem(props: MovieIndexSelectAllMenuItemProps) {
|
|||||||
|
|
||||||
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 { RENAME_MOVIE } from 'Commands/commandNames';
|
import { RENAME_MOVIE } from 'Commands/commandNames';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
@ -128,7 +128,7 @@ function MovieIndexSelectFooter() {
|
|||||||
|
|
||||||
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 MovieIndexSelectModeButtonProps {
|
interface MovieIndexSelectModeButtonProps {
|
||||||
@ -18,7 +18,7 @@ function MovieIndexSelectModeButton(props: MovieIndexSelectModeButtonProps) {
|
|||||||
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 MovieIndexSelectModeMenuItemProps {
|
interface MovieIndexSelectModeMenuItemProps {
|
||||||
@ -19,7 +19,7 @@ function MovieIndexSelectModeMenuItem(
|
|||||||
const onPressWrapper = useCallback(() => {
|
const onPressWrapper = useCallback(() => {
|
||||||
if (isSelectMode) {
|
if (isSelectMode) {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: SelectActionType.Reset,
|
type: 'reset',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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 { SelectActionType, useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
@ -121,7 +121,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
|||||||
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 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';
|
||||||
@ -46,7 +46,7 @@ function MovieIndexTableHeader(props: MovieIndexTableHeaderProps) {
|
|||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }) => {
|
({ value }) => {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll,
|
type: value ? 'selectAll' : 'unselectAll',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[selectDispatch]
|
[selectDispatch]
|
||||||
|
Loading…
Reference in New Issue
Block a user