diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts index e3c1095ee..187193e6a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts @@ -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, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx index c03164785..532c8fe9b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx @@ -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) => { + setCurrentRowSelected({ + newSelectedState: !isSelected, + shouldSelectRange: isDefined(event?.shiftKey) && event.shiftKey, + }); + }, + [isSelected, setCurrentRowSelected], + ); return ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx index 54056f15b..763d7d18c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx @@ -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, ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected.ts index c096d0b0d..1ca68e2e4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected.ts @@ -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 { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/states/lastSelectedRowIndexComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/states/lastSelectedRowIndexComponentState.ts new file mode 100644 index 000000000..c4b1f3866 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/states/lastSelectedRowIndexComponentState.ts @@ -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, +});