Enable multiple row selection with Shift + checkbox click (#12492)
resolve #12291 This PR enables the selection of multiple rows by clicking checkboxes while holding the Shift key. A new Recoil state was created to store the lastRecordSelectedId. When the user clicks a checkbox while pressing Shift, the index of lastRecordSelectedId is retrieved, and a loop is executed between the current row index and the last selected index to select all rows in between. https://github.com/user-attachments/assets/97bdf2a0-f6a6-4f9f-8045-3804268ef924 --------- Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
@ -11,6 +11,7 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { lastSelectedRowIndexComponentState } from '../../record-table-row/states/lastSelectedRowIndexComponentState';
|
||||
|
||||
export const useResetTableRowSelection = (recordTableId?: string) => {
|
||||
const recordTableIdFromContext = useAvailableComponentInstanceIdOrThrow(
|
||||
@ -40,6 +41,12 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
|
||||
),
|
||||
);
|
||||
|
||||
const lastSelectedRowIndexComponentCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
lastSelectedRowIndexComponentState,
|
||||
recordTableIdFromContext,
|
||||
);
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
@ -55,11 +62,13 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
|
||||
set(hasUserSelectedAllRowsState, false);
|
||||
|
||||
set(isActionMenuDropdownOpenState, false);
|
||||
set(lastSelectedRowIndexComponentCallbackState, null);
|
||||
},
|
||||
[
|
||||
recordIndexAllRecordIdsSelector,
|
||||
hasUserSelectedAllRowsState,
|
||||
isActionMenuDropdownOpenState,
|
||||
lastSelectedRowIndexComponentCallbackState,
|
||||
isRowSelectedFamilyState,
|
||||
],
|
||||
);
|
||||
|
||||
@ -4,6 +4,7 @@ import { useCallback } from 'react';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Checkbox } from 'twenty-ui/input';
|
||||
|
||||
export const TABLE_CELL_CHECKBOX_MIN_WIDTH = '24px';
|
||||
@ -26,9 +27,15 @@ export const RecordTableCellCheckbox = () => {
|
||||
|
||||
const { setCurrentRowSelected } = useSetCurrentRowSelected();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setCurrentRowSelected(!isSelected);
|
||||
}, [isSelected, setCurrentRowSelected]);
|
||||
const handleClick = useCallback(
|
||||
(event?: React.MouseEvent<HTMLDivElement>) => {
|
||||
setCurrentRowSelected({
|
||||
newSelectedState: !isSelected,
|
||||
shouldSelectRange: isDefined(event?.shiftKey) && event.shiftKey,
|
||||
});
|
||||
},
|
||||
[isSelected, setCurrentRowSelected],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
||||
|
||||
@ -28,7 +28,20 @@ export const RecordTableRowHotkeyEffect = () => {
|
||||
useScopedHotkeys(
|
||||
'x',
|
||||
() => {
|
||||
setCurrentRowSelected(!isSelected);
|
||||
setCurrentRowSelected({
|
||||
newSelectedState: !isSelected,
|
||||
});
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
`${Key.Shift}+x`,
|
||||
() => {
|
||||
setCurrentRowSelected({
|
||||
newSelectedState: !isSelected,
|
||||
shouldSelectRange: true,
|
||||
});
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
);
|
||||
|
||||
@ -1,30 +1,79 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { lastSelectedRowIndexComponentState } from '@/object-record/record-table/record-table-row/states/lastSelectedRowIndexComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSetCurrentRowSelected = () => {
|
||||
const { recordId } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const { recordId, rowIndex } = useRecordTableRowContextOrThrow();
|
||||
const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2(
|
||||
isRowSelectedComponentFamilyState,
|
||||
);
|
||||
|
||||
const recordIndexAllRecordIdsState = useRecoilComponentCallbackStateV2(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
|
||||
const lastSelectedRowIndexComponentCallbackState =
|
||||
useRecoilComponentCallbackStateV2(lastSelectedRowIndexComponentState);
|
||||
|
||||
const setCurrentRowSelected = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(newSelectedState: boolean) => {
|
||||
const isRowSelected = getSnapshotValue(
|
||||
({
|
||||
newSelectedState,
|
||||
shouldSelectRange = false,
|
||||
}: {
|
||||
newSelectedState: boolean;
|
||||
shouldSelectRange?: boolean;
|
||||
}) => {
|
||||
const allRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexAllRecordIdsState,
|
||||
);
|
||||
|
||||
const isCurrentRowSelected = getSnapshotValue(
|
||||
snapshot,
|
||||
isRowSelectedFamilyState(recordId),
|
||||
);
|
||||
|
||||
if (isRowSelected !== newSelectedState) {
|
||||
const lastSelectedIndex = snapshot
|
||||
.getLoadable(lastSelectedRowIndexComponentCallbackState)
|
||||
.getValue();
|
||||
|
||||
if (shouldSelectRange && isDefined(lastSelectedIndex)) {
|
||||
const startIndex = Math.min(lastSelectedIndex, rowIndex);
|
||||
const endIndex = Math.max(lastSelectedIndex, rowIndex);
|
||||
|
||||
const shouldSelect = !isCurrentRowSelected;
|
||||
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
set(isRowSelectedFamilyState(allRecordIds[i]), shouldSelect);
|
||||
}
|
||||
|
||||
set(lastSelectedRowIndexComponentCallbackState, rowIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCurrentRowSelected !== newSelectedState) {
|
||||
set(isRowSelectedFamilyState(recordId), newSelectedState);
|
||||
|
||||
set(
|
||||
lastSelectedRowIndexComponentCallbackState,
|
||||
newSelectedState ? rowIndex : null,
|
||||
);
|
||||
}
|
||||
},
|
||||
[recordId, isRowSelectedFamilyState],
|
||||
[
|
||||
recordIndexAllRecordIdsState,
|
||||
isRowSelectedFamilyState,
|
||||
recordId,
|
||||
lastSelectedRowIndexComponentCallbackState,
|
||||
rowIndex,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const lastSelectedRowIndexComponentState = createComponentStateV2<
|
||||
number | null | undefined
|
||||
>({
|
||||
key: 'record-table/last-selected-row-index',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
Reference in New Issue
Block a user