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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||||
|
import { lastSelectedRowIndexComponentState } from '../../record-table-row/states/lastSelectedRowIndexComponentState';
|
||||||
|
|
||||||
export const useResetTableRowSelection = (recordTableId?: string) => {
|
export const useResetTableRowSelection = (recordTableId?: string) => {
|
||||||
const recordTableIdFromContext = useAvailableComponentInstanceIdOrThrow(
|
const recordTableIdFromContext = useAvailableComponentInstanceIdOrThrow(
|
||||||
@ -40,6 +41,12 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const lastSelectedRowIndexComponentCallbackState =
|
||||||
|
useRecoilComponentCallbackStateV2(
|
||||||
|
lastSelectedRowIndexComponentState,
|
||||||
|
recordTableIdFromContext,
|
||||||
|
);
|
||||||
|
|
||||||
return useRecoilCallback(
|
return useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
() => {
|
() => {
|
||||||
@ -55,11 +62,13 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
|
|||||||
set(hasUserSelectedAllRowsState, false);
|
set(hasUserSelectedAllRowsState, false);
|
||||||
|
|
||||||
set(isActionMenuDropdownOpenState, false);
|
set(isActionMenuDropdownOpenState, false);
|
||||||
|
set(lastSelectedRowIndexComponentCallbackState, null);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
recordIndexAllRecordIdsSelector,
|
recordIndexAllRecordIdsSelector,
|
||||||
hasUserSelectedAllRowsState,
|
hasUserSelectedAllRowsState,
|
||||||
isActionMenuDropdownOpenState,
|
isActionMenuDropdownOpenState,
|
||||||
|
lastSelectedRowIndexComponentCallbackState,
|
||||||
isRowSelectedFamilyState,
|
isRowSelectedFamilyState,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useCallback } from 'react';
|
|||||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
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 { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Checkbox } from 'twenty-ui/input';
|
import { Checkbox } from 'twenty-ui/input';
|
||||||
|
|
||||||
export const TABLE_CELL_CHECKBOX_MIN_WIDTH = '24px';
|
export const TABLE_CELL_CHECKBOX_MIN_WIDTH = '24px';
|
||||||
@ -26,9 +27,15 @@ export const RecordTableCellCheckbox = () => {
|
|||||||
|
|
||||||
const { setCurrentRowSelected } = useSetCurrentRowSelected();
|
const { setCurrentRowSelected } = useSetCurrentRowSelected();
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(
|
||||||
setCurrentRowSelected(!isSelected);
|
(event?: React.MouseEvent<HTMLDivElement>) => {
|
||||||
}, [isSelected, setCurrentRowSelected]);
|
setCurrentRowSelected({
|
||||||
|
newSelectedState: !isSelected,
|
||||||
|
shouldSelectRange: isDefined(event?.shiftKey) && event.shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[isSelected, setCurrentRowSelected],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
<StyledRecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
||||||
|
|||||||
@ -28,7 +28,20 @@ export const RecordTableRowHotkeyEffect = () => {
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'x',
|
'x',
|
||||||
() => {
|
() => {
|
||||||
setCurrentRowSelected(!isSelected);
|
setCurrentRowSelected({
|
||||||
|
newSelectedState: !isSelected,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
TableHotkeyScope.TableFocus,
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
`${Key.Shift}+x`,
|
||||||
|
() => {
|
||||||
|
setCurrentRowSelected({
|
||||||
|
newSelectedState: !isSelected,
|
||||||
|
shouldSelectRange: true,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
TableHotkeyScope.TableFocus,
|
TableHotkeyScope.TableFocus,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,30 +1,79 @@
|
|||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
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 { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const useSetCurrentRowSelected = () => {
|
export const useSetCurrentRowSelected = () => {
|
||||||
const { recordId } = useRecordTableRowContextOrThrow();
|
const { recordId, rowIndex } = useRecordTableRowContextOrThrow();
|
||||||
|
|
||||||
const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2(
|
const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2(
|
||||||
isRowSelectedComponentFamilyState,
|
isRowSelectedComponentFamilyState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const recordIndexAllRecordIdsState = useRecoilComponentCallbackStateV2(
|
||||||
|
recordIndexAllRecordIdsComponentSelector,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastSelectedRowIndexComponentCallbackState =
|
||||||
|
useRecoilComponentCallbackStateV2(lastSelectedRowIndexComponentState);
|
||||||
|
|
||||||
const setCurrentRowSelected = useRecoilCallback(
|
const setCurrentRowSelected = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
(newSelectedState: boolean) => {
|
({
|
||||||
const isRowSelected = getSnapshotValue(
|
newSelectedState,
|
||||||
|
shouldSelectRange = false,
|
||||||
|
}: {
|
||||||
|
newSelectedState: boolean;
|
||||||
|
shouldSelectRange?: boolean;
|
||||||
|
}) => {
|
||||||
|
const allRecordIds = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
recordIndexAllRecordIdsState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCurrentRowSelected = getSnapshotValue(
|
||||||
snapshot,
|
snapshot,
|
||||||
isRowSelectedFamilyState(recordId),
|
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(isRowSelectedFamilyState(recordId), newSelectedState);
|
||||||
|
|
||||||
|
set(
|
||||||
|
lastSelectedRowIndexComponentCallbackState,
|
||||||
|
newSelectedState ? rowIndex : null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[recordId, isRowSelectedFamilyState],
|
[
|
||||||
|
recordIndexAllRecordIdsState,
|
||||||
|
isRowSelectedFamilyState,
|
||||||
|
recordId,
|
||||||
|
lastSelectedRowIndexComponentCallbackState,
|
||||||
|
rowIndex,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
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