From 8d10e738acda2c78d20eba7eb8703bbc625f56ca Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 10 Sep 2025 19:04:47 +0200 Subject: [PATCH] Fixed table header resize bugs (#14401) This PR fixes the main resize bugs in https://github.com/twentyhq/core-team-issues/issues/1453 Changes : - A new `RecordTableResizeEffect` has been created to modify the last column width to compensate for the resize width dynamically, because of flew-wrap on the table elements, we cannot use width: 100% everywhere and we have to set the width of everything. - CSS transitions have been removed because they were degrading both the performance and the UX when resizing - Created a common `RecordTableHeaderCellContainer`, this was needed to factorize the various table header components - Created a `RecordTableHeaderLabelIdentifierCellPlusButton` component to ease the reading of `RecordTableHeaderLabelIdentifierCell` - Put the CSS of the blue line while resizing in the `RecordTableHeaderResizeHandler` component to avoid duplicating it everywhere. - Extracted `COLUMN_MIN_WIDTH` in a constant file Fixes https://github.com/twentyhq/core-team-issues/issues/1453 --- .../record-table/components/RecordTable.tsx | 2 +- .../RecordTableBodyEffectsWrapper.tsx | 2 +- .../components/RecordTableContent.tsx | 8 +- .../components/RecordTableEmpty.tsx | 2 +- .../components/RecordTableResizeEffect.tsx | 40 ++++++ .../record-table/constants/ColumnMinWidth.ts | 1 + .../useRecordTableLastColumnWidthToFill.ts | 41 ++++++ .../components/RecordTableBodyDroppable.tsx | 26 ++-- .../components/RecordTableLastEmptyCell.tsx | 21 +++- .../RecordTablePlusButtonCellPlaceholder.tsx | 15 +++ .../RecordTableAggregateFooterCell.tsx | 1 - .../RecordTableHeaderAddColumnButton.tsx | 9 -- .../components/RecordTableHeaderCell.tsx | 83 ++----------- .../RecordTableHeaderCellContainer.tsx | 54 ++++++++ .../RecordTableHeaderFirstScrollableCell.tsx | 90 ++------------ .../RecordTableHeaderLabelIdentifierCell.tsx | 117 ++---------------- ...bleHeaderLabelIdentifierCellPlusButton.tsx | 55 ++++++++ .../RecordTableHeaderLastColumn.tsx | 18 ++- .../RecordTableHeaderResizeHandler.tsx | 40 ++++-- .../hooks/useResizeTableHeader.ts | 43 +++++-- .../components/RecordTableRow.tsx | 2 + .../states/recordTableWidthComponentState.ts | 8 ++ .../drag-select/components/DragSelect.tsx | 2 +- 23 files changed, 364 insertions(+), 316 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableResizeEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/constants/ColumnMinWidth.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/recordTableWidthComponentState.ts 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 1ca11ab00c..48783eeec1 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 @@ -24,7 +24,7 @@ export const RecordTable = () => { objectMetadataItem.id, ); - const tableBodyRef = useRef(null); + const tableBodyRef = useRef(null); const { toggleClickOutside } = useClickOutsideListener( RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffectsWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffectsWrapper.tsx index 70f500e301..2c679f1cd7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffectsWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffectsWrapper.tsx @@ -7,7 +7,7 @@ import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/ export interface RecordTableBodyEffectsWrapperProps { hasRecordGroups: boolean; - tableBodyRef: React.RefObject; + tableBodyRef: React.RefObject; } export const RecordTableBodyEffectsWrapper = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx index 89a8114d9a..b57b52f277 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx @@ -1,3 +1,4 @@ +import { RecordTableResizeEffect } from '@/object-record/record-table/components/RecordTableResizeEffect'; import { RecordTableScrollAndZIndexEffect } from '@/object-record/record-table/components/RecordTableScrollAndZIndexEffect'; import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody'; @@ -23,6 +24,7 @@ const StyledTable = styled.div<{ display: flex; flex-wrap: wrap; + width: 100%; div.header-cell { position: sticky; @@ -36,7 +38,6 @@ const StyledTable = styled.div<{ div.header-cell:nth-of-type(1) { left: 0px; - transition: 0.3s ease; background-color: ${({ theme }) => theme.background.primary}; z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; @@ -46,7 +47,6 @@ const StyledTable = styled.div<{ left: 16px; top: 0; - transition: 0.3s ease; background-color: ${({ theme }) => theme.background.primary}; z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; @@ -56,7 +56,6 @@ const StyledTable = styled.div<{ left: 48px; right: 0; - transition: 0.3s ease; background-color: ${({ theme }) => theme.background.primary}; z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; @@ -143,7 +142,7 @@ const StyledTableContainer = styled.div` `; export interface RecordTableContentProps { - tableBodyRef: React.RefObject; + tableBodyRef: React.RefObject; handleDragSelectionStart: () => void; handleDragSelectionEnd: () => void; hasRecordGroups: boolean; @@ -197,6 +196,7 @@ export const RecordTableContent = ({ )} + ; + tableBodyRef: React.RefObject; } export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableResizeEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableResizeEffect.tsx new file mode 100644 index 0000000000..80deb08742 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableResizeEffect.tsx @@ -0,0 +1,40 @@ +import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState'; +import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; + +import { useEffect } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +export const RecordTableResizeEffect = () => { + const setRecordTableWidth = useSetRecoilComponentState( + recordTableWidthComponentState, + ); + + const { scrollWrapperHTMLElement } = useScrollWrapperElement(); + + useEffect(() => { + const tableWidth = scrollWrapperHTMLElement?.clientWidth ?? 0; + + setRecordTableWidth(tableWidth); + + const tableResizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === scrollWrapperHTMLElement) { + const newWidth = scrollWrapperHTMLElement.clientWidth; + + setRecordTableWidth(newWidth); + } + } + }); + + if (isDefined(scrollWrapperHTMLElement)) { + tableResizeObserver.observe(scrollWrapperHTMLElement); + } + + return () => { + tableResizeObserver.disconnect(); + }; + }, [setRecordTableWidth, scrollWrapperHTMLElement]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/ColumnMinWidth.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/ColumnMinWidth.ts new file mode 100644 index 0000000000..8ba4ab7897 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/ColumnMinWidth.ts @@ -0,0 +1 @@ +export const COLUMN_MIN_WIDTH = 48; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill.ts new file mode 100644 index 0000000000..506e1c3566 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill.ts @@ -0,0 +1,41 @@ +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { sumByProperty } from 'twenty-shared/utils'; + +// TODO: use this everywhere and extract in files +export const RECORD_TABLE_DRAG_DROP_COLUMN_WIDTH = 16; +export const RECORD_TABLE_CHECKBOX_COLUMN_WIDTH = 32; +export const RECORD_TABLE_PLUS_BUTTON_COLUMN_WIDTH = 32; + +export const useRecordTableLastColumnWidthToFill = () => { + const { visibleRecordFields } = useRecordTableContextOrThrow(); + + const recordTableWidth = useRecoilComponentValue( + recordTableWidthComponentState, + ); + + const totalColumnsWidth = visibleRecordFields.reduce( + sumByProperty('size'), + 0, + ); + + const widthOfBorders = visibleRecordFields.length; + + const fixedColumnsWidth = + RECORD_TABLE_DRAG_DROP_COLUMN_WIDTH + + RECORD_TABLE_CHECKBOX_COLUMN_WIDTH + + RECORD_TABLE_PLUS_BUTTON_COLUMN_WIDTH + + widthOfBorders; + + const remainingWidthToFill = Math.max( + 0, + recordTableWidth - fixedColumnsWidth - totalColumnsWidth, + ); + + const width = remainingWidthToFill > 0 ? remainingWidthToFill : 0; + + return { + lastColumnWidth: width, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx index 24c8e89530..ab9ea02a04 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx @@ -20,7 +20,6 @@ export const RecordTableBodyDroppable = ({ isDropDisabled, }: RecordTableBodyDroppableProps) => { const [v4Persistable] = useState(v4()); - const recordTableBodyId = `record-table-body${recordGroupId ? '-' + recordGroupId : ''}`; const setRecordTableHoverPosition = useSetRecoilComponentState( recordTableHoverPositionComponentState, @@ -42,19 +41,20 @@ export const RecordTableBodyDroppable = ({ isDropDisabled={isDropDisabled} > {(provided) => ( - - + - {children} - - + + {children} + + + )} ); 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 3dc2cac90a..5f5fcf6436 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 @@ -1,8 +1,27 @@ import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; +import { useRecordTableLastColumnWidthToFill } from '@/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; +import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; export const RecordTableLastEmptyCell = () => { const { isSelected } = useRecordTableRowContextOrThrow(); + const { lastColumnWidth } = useRecordTableLastColumnWidthToFill(); - return ; + const resizeFieldOffset = useRecoilComponentValue( + resizeFieldOffsetComponentState, + ); + + const width = + resizeFieldOffset > 0 + ? lastColumnWidth + resizeFieldOffset + : lastColumnWidth; + + return ( + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder.tsx new file mode 100644 index 0000000000..a15ba68702 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder.tsx @@ -0,0 +1,15 @@ +import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; +import { RECORD_TABLE_PLUS_BUTTON_COLUMN_WIDTH } from '@/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill'; +import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; + +export const RecordTablePlusButtonCellPlaceholder = () => { + const { isSelected } = useRecordTableRowContextOrThrow(); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx index b1092e72b1..127124664f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx @@ -21,7 +21,6 @@ const StyledColumnFooterCell = styled.div<{ width: ${columnWidth}px; `} text-align: left; - transition: 0.3s ease; ${({ theme }) => { return ` &:hover { 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 34e4522268..1c74f23793 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,13 +5,11 @@ import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useTheme } from '@emotion/react'; import { IconPlus } from 'twenty-ui/display'; const StyledPlusIconHeaderCell = styled.div<{ - isTableWiderThanScreen: boolean; isFirstRowActiveOrFocused: boolean; }>` border-bottom: ${({ isFirstRowActiveOrFocused, theme }) => @@ -53,12 +51,6 @@ const StyledDropdownContainer = styled.div` export const RecordTableHeaderAddColumnButton = () => { const theme = useTheme(); - const { scrollWrapperHTMLElement } = useScrollWrapperElement(); - - const isTableWiderThanScreen = - (scrollWrapperHTMLElement?.clientWidth ?? 0) < - (scrollWrapperHTMLElement?.scrollWidth ?? 0); - const isFirstRowActive = useRecoilComponentFamilyValue( isRecordTableRowActiveComponentFamilyState, 0, @@ -73,7 +65,6 @@ export const RecordTableHeaderAddColumnButton = () => { return ( 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 77b8200be5..1caf9b90c5 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 @@ -1,8 +1,8 @@ -import styled from '@emotion/styled'; - import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; +import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer'; import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; @@ -11,71 +11,6 @@ import { resizeFieldOffsetComponentState } from '@/object-record/record-table/st import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -const COLUMN_MIN_WIDTH = 48; - -const StyledColumnHeaderCell = styled.div<{ - columnWidth: number; - isResizing?: boolean; - isFirstRowActiveOrFocused: boolean; -}>` - color: ${({ theme }) => theme.font.color.tertiary}; - padding: 0; - text-align: left; - - height: 32px; - max-height: 32px; - - background-color: ${({ theme }) => theme.background.primary}; - border-right: 1px solid ${({ theme }) => theme.border.color.light}; - - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - - ${({ columnWidth }) => ` - min-width: ${columnWidth}px; - width: ${columnWidth}px; - `} - user-select: none; - ${({ theme }) => { - return ` - &:hover { - background: ${theme.background.secondary}; - }; - &:active { - background: ${theme.background.tertiary}; - }; - `; - }}; - ${({ isResizing, theme }) => { - if (isResizing === true) { - return `&:after { - background-color: ${theme.color.blue}; - bottom: 0; - content: ''; - display: block; - position: absolute; - right: -1px; - top: 0; - width: 2px; - }`; - } - }}; - - // TODO: refactor this, each component should own its CSS - div { - overflow: hidden; - } -`; - -const StyledColumnHeadContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - - & > :first-of-type { - flex: 1; - } -`; - type RecordTableHeaderCellProps = { recordField: RecordField; }; @@ -117,7 +52,7 @@ export const RecordTableHeaderCell = ({ const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; return ( - - - - + - + ); }; 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 new file mode 100644 index 0000000000..19c5403eba --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer.tsx @@ -0,0 +1,54 @@ +import styled from '@emotion/styled'; + +const StyledHeaderCell = styled.div<{ + columnWidth: number; + isResizing?: boolean; + isFirstRowActiveOrFocused: boolean; + zIndex?: number; +}>` + color: ${({ theme }) => theme.font.color.tertiary}; + padding: 0; + text-align: left; + + height: 32px; + max-height: 32px; + + background-color: ${({ theme }) => theme.background.primary}; + border-right: 1px solid ${({ theme }) => theme.border.color.light}; + + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + + ${({ columnWidth }) => ` + min-width: ${columnWidth}px; + width: ${columnWidth}px; + `} + user-select: none; + ${({ theme }) => { + return ` + &:hover { + background: ${theme.background.secondary}; + }; + &:active { + background: ${theme.background.tertiary}; + }; + `; + }}; + ${({ isResizing, theme }) => { + if (isResizing === true) { + return `&:after { + background-color: ${theme.color.blue}; + bottom: 0; + content: ''; + display: block; + position: absolute; + right: -1px; + top: 0; + width: 2px; + }`; + } + }}; + + z-index: ${({ zIndex }) => zIndex ?? 'auto'}; +`; + +export const RecordTableHeaderCellContainer = StyledHeaderCell; 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 3290e46764..4c6d6c40ed 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 @@ -1,12 +1,13 @@ -import styled from '@emotion/styled'; - import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler'; -import { COLUMN_RESIZE_MIN_WIDTH } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader'; + +import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer'; + +import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; @@ -17,73 +18,6 @@ import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-st import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { filterOutByProperty } from 'twenty-shared/utils'; -// TODO: factorize duplicated code here -const StyledColumnHeaderCell = styled.div<{ - columnWidth: number; - isResizing?: boolean; - isFirstRowActiveOrFocused: boolean; - zIndex: number; -}>` - color: ${({ theme }) => theme.font.color.tertiary}; - padding: 0; - text-align: left; - - height: 32px; - max-height: 32px; - - background-color: ${({ theme }) => theme.background.primary}; - border-right: 1px solid ${({ theme }) => theme.border.color.light}; - - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - - ${({ columnWidth }) => ` - min-width: ${columnWidth}px; - width: ${columnWidth}px; - `} - user-select: none; - ${({ theme }) => { - return ` - &:hover { - background: ${theme.background.secondary}; - }; - &:active { - background: ${theme.background.tertiary}; - }; - `; - }}; - ${({ isResizing, theme }) => { - if (isResizing === true) { - return `&:after { - background-color: ${theme.color.blue}; - bottom: 0; - content: ''; - display: block; - position: absolute; - right: -1px; - top: 0; - width: 2px; - }`; - } - }}; - - // TODO: refactor this, each component should own its CSS - div { - overflow: hidden; - } - - z-index: ${({ zIndex }) => zIndex}; -`; - -const StyledColumnHeadContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - - & > :first-of-type { - flex: 1; - } -`; - export const RecordTableHeaderFirstScrollableCell = () => { const { objectMetadataItem, visibleRecordFields } = useRecordTableContextOrThrow(); @@ -124,7 +58,7 @@ export const RecordTableHeaderFirstScrollableCell = () => { const computedDynamicWidth = baseWidth + widthOffsetWhileResizing; - const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH); + const columnWidth = Math.max(computedDynamicWidth, COLUMN_MIN_WIDTH); const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; @@ -151,7 +85,7 @@ export const RecordTableHeaderFirstScrollableCell = () => { } return ( - { isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} zIndex={zIndex} > - - - + - + ); }; 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 6b61250240..716c62aee2 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 @@ -1,102 +1,40 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; -import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; - -import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; -import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler'; -import { COLUMN_RESIZE_MIN_WIDTH } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader'; + +import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer'; + +import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth'; +import { RecordTableHeaderLabelIdentifierCellPlusButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useState } from 'react'; import { findByProperty } from 'twenty-shared/utils'; -import { IconPlus } from 'twenty-ui/display'; -import { LightIconButton } from 'twenty-ui/input'; - -// TODO: factorize duplicated code here -const StyledColumnHeaderCell = styled.div<{ - columnWidth: number; - isResizing?: boolean; - isFirstRowActiveOrFocused: boolean; -}>` - color: ${({ theme }) => theme.font.color.tertiary}; - padding: 0; - text-align: left; - - height: 32px; - max-height: 32px; - - background-color: ${({ theme }) => theme.background.primary}; - border-right: 1px solid ${({ theme }) => theme.border.color.light}; - - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - - ${({ columnWidth }) => ` - min-width: ${columnWidth}px; - width: ${columnWidth}px; - `} - user-select: none; - ${({ theme }) => { - return ` - &:hover { - background: ${theme.background.secondary}; - }; - &:active { - background: ${theme.background.tertiary}; - }; - `; - }}; - ${({ isResizing, theme }) => { - if (isResizing === true) { - return `&:after { - background-color: ${theme.color.blue}; - bottom: 0; - content: ''; - display: block; - position: absolute; - right: -1px; - top: 0; - width: 2px; - }`; - } - }}; - - // TODO: refactor this, each component should own its CSS - div { - overflow: hidden; - } -`; const StyledColumnHeadContainer = styled.div` display: flex; flex-direction: row; justify-content: space-between; + overflow: hidden; & > :first-of-type { flex: 1; } `; -const StyledHeaderIcon = styled.div` - margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)}; -`; - export const RecordTableHeaderLabelIdentifierCell = () => { - const { objectMetadataItem, objectPermissions, visibleRecordFields } = + const { objectMetadataItem, visibleRecordFields } = useRecordTableContextOrThrow(); const [iconIsVisible, setIconIsVisible] = useState(false); - const isMobile = useIsMobile(); - const isFirstRowActive = useRecoilComponentFamilyValue( isRecordTableRowActiveComponentFamilyState, 0, @@ -113,10 +51,6 @@ export const RecordTableHeaderLabelIdentifierCell = () => { findByProperty('fieldMetadataItemId', labelIdentifierFieldMetadataItem?.id), ); - const { createNewIndexRecord } = useCreateNewIndexRecord({ - objectMetadataItem, - }); - const resizeFieldOffset = useRecoilComponentValue( resizeFieldOffsetComponentState, ); @@ -125,25 +59,10 @@ export const RecordTableHeaderLabelIdentifierCell = () => { resizedFieldMetadataIdComponentState, ); - const hasAnySoftDeleteFilterOnView = useRecoilComponentValue( - hasAnySoftDeleteFilterOnViewComponentSelector, - ); - if (!recordField) { return <>; } - const handlePlusButtonClick = () => { - createNewIndexRecord(); - }; - - const isReadOnly = isObjectMetadataReadOnly({ - objectPermissions, - objectMetadataItem, - }); - - const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; - const widthOffsetWhileResizing = resizedFieldMetadataItemId === recordField.fieldMetadataItemId ? resizeFieldOffset @@ -153,12 +72,12 @@ export const RecordTableHeaderLabelIdentifierCell = () => { const computedDynamicWidth = baseWidth + widthOffsetWhileResizing; - const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH); + const columnWidth = Math.max(computedDynamicWidth, COLUMN_MIN_WIDTH); const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; return ( - { recordField={recordField} objectMetadataId={objectMetadataItem.id} /> - {(isMobile || iconIsVisible) && - !isReadOnly && - hasObjectUpdatePermissions && - !hasAnySoftDeleteFilterOnView && ( - - - - )} + {iconIsVisible && } - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx new file mode 100644 index 0000000000..991a661e52 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx @@ -0,0 +1,55 @@ +import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; +import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import styled from '@emotion/styled'; +import { IconPlus } from 'twenty-ui/display'; +import { LightIconButton } from 'twenty-ui/input'; +import { useIsMobile } from 'twenty-ui/utilities'; + +const StyledHeaderIcon = styled.div` + margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)}; +`; + +export const RecordTableHeaderLabelIdentifierCellPlusButton = () => { + const { objectMetadataItem, objectPermissions } = + useRecordTableContextOrThrow(); + + const isMobile = useIsMobile(); + + const { createNewIndexRecord } = useCreateNewIndexRecord({ + objectMetadataItem, + }); + + const handlePlusButtonClick = () => { + createNewIndexRecord(); + }; + + const isReadOnly = isObjectMetadataReadOnly({ + objectPermissions, + objectMetadataItem, + }); + + const hasAnySoftDeleteFilterOnView = useRecoilComponentValue( + hasAnySoftDeleteFilterOnViewComponentSelector, + ); + + const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; + + return ( + !isMobile && + !isReadOnly && + hasObjectUpdatePermissions && + !hasAnySoftDeleteFilterOnView && ( + + + + ) + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx index 6ded68f2c9..1d20b38ce6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx @@ -1,19 +1,29 @@ +import { useRecordTableLastColumnWidthToFill } from '@/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill'; +import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; -const StyledLastColumnHeader = styled.div` +const StyledLastColumnHeader = styled.div<{ width: number }>` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; background-color: ${({ theme }) => theme.background.primary}; border-left: none !important; color: ${({ theme }) => theme.font.color.tertiary}; - width: fit-content; height: 32px; max-height: 32px; + + width: ${({ width }) => width}px; `; export const RecordTableHeaderLastColumn = () => { - return ( - + const { lastColumnWidth } = useRecordTableLastColumnWidthToFill(); + + const resizeFieldOffset = useRecoilComponentValue( + resizeFieldOffsetComponentState, ); + + const width = lastColumnWidth - resizeFieldOffset; + + return ; }; 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 b5fb05ebba..8f585867e8 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,10 +1,11 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +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` +const StyledResizeHandler = styled.div<{ isResizing: boolean }>` bottom: 0; cursor: col-resize; padding: 0 ${({ theme }) => theme.spacing(2)}; @@ -13,6 +14,21 @@ const StyledResizeHandler = styled.div` top: 0; width: 3px; z-index: 1; + + ${({ isResizing, theme }) => { + if (isResizing === true) { + return `&:after { + background-color: ${theme.color.blue}; + bottom: 0; + content: ''; + display: block; + position: absolute; + right: 8px; + top: 0; + width: 2px; + }`; + } + }}; `; export const RecordTableHeaderResizeHandler = ({ @@ -24,18 +40,26 @@ export const RecordTableHeaderResizeHandler = ({ const columnResizeDisabled = isMobile; - const setResizedFieldMetadataItemId = useSetRecoilComponentState( - resizedFieldMetadataIdComponentState, - ); + const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = + useRecoilComponentState(resizedFieldMetadataIdComponentState); + + const isResizing = + recordField.fieldMetadataItemId === resizedFieldMetadataItemId; + + const { setDragSelectionStartEnabled } = useDragSelect(); + + const handlePointerDown = () => { + setDragSelectionStartEnabled(false); + setResizedFieldMetadataItemId(recordField.fieldMetadataItemId); + }; return ( !columnResizeDisabled && ( { - setResizedFieldMetadataItemId(recordField.fieldMetadataItemId); - }} + onPointerDown={handlePointerDown} + isResizing={isResizing} /> ) ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResizeTableHeader.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResizeTableHeader.ts index 0a1b6af91f..1feb3cb7f3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResizeTableHeader.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResizeTableHeader.ts @@ -1,7 +1,11 @@ import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; + import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; @@ -13,8 +17,6 @@ import { useCallback, useState } from 'react'; import { useRecoilCallback } from 'recoil'; import { findByProperty, throwIfNotDefined } from 'twenty-shared/utils'; -export const COLUMN_RESIZE_MIN_WIDTH = 48; - export const useResizeTableHeader = () => { const { visibleRecordFields } = useRecordTableContextOrThrow(); @@ -33,33 +35,48 @@ export const useResizeTableHeader = () => { const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = useRecoilComponentState(resizedFieldMetadataIdComponentState); + const recordField = visibleRecordFields.find( + findByProperty('fieldMetadataItemId', resizedFieldMetadataItemId), + ); + + const { resetTableRowSelection } = useResetTableRowSelection(); + const { saveRecordFields } = useSaveRecordFields(); const { updateRecordField } = useUpdateRecordField(); const handleResizeHandlerStart = useCallback( ({ x }) => { + resetTableRowSelection(); setInitialPointerPositionX(x); }, - [], + [resetTableRowSelection], ); const handleResizeHandlerMove = useCallback( ({ x }) => { if (!initialPointerPositionX) return; + throwIfNotDefined(recordField, 'recordField'); + + const newResizeOffset = x - initialPointerPositionX; + + const newRecordFieldSizeWithOffset = recordField.size + newResizeOffset; + + if (newRecordFieldSizeWithOffset < COLUMN_MIN_WIDTH) { + return; + } + setResizeFieldOffset(x - initialPointerPositionX); }, - [setResizeFieldOffset, initialPointerPositionX], + [setResizeFieldOffset, initialPointerPositionX, recordField], ); + const { setDragSelectionStartEnabled } = useDragSelect(); + const handleResizeHandlerEnd = useRecoilCallback( ({ snapshot, set }) => async () => { - const recordField = visibleRecordFields.find( - findByProperty('fieldMetadataItemId', resizedFieldMetadataItemId), - ); - throwIfNotDefined(recordField, 'recordField'); if (!resizedFieldMetadataItemId) return; @@ -70,10 +87,7 @@ export const useResizeTableHeader = () => { ); const nextWidth = Math.round( - Math.max( - recordField.size + resizeFieldOffset, - COLUMN_RESIZE_MIN_WIDTH, - ), + Math.max(recordField.size + resizeFieldOffset, COLUMN_MIN_WIDTH), ); set(resizeFieldOffsetCallbackState, 0); @@ -90,6 +104,8 @@ export const useResizeTableHeader = () => { saveRecordFields([updatedRecordField]); } + + setDragSelectionStartEnabled(true); }, [ saveRecordFields, @@ -97,7 +113,8 @@ export const useResizeTableHeader = () => { resizeFieldOffsetCallbackState, setResizedFieldMetadataItemId, updateRecordField, - visibleRecordFields, + setDragSelectionStartEnabled, + recordField, ], ); 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 d128c04883..de8a3a258b 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 @@ -2,6 +2,7 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox'; import { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip'; import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell'; +import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder'; 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 { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect'; @@ -51,6 +52,7 @@ export const RecordTableRow = ({ + ({ + key: 'recordTableWidthComponentState', + defaultValue: 0, + componentInstanceContext: RecordTableComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx index 8170e77345..68eab7e4fb 100644 --- a/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx @@ -1,11 +1,11 @@ import styled from '@emotion/styled'; import { type RefObject, useCallback, useState } from 'react'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useDragSelectWithAutoScroll } from '@/ui/utilities/drag-select/hooks/useDragSelectWithAutoScroll'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { isDefined } from 'twenty-shared/utils'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; -import { useDragSelect } from '../hooks/useDragSelect'; import { type SelectionBox } from '../types/SelectionBox'; import { isValidSelectionStart } from '../utils/selectionBoxValidation';