diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx index e72f4118d..5b1120a75 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx @@ -21,11 +21,10 @@ import { useCallback } from 'react'; import { Link } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { useNavigateApp } from '~/hooks/useNavigateApp'; -import { Button } from 'twenty-ui/input'; import { IconBrowserMaximize } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; import { getOsControlSymbol } from 'twenty-ui/utilities'; - +import { useNavigateApp } from '~/hooks/useNavigateApp'; const StyledLink = styled(Link)` text-decoration: none; `; diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 775a4b4f7..d99712590 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -15,21 +15,26 @@ import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksO import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath'; +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; +import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; +import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { AppBasePath } from '@/types/AppBasePath'; import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { SettingsPath } from '@/types/SettingsPath'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isDefined } from 'twenty-shared/utils'; +import { AnalyticsType } from '~/generated/graphql'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState'; -import { AnalyticsType } from '~/generated/graphql'; import { getPageTitleFromPath } from '~/utils/title-utils'; - // TODO: break down into smaller functions and / or hooks // - moved usePageChangeEffectNavigateLocation into dedicated hook export const PageChangeEffect = () => { @@ -54,7 +59,19 @@ export const PageChangeEffect = () => { const objectNamePlural = useParams().objectNamePlural ?? CoreObjectNamePlural.Person; - const resetTableSelections = useResetTableRowSelection(objectNamePlural); + const contextStoreCurrentViewId = useRecoilComponentValueV2( + contextStoreCurrentViewIdComponentState, + MAIN_CONTEXT_STORE_INSTANCE_ID, + ); + + const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId( + objectNamePlural, + contextStoreCurrentViewId || '', + ); + + const resetTableSelections = useResetTableRowSelection(recordIndexId); + const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId); + const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId); const { executeTasksOnAnyLocationChange } = useExecuteTasksOnAnyLocationChange(); @@ -84,8 +101,16 @@ export const PageChangeEffect = () => { if (isLeavingRecordIndexPage) { resetTableSelections(); + unfocusRecordTableRow(); + deactivateRecordTableRow(); } - }, [isMatchingLocation, previousLocation, resetTableSelections]); + }, [ + isMatchingLocation, + previousLocation, + resetTableSelections, + unfocusRecordTableRow, + deactivateRecordTableRow, + ]); useEffect(() => { switch (true) { diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx index f67b3db68..fa47d4928 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx @@ -68,6 +68,7 @@ export const RecordChip = ({ const isSidePanelViewOpenRecordInType = recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL; + const onClick = isSidePanelViewOpenRecordInType ? () => openRecordInCommandMenu({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 57edee8de..510694369 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -6,12 +6,14 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record import { RecordTableBodyEffectsWrapper } from '@/object-record/record-table/components/RecordTableBodyEffectsWrapper'; import { RecordTableContent } from '@/object-record/record-table/components/RecordTableContent'; import { RecordTableEmpty } from '@/object-record/record-table/components/RecordTableEmpty'; -import { RecordTableScrollToFocusedElementEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedElementEffect'; +import { RecordTableScrollToFocusedCellEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedCellEffect'; +import { RecordTableScrollToFocusedRowEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedRowEffect'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState'; +import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; +import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -46,8 +48,13 @@ export const RecordTable = () => { const recordTableIsEmpty = !isRecordTableInitialLoading && allRecordIds.length === 0; - const isRecordTableFocusActive = useRecoilComponentValueV2( - isRecordTableFocusActiveComponentState, + const isRecordTableCellFocusActive = useRecoilComponentValueV2( + isRecordTableCellFocusActiveComponentState, + recordTableId, + ); + + const isRecordTableRowFocusActive = useRecoilComponentValueV2( + isRecordTableRowFocusActiveComponentState, recordTableId, ); @@ -71,7 +78,9 @@ export const RecordTable = () => { tableBodyRef={tableBodyRef} /> - {isRecordTableFocusActive && } + {isRecordTableCellFocusActive && } + + {isRecordTableRowFocusActive && } {recordTableIsEmpty && !hasRecordGroups ? ( { - const isAtLeastOneRecordSelected = useRecoilComponentValueV2( - isAtLeastOneTableRowSelectedSelector, - ); - - const isRecordTableFocusActive = useRecoilComponentValueV2( - isRecordTableFocusActiveComponentState, + const isRecordTableRowFocusActive = useRecoilComponentValueV2( + isRecordTableRowFocusActiveComponentState, ); return ( @@ -31,9 +28,11 @@ export const RecordTableBodyEffectsWrapper = ({ ) : ( )} - {isAtLeastOneRecordSelected && } - {isRecordTableFocusActive && } + + + {isRecordTableRowFocusActive && } + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect.tsx new file mode 100644 index 000000000..4eabda7f7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect.tsx @@ -0,0 +1,12 @@ +import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; +import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose'; + +export const RecordTableDeactivateRecordTableRowEffect = () => { + const { deactivateRecordTableRow } = useActiveRecordTableRow(); + + useListenRightDrawerClose(() => { + deactivateRecordTableRow(); + }); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx index 4278883a8..339215d6c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx @@ -1,7 +1,7 @@ import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; -import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; +import { useRecordTableMoveFocusedCell } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedCell'; import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup'; import { useMoveHoverToCurrentCell } from '@/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell'; import { @@ -28,7 +28,7 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({ openTableCell(args); }; - const { moveFocus } = useRecordTableMoveFocus(recordTableId); + const { moveFocus } = useRecordTableMoveFocusedCell(recordTableId); const handleMoveFocus = (direction: MoveFocusDirection) => { moveFocus(direction); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx index 8151628f5..68a89348d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx @@ -1,7 +1,7 @@ import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; -import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; +import { useRecordTableMove } from '@/object-record/record-table/hooks/useRecordTableMove'; import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup'; import { useMoveHoverToCurrentCell } from '@/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell'; import { @@ -29,10 +29,10 @@ export const RecordTableRecordGroupBodyContextProvider = ({ openTableCell(args); }; - const { moveFocus } = useRecordTableMoveFocus(recordTableId); + const { move } = useRecordTableMove(recordTableId); const handleMoveFocus = (direction: MoveFocusDirection) => { - moveFocus(direction); + move(direction); }; const { closeTableCellInGroup } = useCloseRecordTableCellInGroup(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedElementEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedCellEffect.tsx similarity index 77% rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedElementEffect.tsx rename to packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedCellEffect.tsx index b018f500f..0834c04b4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedElementEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedCellEffect.tsx @@ -1,13 +1,18 @@ +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useEffect } from 'react'; import { isDefined } from 'twenty-shared/utils'; -export const RecordTableScrollToFocusedElementEffect = () => { +export const RecordTableScrollToFocusedCellEffect = () => { + const { recordTableId } = useRecordTableContextOrThrow(); + const focusPosition = useRecoilComponentValueV2( recordTableFocusPositionComponentState, + recordTableId, ); + // Handle cell focus useEffect(() => { if (!focusPosition) { return; @@ -36,11 +41,15 @@ export const RecordTableScrollToFocusedElementEffect = () => { } } + focusElement.style.scrollMarginTop = '32px'; + focusElement.style.scrollMarginBottom = '32px'; + focusElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); return () => { if (isDefined(focusElement)) { focusElement.style.scrollMarginLeft = ''; + focusElement.style.scrollMarginBottom = ''; } }; }, [focusPosition]); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedRowEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedRowEffect.tsx new file mode 100644 index 000000000..23643452d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedRowEffect.tsx @@ -0,0 +1,63 @@ +import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState'; +import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useEffect } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +export const RecordTableScrollToFocusedRowEffect = () => { + const { recordTableId } = useRecordTableContextOrThrow(); + + const focusedRowIndex = useRecoilComponentValueV2( + focusedRecordTableRowIndexComponentState, + recordTableId, + ); + + const isRowFocusActive = useRecoilComponentValueV2( + isRecordTableRowFocusActiveComponentState, + recordTableId, + ); + + const allRecordIds = useRecoilComponentValueV2( + recordIndexAllRecordIdsComponentSelector, + recordTableId, + ); + + useEffect(() => { + if ( + !isRowFocusActive || + !isDefined(focusedRowIndex) || + !allRecordIds?.length + ) { + return; + } + + const recordId = allRecordIds[focusedRowIndex]; + + if (!recordId) { + return; + } + + const focusElement = document.getElementById( + `record-table-cell-0-${focusedRowIndex}`, + ); + + if (!focusElement) { + return; + } + + focusElement.style.scrollMarginBottom = '32px'; + focusElement.style.scrollMarginTop = '32px'; + + focusElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + + return () => { + if (isDefined(focusElement)) { + focusElement.style.scrollMarginBottom = ''; + } + }; + }, [focusedRowIndex, isRowFocusActive, allRecordIds]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index 8f3ad47a8..20283baf3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -1,7 +1,11 @@ import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; +import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; +import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -24,11 +28,27 @@ export const useLeaveTableFocus = (recordTableId?: string) => { recordTableIdFromContext, ); + const { unfocusRecordTableRow } = useFocusedRecordTableRow( + recordTableIdFromContext, + ); + + const { deactivateRecordTableRow } = useActiveRecordTableRow( + recordTableIdFromContext, + ); + + const setHotkeyScope = useSetHotkeyScope(); + return () => { resetTableRowSelection(); setIsFocusActiveForCurrentPosition(false); + unfocusRecordTableRow(); + + deactivateRecordTableRow(); + setRecordTableHoverPosition(null); + + setHotkeyScope(TableHotkeyScope.Table); }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useActiveRecordTableRow.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useActiveRecordTableRow.ts new file mode 100644 index 000000000..9a586a69c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useActiveRecordTableRow.ts @@ -0,0 +1,62 @@ +import { activeRecordTableRowIndexComponentState } from '@/object-record/record-table/states/activeRecordTableRowIndexComponentState'; +import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useActiveRecordTableRow = (recordTableId?: string) => { + const isRowActiveState = useRecoilComponentCallbackStateV2( + isRecordTableRowActiveComponentFamilyState, + recordTableId, + ); + + const activeRowIndexState = useRecoilComponentCallbackStateV2( + activeRecordTableRowIndexComponentState, + recordTableId, + ); + + const deactivateRecordTableRow = useRecoilCallback( + ({ set, snapshot }) => + () => { + const activeRowIndex = snapshot + .getLoadable(activeRowIndexState) + .getValue(); + + if (!isDefined(activeRowIndex)) { + return; + } + + set(activeRowIndexState, null); + + set(isRowActiveState(activeRowIndex), false); + }, + [activeRowIndexState, isRowActiveState], + ); + + const activateRecordTableRow = useRecoilCallback( + ({ set, snapshot }) => + (rowIndex: number) => { + const activeRowIndex = snapshot + .getLoadable(activeRowIndexState) + .getValue(); + + if (activeRowIndex === rowIndex) { + return; + } + + if (isDefined(activeRowIndex)) { + set(isRowActiveState(activeRowIndex), false); + } + + set(activeRowIndexState, rowIndex); + + set(isRowActiveState(rowIndex), true); + }, + [activeRowIndexState, isRowActiveState], + ); + + return { + activateRecordTableRow, + deactivateRecordTableRow, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useFocusedRecordTableRow.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useFocusedRecordTableRow.ts new file mode 100644 index 000000000..c05921d24 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useFocusedRecordTableRow.ts @@ -0,0 +1,110 @@ +import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState'; +import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState'; +import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; +import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; +import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useFocusedRecordTableRow = (recordTableId?: string) => { + const isRowFocusedState = useRecoilComponentCallbackStateV2( + isRecordTableRowFocusedComponentFamilyState, + recordTableId, + ); + + const focusedRowIndexState = useRecoilComponentCallbackStateV2( + focusedRecordTableRowIndexComponentState, + recordTableId, + ); + + const isRowFocusActiveState = useRecoilComponentCallbackStateV2( + isRecordTableRowFocusActiveComponentState, + recordTableId, + ); + + const focusedCellPositionState = useRecoilComponentCallbackStateV2( + recordTableFocusPositionComponentState, + recordTableId, + ); + + const isRecordTableCellFocusActiveState = useRecoilComponentCallbackStateV2( + isRecordTableCellFocusActiveComponentState, + recordTableId, + ); + + const unfocusRecordTableRow = useRecoilCallback( + ({ set, snapshot }) => + () => { + const focusedRowIndex = snapshot + .getLoadable(focusedRowIndexState) + .getValue(); + + if (!isDefined(focusedRowIndex)) { + return; + } + + set(focusedRowIndexState, null); + set(isRowFocusedState(focusedRowIndex), false); + set(isRowFocusActiveState, false); + }, + [focusedRowIndexState, isRowFocusedState, isRowFocusActiveState], + ); + + const focusRecordTableRow = useRecoilCallback( + ({ set, snapshot }) => + (rowIndex: number) => { + const focusedRowIndex = snapshot + .getLoadable(focusedRowIndexState) + .getValue(); + + if (isDefined(focusedRowIndex) && focusedRowIndex !== rowIndex) { + set(isRowFocusedState(focusedRowIndex), false); + } + + set(focusedRowIndexState, rowIndex); + set(isRowFocusedState(rowIndex), true); + set(isRowFocusActiveState, true); + }, + [focusedRowIndexState, isRowFocusedState, isRowFocusActiveState], + ); + + const restoreRecordTableRowFocusFromCellPosition = useRecoilCallback( + ({ snapshot }) => + () => { + const focusedRowIndex = snapshot + .getLoadable(focusedRowIndexState) + .getValue(); + + const focusedCellPosition = snapshot + .getLoadable(focusedCellPositionState) + .getValue(); + + const isRecordTableCellFocusActive = snapshot + .getLoadable(isRecordTableCellFocusActiveState) + .getValue(); + + if ( + !isDefined(focusedRowIndex) || + !isDefined(focusedCellPosition) || + !isRecordTableCellFocusActive + ) { + return; + } + + focusRecordTableRow(focusedCellPosition.row); + }, + [ + focusedRowIndexState, + focusedCellPositionState, + isRecordTableCellFocusActiveState, + focusRecordTableRow, + ], + ); + + return { + focusRecordTableRow, + unfocusRecordTableRow, + restoreRecordTableRowFocusFromCellPosition, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 5d307d279..3d7fdb9ec 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -3,9 +3,7 @@ import { Key } from 'ts-key-enum'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { useSetHasUserSelectedAllRows } from '@/object-record/record-table/hooks/internal/useSetAllRowSelectedState'; -import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; @@ -13,15 +11,16 @@ import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState'; import { ColumnDefinition } from '../types/ColumnDefinition'; import { TableHotkeyScope } from '../types/TableHotkeyScope'; -import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive'; import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState'; import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState'; +import { useRecordTableMove } from '@/object-record/record-table/hooks/useRecordTableMove'; import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState'; import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -31,7 +30,6 @@ import { useSelectAllRows } from './internal/useSelectAllRows'; import { useSetRecordTableData } from './internal/useSetRecordTableData'; import { useSetRecordTableFocusPosition } from './internal/useSetRecordTableFocusPosition'; import { useSetRowSelectedState } from './internal/useSetRowSelectedState'; - type useRecordTableProps = { recordTableId?: string; }; @@ -141,62 +139,65 @@ export const useRecordTable = (props?: useRecordTableProps) => { const setFocusPosition = useSetRecordTableFocusPosition(recordTableId); - const { setIsFocusActiveForCurrentPosition } = - useSetIsRecordTableFocusActive(recordTableId); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); - const { moveDown, moveLeft, moveRight, moveUp } = - useRecordTableMoveFocus(recordTableId); + const { move } = useRecordTableMove(recordTableId); const useMapKeyboardToFocus = () => { - const setHotkeyScope = useSetHotkeyScope(); - useScopedHotkeys( [Key.ArrowUp, `${Key.Shift}+${Key.Enter}`], () => { - moveUp(); + move('up'); }, TableHotkeyScope.TableFocus, - [moveUp], + [move], ); useScopedHotkeys( Key.ArrowDown, () => { - moveDown(); + move('down'); }, TableHotkeyScope.TableFocus, - [moveDown], + [move], + ); + + useScopedHotkeys( + [Key.ArrowUp, 'k'], + () => { + setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); + move('up'); + }, + TableHotkeyScope.Table, + [move], + ); + + useScopedHotkeys( + [Key.ArrowDown, 'j'], + () => { + setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); + move('down'); + }, + TableHotkeyScope.Table, + [move], ); useScopedHotkeys( [Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`], () => { - moveLeft(); + move('left'); }, TableHotkeyScope.TableFocus, - [moveLeft], + [move], ); useScopedHotkeys( [Key.ArrowRight, Key.Tab], () => { - moveRight(); + move('right'); }, TableHotkeyScope.TableFocus, - [moveRight], - ); - - useScopedHotkeys( - [Key.Escape], - () => { - setHotkeyScope(TableHotkeyScope.Table, { - goto: true, - keyboardShortcutMenu: true, - }); - setIsFocusActiveForCurrentPosition(false); - }, - TableHotkeyScope.TableFocus, - [setIsFocusActiveForCurrentPosition], + [move], ); }; @@ -211,10 +212,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { setRowSelected, resetTableRowSelection, upsertRecordTableItem, - moveDown, - moveLeft, - moveRight, - moveUp, + move, useMapKeyboardToFocus, selectAllRows, setOnColumnsChange, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMove.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMove.ts new file mode 100644 index 000000000..3c66d6033 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMove.ts @@ -0,0 +1,39 @@ +import { useRecordTableMoveFocusedCell } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedCell'; +import { useRecordTableMoveFocusedRow } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedRow'; +import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState'; +import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; + +export const useRecordTableMove = (recordTableId?: string) => { + const { moveFocusedRow } = useRecordTableMoveFocusedRow(recordTableId); + + const { moveFocus } = useRecordTableMoveFocusedCell(recordTableId); + + const isRecordTableFocusActiveState = useRecoilComponentCallbackStateV2( + isRecordTableCellFocusActiveComponentState, + recordTableId, + ); + + const move = useRecoilCallback( + ({ snapshot }) => + (direction: MoveFocusDirection) => { + const isRecordTableFocusActive = getSnapshotValue( + snapshot, + isRecordTableFocusActiveState, + ); + + if (isRecordTableFocusActive) { + moveFocus(direction); + } else { + moveFocusedRow(direction); + } + }, + [isRecordTableFocusActiveState, moveFocusedRow, moveFocus], + ); + + return { + move, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts similarity index 98% rename from packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts rename to packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts index 09cada125..43f8062f2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts @@ -9,7 +9,7 @@ import { recordTableFocusPositionComponentState } from '@/object-record/record-t import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; -export const useRecordTableMoveFocus = (recordTableId?: string) => { +export const useRecordTableMoveFocusedCell = (recordTableId?: string) => { const setFocusPosition = useSetRecordTableFocusPosition(recordTableId); const focusPositionState = useRecoilComponentCallbackStateV2( diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedRow.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedRow.ts new file mode 100644 index 000000000..cab82d746 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedRow.ts @@ -0,0 +1,93 @@ +import { useRecoilCallback } from 'recoil'; + +import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; +import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState'; +import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; +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 useRecordTableMoveFocusedRow = (recordTableId?: string) => { + const { focusRecordTableRow } = useFocusedRecordTableRow(recordTableId); + + const focusedRowIndexState = useRecoilComponentCallbackStateV2( + focusedRecordTableRowIndexComponentState, + recordTableId, + ); + + const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackStateV2( + recordIndexAllRecordIdsComponentSelector, + recordTableId, + ); + + const moveFocusedRowUp = useRecoilCallback( + ({ snapshot }) => + () => { + const focusedRowIndex = getSnapshotValue( + snapshot, + focusedRowIndexState, + ); + + if (!isDefined(focusedRowIndex)) { + focusRecordTableRow(0); + return; + } + + let newRowIndex = focusedRowIndex - 1; + + if (newRowIndex < 0) { + newRowIndex = 0; + } + + focusRecordTableRow(newRowIndex); + }, + [focusedRowIndexState, focusRecordTableRow], + ); + + const moveFocusedRowDown = useRecoilCallback( + ({ snapshot }) => + () => { + const allRecordIds = getSnapshotValue( + snapshot, + recordIndexAllRecordIdsSelector, + ); + const focusedRowIndex = getSnapshotValue( + snapshot, + focusedRowIndexState, + ); + + if (!isDefined(focusedRowIndex)) { + focusRecordTableRow(0); + return; + } + + let newRowIndex = focusedRowIndex + 1; + + if (newRowIndex >= allRecordIds.length) { + newRowIndex = allRecordIds.length - 1; + } + + focusRecordTableRow(newRowIndex); + }, + [ + recordIndexAllRecordIdsSelector, + focusedRowIndexState, + focusRecordTableRow, + ], + ); + + const moveFocusedRow = (direction: MoveFocusDirection) => { + if (direction === 'up') { + moveFocusedRowUp(); + } else if (direction === 'down') { + moveFocusedRowDown(); + } + }; + + return { + moveFocusedRowUp, + moveFocusedRowDown, + moveFocusedRow, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx index e1a1f6f03..1363ff88c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx @@ -6,20 +6,20 @@ const StyledTbody = styled.tbody` position: sticky; left: 0; z-index: 6; - transition: 0.3s ease; + transition: transform 0.3s ease; } td:nth-of-type(2) { position: sticky; left: 11px; z-index: 6; - transition: 0.3s ease; + transition: transform 0.3s ease; } tr:not(:last-child) td:nth-of-type(3) { // Last row is aggregate footer position: sticky; left: 43px; z-index: 6; - transition: 0.3s ease; + transition: transform 0.3s ease; &:not(.disable-shadow)::after { content: ''; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx index 54272d089..0fa9d56e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx @@ -1,10 +1,12 @@ import { Key } from 'ts-key-enum'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; - +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableBodyEscapeHotkeyEffect = () => { const { recordTableId } = useRecordTableContextOrThrow(); @@ -12,12 +14,22 @@ export const RecordTableBodyEscapeHotkeyEffect = () => { recordTableId, }); + const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordTableId); + + const isAtLeastOneRecordSelected = useRecoilComponentValueV2( + isAtLeastOneTableRowSelectedSelector, + ); + useScopedHotkeys( [Key.Escape], () => { - resetTableRowSelection(); + unfocusRecordTableRow(); + if (isAtLeastOneRecordSelected) { + resetTableRowSelection(); + } }, TableHotkeyScope.Table, + [isAtLeastOneRecordSelected, resetTableRowSelection, unfocusRecordTableRow], ); return <>; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx index 98e7b90ab..08424ac9c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx @@ -1,5 +1,13 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive'; +import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { Key } from 'ts-key-enum'; export const RecordTableBodyFocusKeyboardEffect = () => { const { recordTableId } = useRecordTableContextOrThrow(); @@ -8,7 +16,41 @@ export const RecordTableBodyFocusKeyboardEffect = () => { recordTableId, }); + const setHotkeyScope = useSetHotkeyScope(); + + const { restoreRecordTableRowFocusFromCellPosition } = + useFocusedRecordTableRow(recordTableId); + + const { setIsFocusActiveForCurrentPosition } = + useSetIsRecordTableFocusActive(recordTableId); + + const isRecordTableFocusActive = useRecoilComponentValueV2( + isRecordTableCellFocusActiveComponentState, + ); + useMapKeyboardToFocus(); + useScopedHotkeys( + [Key.Escape], + () => { + if (isRecordTableFocusActive) { + restoreRecordTableRowFocusFromCellPosition(); + setIsFocusActiveForCurrentPosition(false); + } else { + setHotkeyScope(TableHotkeyScope.Table, { + goto: true, + keyboardShortcutMenu: true, + }); + } + }, + TableHotkeyScope.TableFocus, + [ + setIsFocusActiveForCurrentPosition, + restoreRecordTableRowFocusFromCellPosition, + setHotkeyScope, + isRecordTableFocusActive, + ], + ); + return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect.tsx new file mode 100644 index 000000000..61afee3cc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect.tsx @@ -0,0 +1,31 @@ +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableMoveFocusedRow } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedRow'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { Key } from 'ts-key-enum'; + +export const RecordTableBodyRowFocusKeyboardEffect = () => { + const { recordTableId } = useRecordTableContextOrThrow(); + + const { moveFocusedRow } = useRecordTableMoveFocusedRow(recordTableId); + + useScopedHotkeys( + [Key.ArrowUp, 'k'], + () => { + moveFocusedRow('up'); + }, + TableHotkeyScope.TableFocus, + [moveFocusedRow], + ); + + useScopedHotkeys( + [Key.ArrowDown, 'j'], + () => { + moveFocusedRow('down'); + }, + TableHotkeyScope.TableFocus, + [moveFocusedRow], + ); + + return <>; +}; 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 c4f7a4eca..20c488cbf 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 @@ -17,6 +17,10 @@ const StyledContainer = styled.div` min-width: ${TABLE_CELL_CHECKBOX_MIN_WIDTH}; `; +const StyledRecordTableTd = styled(RecordTableTd)` + border-left: 1px solid transparent; +`; + export const RecordTableCellCheckbox = () => { const { isSelected } = useRecordTableRowContextOrThrow(); @@ -27,10 +31,10 @@ export const RecordTableCellCheckbox = () => { }, [isSelected, setCurrentRowSelected]); return ( - + - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx index 50b5273fb..f649f0a5c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx @@ -6,20 +6,25 @@ import styled from '@emotion/styled'; import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; +import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableCellDisplayMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode'; import { RecordTableCellEditButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditButton'; import { RecordTableCellEditMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditMode'; import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput'; +import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useContext } from 'react'; import { BORDER_COMMON } from 'twenty-ui/theme'; import { useIsMobile } from 'twenty-ui/utilities'; const StyledRecordTableCellHoveredPortalContent = styled.div<{ isReadOnly: boolean; + isRowActive: boolean; }>` align-items: center; background: ${({ theme }) => theme.background.transparent.secondary}; - background-color: ${({ theme }) => theme.background.primary}; + background-color: ${({ theme, isRowActive }) => + isRowActive ? theme.accent.quaternary : theme.background.primary}; border-radius: ${({ isReadOnly }) => !isReadOnly ? BORDER_COMMON.radius.sm : 'none'}; box-sizing: border-box; @@ -28,10 +33,12 @@ const StyledRecordTableCellHoveredPortalContent = styled.div<{ height: 32px; - outline: ${({ theme, isReadOnly }) => - isReadOnly - ? `1px solid ${theme.border.color.medium}` - : `1px solid ${theme.font.color.extraLight}`}; + outline: ${({ theme, isReadOnly, isRowActive }) => + isRowActive + ? 'none' + : isReadOnly + ? `1px solid ${theme.border.color.medium}` + : `1px solid ${theme.font.color.extraLight}`}; position: relative; user-select: none; @@ -53,8 +60,18 @@ const RecordTableCellHoveredPortalContent = () => { const showButton = !isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn); + const { rowIndex } = useRecordTableRowContextOrThrow(); + + const isRowActive = useRecoilComponentFamilyValueV2( + isRecordTableRowActiveComponentFamilyState, + rowIndex, + ); + return ( - + {isFieldInputOnly ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx index f48c05abb..48097b0e7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx @@ -1,14 +1,14 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableCellEditModePortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal'; import { RecordTableCellHoveredPortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal'; -import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState'; +import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableCellPortals = () => { const { recordTableId } = useRecordTableContextOrThrow(); const isRecordTableFocusActive = useRecoilComponentValueV2( - isRecordTableFocusActiveComponentState, + isRecordTableCellFocusActiveComponentState, recordTableId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx index 2243a41e6..3dc2cac90 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx @@ -4,10 +4,5 @@ import { RecordTableTd } from '@/object-record/record-table/record-table-cell/co export const RecordTableLastEmptyCell = () => { const { isSelected } = useRecordTableRowContextOrThrow(); - return ( - <> - - - - ); + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx index 881ef648c..355b63efa 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx @@ -26,7 +26,7 @@ const StyledTd = styled.td<{ hasRightBorder && !isDragging ? `1px solid ${borderColor}` : 'none'}; padding: 0; - transition: 0.3s ease; + transition: transform 0.3s ease; text-align: left; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx index 4f7769ddb..329049d7d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx @@ -4,7 +4,7 @@ import { RecoilRoot, useRecoilValue } from 'recoil'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive'; -import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState'; +import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState'; import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState'; import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; @@ -19,7 +19,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( { set( - isRecordTableFocusActiveComponentState.atomFamily({ + isRecordTableCellFocusActiveComponentState.atomFamily({ instanceId: 'test-table-id', }), false, @@ -50,7 +50,7 @@ const renderHooks = () => { const { setIsFocusActive, setIsFocusActiveForCurrentPosition } = useSetIsRecordTableFocusActive('test-table-id'); const isRecordTableFocusActive = useRecoilValue( - isRecordTableFocusActiveComponentState.atomFamily({ + isRecordTableCellFocusActiveComponentState.atomFamily({ instanceId: 'test-table-id', }), ); @@ -99,7 +99,7 @@ describe('useSetIsRecordTableFocusActive', () => { expect(result.current.focusPosition).toEqual(cellPosition); }); - it('should remove focus-active class when focus is deactivated and update isRecordTableFocusActiveComponentState', () => { + it('should remove focus-active class when focus is deactivated and update isRecordTableCellFocusActiveComponentState', () => { const { result } = renderHooks(); const cellPosition: TableCellPosition = { column: 1, row: 0 }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts index f90bf23de..6015c3024 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts @@ -9,7 +9,6 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; -import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition'; import { TableHotkeyScope } from '../../types/TableHotkeyScope'; export const DEFAULT_CELL_SCOPE: HotkeyScope = { @@ -36,8 +35,6 @@ export const useOpenRecordTableCellFromCell = () => { const { cellPosition } = useContext(RecordTableCellContext); - const setFocusPosition = useSetRecordTableFocusPosition(); - const openTableCell = ( initialValue?: string, isActionButtonClick = false, @@ -54,8 +51,6 @@ export const useOpenRecordTableCellFromCell = () => { isActionButtonClick, isNavigating, }); - - setFocusPosition(cellPosition); }; return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts index db5f3bee7..3bddfb32a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts @@ -25,6 +25,10 @@ import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropd import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; +import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition'; +import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; +import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; +import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -82,6 +86,18 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { const { openFieldInput } = useOpenFieldInputEditMode(); + const { activateRecordTableRow, deactivateRecordTableRow } = + useActiveRecordTableRow(tableScopeId); + + const { unfocusRecordTableRow } = useFocusedRecordTableRow(tableScopeId); + + const setIsRowFocusActive = useSetRecoilComponentStateV2( + isRecordTableRowFocusActiveComponentState, + tableScopeId, + ); + + const setFocusPosition = useSetRecordTableFocusPosition(); + const openTableCell = useRecoilCallback( ({ snapshot, set }) => ({ @@ -134,6 +150,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { recordId, objectNameSingular, }); + + activateRecordTableRow(cellPosition.row); + unfocusRecordTableRow(); } return; @@ -147,6 +166,12 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { return; } + deactivateRecordTableRow(); + + setFocusPosition(cellPosition); + + setIsRowFocusActive(false); + setDragSelectionStartEnabled(false); openFieldInput({ @@ -179,6 +204,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { }, [ clickOutsideListenerIsActivatedState, + deactivateRecordTableRow, + setFocusPosition, + setIsRowFocusActive, setDragSelectionStartEnabled, openFieldInput, setCurrentTableCellInEditModePosition, @@ -189,6 +217,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { navigate, indexIdentifierUrl, openRecordInCommandMenu, + activateRecordTableRow, + unfocusRecordTableRow, setViewableRecordId, setViewableRecordNameSingular, ], diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts index 6461e3f1d..8cf482847 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts @@ -1,4 +1,4 @@ -import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState'; +import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState'; import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState'; import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -6,7 +6,7 @@ import { useRecoilCallback } from 'recoil'; export const useSetIsRecordTableFocusActive = (recordTableId?: string) => { const isRecordTableFocusActiveState = useRecoilComponentCallbackStateV2( - isRecordTableFocusActiveComponentState, + isRecordTableCellFocusActiveComponentState, recordTableId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 994b24269..f195ec216 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -8,6 +8,8 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; +import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; +import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; @@ -17,23 +19,27 @@ import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPoin import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; import { IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; +import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; const COLUMN_MIN_WIDTH = 104; const StyledColumnHeaderCell = styled.th<{ columnWidth: number; isResizing?: boolean; + isFirstRowActiveOrFocused: boolean; }>` - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + border-bottom: ${({ isFirstRowActiveOrFocused, theme }) => + isFirstRowActiveOrFocused + ? 'none' + : `1px solid ${theme.border.color.light}`}; color: ${({ theme }) => theme.font.color.tertiary}; padding: 0; text-align: left; - transition: 0.3s ease; background-color: ${({ theme }) => theme.background.primary}; border-right: 1px solid ${({ theme }) => theme.border.color.light}; @@ -215,6 +221,18 @@ export const RecordTableHeaderCell = ({ const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); + const isFirstRowActive = useRecoilComponentFamilyValueV2( + isRecordTableRowActiveComponentFamilyState, + 0, + ); + + const isFirstRowFocused = useRecoilComponentFamilyValueV2( + isRecordTableRowFocusedComponentFamilyState, + 0, + ); + + const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; + return ( setIconVisibility(true)} onMouseLeave={() => setIconVisibility(false)} + isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx index 5cdd9ce4e..b7d2a95e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx @@ -4,7 +4,10 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; +import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; +import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { Checkbox } from 'twenty-ui/input'; @@ -16,9 +19,14 @@ const StyledContainer = styled.div` background-color: ${({ theme }) => theme.background.primary}; `; -const StyledColumnHeaderCell = styled.th` +const StyledColumnHeaderCell = styled.th<{ + isFirstRowActiveOrFocused: boolean; +}>` background-color: ${({ theme }) => theme.background.primary}; - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + border-bottom: ${({ isFirstRowActiveOrFocused, theme }) => + isFirstRowActiveOrFocused + ? 'none' + : `1px solid ${theme.border.color.light}`}; border-right: transparent; width: 30px; `; @@ -58,8 +66,22 @@ export const RecordTableHeaderCheckboxColumn = () => { } }; + const isFirstRowActive = useRecoilComponentFamilyValueV2( + isRecordTableRowActiveComponentFamilyState, + 0, + ); + + const isFirstRowFocused = useRecoilComponentFamilyValueV2( + isRecordTableRowFocusedComponentFamilyState, + 0, + ); + + const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; + return ( - + ` - ${({ theme }) => { - return ` - &:hover { - background: ${theme.background.transparent.light}; - }; - `; - }}; - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + border-bottom: ${({ isFirstRowActiveOrFocused, theme }) => + isFirstRowActiveOrFocused + ? 'none' + : `1px solid ${theme.border.color.light}`}; background-color: ${({ theme }) => theme.background.primary}; border-left: none !important; color: ${({ theme }) => theme.font.color.tertiary}; border-right: none !important; - width: 32px; + cursor: default; ${({ isTableWiderThanScreen, theme }) => isTableWiderThanScreen ? ` background-color: ${theme.background.primary}; + width: 32px; ` - : ''}; + : 'width: 100%'}; z-index: 1; `; -const StyledEmptyHeaderCell = styled.th` - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - background-color: ${({ theme }) => theme.background.primary}; - width: 100%; -`; - const StyledPlusIconContainer = styled.div` align-items: center; display: flex; height: 32px; justify-content: center; - width: 100%; +`; + +const StyledDropdownContainer = styled.div` + &:hover { + background: ${({ theme }) => theme.background.transparent.light}; + } + cursor: pointer; `; const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID = @@ -59,9 +60,24 @@ export const RecordTableHeaderLastColumn = () => { (scrollWrapperHTMLElement?.clientWidth ?? 0) < (scrollWrapperHTMLElement?.scrollWidth ?? 0); + const isFirstRowActive = useRecoilComponentFamilyValueV2( + isRecordTableRowActiveComponentFamilyState, + 0, + ); + + const isFirstRowFocused = useRecoilComponentFamilyValueV2( + isRecordTableRowFocusedComponentFamilyState, + 0, + ); + + const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; + return ( - <> - + + { scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID, }} /> - - - + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx index b5acdedf9..32c2007e5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx @@ -5,8 +5,13 @@ import { RecordTableCellGrip } from '@/object-record/record-table/record-table-c import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell'; import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells'; import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr'; +import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect'; +import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; +import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect'; import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type RecordTableRowProps = { recordId: string; @@ -23,6 +28,13 @@ export const RecordTableRow = ({ const listenedFields = getDefaultRecordFieldsToListen({ objectNameSingular, }); + const isFocused = useRecoilComponentFamilyValueV2( + isRecordTableRowFocusedComponentFamilyState, + rowIndexForFocus, + ); + const isRowFocusActive = useRecoilComponentValueV2( + isRecordTableRowFocusActiveComponentState, + ); return ( + {isRowFocusActive && isFocused && } 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 new file mode 100644 index 000000000..54056f15b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx @@ -0,0 +1,63 @@ +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; +import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; +import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition'; +import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; +import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected'; +import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { Key } from 'ts-key-enum'; + +export const RecordTableRowHotkeyEffect = () => { + const { isSelected, recordId, objectNameSingular, rowIndex } = + useRecordTableRowContextOrThrow(); + + const { setCurrentRowSelected } = useSetCurrentRowSelected(); + + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); + + const { activateRecordTableRow } = useActiveRecordTableRow(); + + const setIsRowFocusActive = useSetRecoilComponentStateV2( + isRecordTableRowFocusActiveComponentState, + ); + + const setFocusPosition = useSetRecordTableFocusPosition(); + + useScopedHotkeys( + 'x', + () => { + setCurrentRowSelected(!isSelected); + }, + TableHotkeyScope.TableFocus, + ); + + useScopedHotkeys( + [`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`], + () => { + openRecordInCommandMenu({ + recordId: recordId, + objectNameSingular: objectNameSingular, + isNewRecord: false, + }); + + activateRecordTableRow(rowIndex); + }, + TableHotkeyScope.TableFocus, + ); + + useScopedHotkeys( + Key.Enter, + () => { + setIsRowFocusActive(false); + setFocusPosition({ + row: rowIndex, + column: 0, + }); + }, + TableHotkeyScope.TableFocus, + ); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx index a7aa53d9b..301580143 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx @@ -3,17 +3,70 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { isRowVisibleComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowVisibleComponentFamilyState'; +import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; +import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; +import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; import { ReactNode, forwardRef } from 'react'; -const StyledTr = styled.tr<{ isDragging: boolean }>` - position: relative; +const StyledTr = styled.tr<{ + isDragging: boolean; +}>` border: ${({ isDragging, theme }) => isDragging ? `1px solid ${theme.border.color.medium}` : '1px solid transparent'}; + position: relative; transition: border-left-color 0.2s ease-in-out; + + &[data-next-row-active-or-focused='true'] { + td { + border-bottom: none; + } + } + + &[data-focused='true'] { + td { + &:not(:first-of-type) { + border-bottom: 1px solid ${({ theme }) => theme.border.color.medium}; + border-top: 1px solid ${({ theme }) => theme.border.color.medium}; + border-color: ${({ theme }) => theme.border.color.medium}; + background-color: ${({ theme }) => theme.background.tertiary}; + } + &:nth-of-type(2) { + border-left: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm} 0 0 + ${({ theme }) => theme.border.radius.sm}; + } + &:last-of-type { + border-right: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: 0 ${({ theme }) => theme.border.radius.sm} + ${({ theme }) => theme.border.radius.sm} 0; + } + } + } + + &[data-active='true'] { + td { + &:not(:first-of-type) { + border-bottom: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; + border-top: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; + background-color: ${({ theme }) => theme.accent.quaternary}; + } + &:nth-of-type(2) { + border-left: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; + border-radius: ${({ theme }) => theme.border.radius.sm} 0 0 + ${({ theme }) => theme.border.radius.sm}; + } + &:last-of-type { + border-right: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; + border-radius: 0 ${({ theme }) => theme.border.radius.sm} + ${({ theme }) => theme.border.radius.sm} 0; + } + } + } `; type RecordTableTrProps = { @@ -21,7 +74,10 @@ type RecordTableTrProps = { recordId: string; focusIndex: number; isDragging?: boolean; -} & React.ComponentProps; +} & Omit< + React.ComponentProps, + 'isActive' | 'isNextRowActiveOrFocused' | 'isFocused' +>; export const RecordTableTr = forwardRef< HTMLTableRowElement, @@ -38,6 +94,33 @@ export const RecordTableTr = forwardRef< recordId, ); + const isActive = useRecoilComponentFamilyValueV2( + isRecordTableRowActiveComponentFamilyState, + focusIndex, + ); + + const isNextRowActive = useRecoilComponentFamilyValueV2( + isRecordTableRowActiveComponentFamilyState, + focusIndex + 1, + ); + + const isFocused = useRecoilComponentFamilyValueV2( + isRecordTableRowFocusedComponentFamilyState, + focusIndex, + ); + + const isRowFocusActive = useRecoilComponentValueV2( + isRecordTableRowFocusActiveComponentState, + ); + + const isNextRowFocused = useRecoilComponentFamilyValueV2( + isRecordTableRowFocusedComponentFamilyState, + focusIndex + 1, + ); + + const isNextRowActiveOrFocused = + (isRowFocusActive && isNextRowFocused) || isNextRowActive; + return ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/activeRecordTableRowIndexComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/activeRecordTableRowIndexComponentState.ts new file mode 100644 index 000000000..4b5dd9ebc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/activeRecordTableRowIndexComponentState.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 activeRecordTableRowIndexComponentState = createComponentStateV2< + number | null +>({ + key: 'activeRecordTableRowIndexComponentState', + defaultValue: null, + componentInstanceContext: RecordTableComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/focusedRecordTableRowIndexComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/focusedRecordTableRowIndexComponentState.ts new file mode 100644 index 000000000..2f9b4a862 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/focusedRecordTableRowIndexComponentState.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 focusedRecordTableRowIndexComponentState = createComponentStateV2< + number | null +>({ + key: 'focusedRecordTableRowIndexComponentState', + defaultValue: null, + componentInstanceContext: RecordTableComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableCellFocusActiveComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableCellFocusActiveComponentState.ts new file mode 100644 index 000000000..f332feb6d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableCellFocusActiveComponentState.ts @@ -0,0 +1,9 @@ +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const isRecordTableCellFocusActiveComponentState = + createComponentStateV2({ + key: 'isRecordTableCellFocusActiveComponentState', + defaultValue: false, + componentInstanceContext: RecordTableComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState.ts new file mode 100644 index 000000000..21e9288d5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState.ts @@ -0,0 +1,9 @@ +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; + +export const isRecordTableRowActiveComponentFamilyState = + createComponentFamilyStateV2({ + key: 'isRecordTableRowActiveComponentFamilyState', + defaultValue: false, + componentInstanceContext: RecordTableComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableFocusActiveComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusActiveComponentState.ts similarity index 77% rename from packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableFocusActiveComponentState.ts rename to packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusActiveComponentState.ts index 36466471d..5752e9c26 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableFocusActiveComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusActiveComponentState.ts @@ -1,9 +1,9 @@ import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -export const isRecordTableFocusActiveComponentState = +export const isRecordTableRowFocusActiveComponentState = createComponentStateV2({ - key: 'isRecordTableFocusActiveComponentState', + key: 'isRecordTableRowFocusActiveComponentState', defaultValue: false, componentInstanceContext: RecordTableComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState.ts new file mode 100644 index 000000000..aad8da42b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState.ts @@ -0,0 +1,9 @@ +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; + +export const isRecordTableRowFocusedComponentFamilyState = + createComponentFamilyStateV2({ + key: 'isRecordTableRowFocusedComponentFamilyState', + defaultValue: false, + componentInstanceContext: RecordTableComponentInstanceContext, + });