From 6e47c1b5c62b338396e592cea93e8a1ecd5aea5c Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 17 Sep 2025 14:39:44 +0200 Subject: [PATCH] Fix column resize handle (#14559) This PR fixes the column resize handle issues. Fixes in https://github.com/twentyhq/core-team-issues/issues/1490 : - We see the pointer blink when resizing => Already here before refactor, this is because the mouse enter / leave events shouldn't be triggered while resizing. - Resizing handle sometimes cannot be clicked anymore / does not appear anymore (see if another listener doesn't prevent this) The code will need to be cleaned after all fixes have been done though has it duplicates logic between the different header components. --- .../RecordTableHeaderAddColumnButton.tsx | 8 +++++ .../components/RecordTableHeaderCell.tsx | 18 +++++++++- .../RecordTableHeaderCellContainer.tsx | 9 +++-- .../RecordTableHeaderFirstScrollableCell.tsx | 13 +++++-- .../RecordTableHeaderLabelIdentifierCell.tsx | 12 +++++-- .../RecordTableHeaderResizeHandler.tsx | 34 +++++++++++++------ 6 files changed, 77 insertions(+), 17 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx index 4a2b26f07f9..1ab02e37de5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx @@ -5,7 +5,9 @@ import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/co import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth'; import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidthClassName'; import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent'; +import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler'; 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'; @@ -87,6 +89,8 @@ export const RecordTableHeaderAddColumnButton = () => { const shouldDisplayBorderBottom = hasRecordGroups || !isFirstRowActiveOrFocused || isScrolledVertically; + const { visibleRecordFields } = useRecordTableContextOrThrow(); + return ( { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME, )} > + + - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer.tsx index 20f27d3973c..45cb8174e5f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer.tsx @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; const StyledHeaderCell = styled.div<{ zIndex?: number; shouldDisplayBorderBottom: boolean; + isResizing: boolean; }>` color: ${({ theme }) => theme.font.color.tertiary}; padding: 0; @@ -23,7 +24,11 @@ const StyledHeaderCell = styled.div<{ : 'none'}; user-select: none; - ${({ theme }) => { + ${({ theme, isResizing }) => { + if (isResizing) { + return ''; + } + return ` &:hover { background: ${theme.background.secondary}; @@ -34,7 +39,7 @@ const StyledHeaderCell = styled.div<{ `; }}; - cursor: pointer; + cursor: ${({ isResizing }) => (isResizing ? 'col-resize' : 'pointer')}; z-index: ${({ zIndex }) => zIndex ?? 'auto'}; `; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx index 5ef2dbeeff5..67e82ac8e17 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx @@ -13,11 +13,12 @@ import { isRecordTableRowFocusActiveComponentState } from '@/object-record/recor import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; +import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { cx } from '@linaria/core'; -import { filterOutByProperty } from 'twenty-shared/utils'; +import { filterOutByProperty, isDefined } from 'twenty-shared/utils'; export const RecordTableHeaderFirstScrollableCell = () => { const { objectMetadataItem, visibleRecordFields } = @@ -94,6 +95,12 @@ export const RecordTableHeaderFirstScrollableCell = () => { const shouldDisplayBorderBottom = hasRecordGroups || !isFirstRowActiveOrFocused || isScrolledVertically; + const resizedFieldMetadataItemId = useRecoilComponentValue( + resizedFieldMetadataIdComponentState, + ); + + const isResizingAnyColumn = isDefined(resizedFieldMetadataItemId); + if (!recordField) { return <>; } @@ -104,12 +111,14 @@ export const RecordTableHeaderFirstScrollableCell = () => { key={recordField.fieldMetadataItemId} shouldDisplayBorderBottom={shouldDisplayBorderBottom} zIndex={zIndex} + isResizing={isResizingAnyColumn} > + - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCell.tsx index ee69094abf5..57511d7a398 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCell.tsx @@ -13,12 +13,13 @@ import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/reco import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; +import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { cx } from '@linaria/core'; import { useState } from 'react'; -import { findByProperty } from 'twenty-shared/utils'; +import { findByProperty, isDefined } from 'twenty-shared/utils'; const StyledColumnHeadContainer = styled.div` cursor: pointer; @@ -66,6 +67,12 @@ export const RecordTableHeaderLabelIdentifierCell = () => { hasRecordGroupsComponentSelector, ); + const resizedFieldMetadataItemId = useRecoilComponentValue( + resizedFieldMetadataIdComponentState, + ); + + const isResizingAnyColumn = isDefined(resizedFieldMetadataItemId); + const shouldDisplayBorderBottom = hasRecordGroups || !isFirstRowActiveOrFocused || isScrolledVertically; @@ -80,6 +87,7 @@ export const RecordTableHeaderLabelIdentifierCell = () => { onMouseEnter={() => setIconIsVisible(true)} onMouseLeave={() => setIconIsVisible(false)} shouldDisplayBorderBottom={shouldDisplayBorderBottom} + isResizing={isResizingAnyColumn} > { /> {iconIsVisible && } - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler.tsx index 8f585867e80..b3e2d5ef1b5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler.tsx @@ -1,21 +1,24 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import styled from '@emotion/styled'; import { useIsMobile } from 'twenty-ui/utilities'; -const StyledResizeHandler = styled.div<{ isResizing: boolean }>` +const StyledResizeHandler = styled.div<{ + isResizing: boolean; + position: 'left' | 'right'; +}>` bottom: 0; cursor: col-resize; - padding: 0 ${({ theme }) => theme.spacing(2)}; position: absolute; - right: -9px; + ${({ position }) => (position === 'left' ? 'left: -1px;' : 'right: -1px;')} top: 0; - width: 3px; + width: 10px; z-index: 1; - ${({ isResizing, theme }) => { + ${({ isResizing, theme, position }) => { if (isResizing === true) { return `&:after { background-color: ${theme.color.blue}; @@ -23,7 +26,8 @@ const StyledResizeHandler = styled.div<{ isResizing: boolean }>` content: ''; display: block; position: absolute; - right: 8px; + ${position === 'left' ? 'left: -1px;' : 'right: -1px;'} + top: 0; width: 2px; }`; @@ -32,10 +36,19 @@ const StyledResizeHandler = styled.div<{ isResizing: boolean }>` `; export const RecordTableHeaderResizeHandler = ({ - recordField, + recordFieldIndex, + position, }: { - recordField: RecordField; + recordFieldIndex: number; + position: 'left' | 'right'; }) => { + const { visibleRecordFields } = useRecordTableContextOrThrow(); + + const recordField: RecordField | undefined = + position === 'left' + ? visibleRecordFields[recordFieldIndex - 1] + : visibleRecordFields[recordFieldIndex]; + const isMobile = useIsMobile(); const columnResizeDisabled = isMobile; @@ -44,13 +57,13 @@ export const RecordTableHeaderResizeHandler = ({ useRecoilComponentState(resizedFieldMetadataIdComponentState); const isResizing = - recordField.fieldMetadataItemId === resizedFieldMetadataItemId; + recordField?.fieldMetadataItemId === resizedFieldMetadataItemId; const { setDragSelectionStartEnabled } = useDragSelect(); const handlePointerDown = () => { setDragSelectionStartEnabled(false); - setResizedFieldMetadataItemId(recordField.fieldMetadataItemId); + setResizedFieldMetadataItemId(recordField?.fieldMetadataItemId); }; return ( @@ -60,6 +73,7 @@ export const RecordTableHeaderResizeHandler = ({ role="separator" onPointerDown={handlePointerDown} isResizing={isResizing} + position={position} /> ) );