|
|
|
@ -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<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 }
|
|
|
|
|
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<T extends ModelBase> {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
@ -60,90 +30,40 @@ interface SelectProviderOptions<T extends ModelBase> {
|
|
|
|
|
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: {
|
|
|
|
|
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<T extends ModelBase>(
|
|
|
|
|
props: SelectProviderOptions<T>
|
|
|
|
|
) {
|
|
|
|
|
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 (
|
|
|
|
|
<SelectContext.Provider value={value}>
|
|
|
|
|