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 9d40f65001..89a8114d9a 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,25 +1,21 @@ -import { RecordTableStickyBottomEffect } from '@/object-record/record-table/components/RecordTableStickyBottomEffect'; -import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect'; +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'; import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; -import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; -import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass'; import { useRecoilComponentFamilyCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyCallbackState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; import { useRef, useState } from 'react'; import { useRecoilCallback } from 'recoil'; import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; -const StyledTableWithPointerEvents = styled.div<{ +export const RECORD_TABLE_HTML_ID = 'record-table'; + +const StyledTable = styled.div<{ isDragging: boolean; - stickyColumnZIndex: number; - normalColumnZIndex: number; }>` & > * { pointer-events: ${({ isDragging }) => (isDragging ? 'none' : 'auto')}; @@ -33,35 +29,38 @@ const StyledTableWithPointerEvents = styled.div<{ top: 0; } - div.header-cell:nth-of-type(n + 3) { - z-index: ${({ normalColumnZIndex }) => normalColumnZIndex}; + div.header-cell:nth-of-type(n + 5) { + z-index: ${TABLE_Z_INDEX.headerColumnsNormal}; } div.header-cell:nth-of-type(1) { - // position: sticky; left: 0px; - z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; + transition: 0.3s ease; background-color: ${({ theme }) => theme.background.primary}; + + z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; } div.header-cell:nth-of-type(2) { - // position: sticky; left: 16px; top: 0; - z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; + transition: 0.3s ease; background-color: ${({ theme }) => theme.background.primary}; + + z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; } div.header-cell:nth-of-type(3) { - // position: sticky; left: 48px; right: 0; - z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; + transition: 0.3s ease; background-color: ${({ theme }) => theme.background.primary}; + z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; + // &::after { // content: ''; // position: absolute; @@ -80,6 +79,40 @@ const StyledTableWithPointerEvents = styled.div<{ } } + // TODO: re-implement horizontal scroll here after table have been refactored to divs + div.table-cell:nth-of-type(1) { + position: sticky; + left: 0px; + z-index: ${TABLE_Z_INDEX.cell.sticky}; + } + + div.table-cell:nth-of-type(2) { + position: sticky; + left: 16px; + z-index: ${TABLE_Z_INDEX.cell.sticky}; + } + + div.table-cell-0-0 { + position: sticky; + left: 48px; + + @media (max-width: ${MOBILE_VIEWPORT}px) { + width: ${38}px; + max-width: ${38}px; + } + } + + div.table-cell:nth-of-type(3) { + position: sticky; + left: 48px; + z-index: ${TABLE_Z_INDEX.cell.sticky}; + + @media (max-width: ${MOBILE_VIEWPORT}px) { + width: ${38}px; + max-width: ${38}px; + } + } + div.footer-cell:nth-of-type(n + 3) { z-index: ${TABLE_Z_INDEX.footer.default}; @@ -148,39 +181,14 @@ export const RecordTableContent = ({ [isRowSelectedCallbackFamilyState], ); - const isRecordTableScrolledHorizontally = useRecoilComponentValue( - isRecordTableScrolledHorizontallyComponentState, - ); - - const isRecordTableScrolledVertically = useRecoilComponentValue( - isRecordTableScrolledVerticallyComponentState, - ); - - const computedStickyColumnZIndex = - isRecordTableScrolledHorizontally && isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.headerColumnsSticky - : isRecordTableScrolledHorizontally - ? TABLE_Z_INDEX.scrolledHorizontallyOnly.headerColumnsSticky - : isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledVerticallyOnly.headerColumnsSticky - : TABLE_Z_INDEX.noScrollAtAll.headerColumnsSticky; - - const computedNormalColumnZIndex = - isRecordTableScrolledHorizontally && isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.headerColumnsNormal - : isRecordTableScrolledHorizontally - ? TABLE_Z_INDEX.scrolledHorizontallyOnly.headerColumnsNormal - : isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledVerticallyOnly.headerColumnsNormal - : TABLE_Z_INDEX.noScrollAtAll.headerColumnsNormal; + const recordTableScrollWrapperId = `record-table-scroll-${recordTableId}`; return ( - {hasRecordGroups ? ( @@ -188,15 +196,14 @@ export const RecordTableContent = ({ ) : ( )} - - - + + diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollAndZIndexEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollAndZIndexEffect.tsx new file mode 100644 index 0000000000..9cc57c6604 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollAndZIndexEffect.tsx @@ -0,0 +1,71 @@ +import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; +import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; + +import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; + +import { useEffect, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +export const RecordTableScrollAndZIndexEffect = () => { + const { scrollWrapperHTMLElement } = useScrollWrapperElement(); + + const setIsRecordTableScrolledHorizontally = useSetRecoilComponentState( + isRecordTableScrolledHorizontallyComponentState, + ); + + const setIsRecordTableScrolledVertically = useSetRecoilComponentState( + isRecordTableScrolledVerticallyComponentState, + ); + + const [isScrolledVertically, setIsScrolledVertically] = useState(false); + const [isScrolledHorizontally, setIsScrolledHorizontally] = useState(false); + + useEffect(() => { + if (!isDefined(scrollWrapperHTMLElement)) { + return; + } + + const handleScroll = (event: any) => { + const target = event.currentTarget; + + let somethingHasChanged = false; + + const newIsScrolledVertically = target?.scrollTop > 0; + + if (newIsScrolledVertically !== isScrolledVertically) { + setIsScrolledVertically(newIsScrolledVertically); + setIsRecordTableScrolledVertically(newIsScrolledVertically); + somethingHasChanged = true; + } + + const newIsScrolledHorizontally = target?.scrollLeft > 0; + + if (newIsScrolledHorizontally !== isScrolledHorizontally) { + setIsScrolledHorizontally(newIsScrolledHorizontally); + setIsRecordTableScrolledHorizontally(newIsScrolledHorizontally); + somethingHasChanged = true; + } + + if (!somethingHasChanged) { + return; + } + + // TODO: insert imperative CSS update here + }; + + scrollWrapperHTMLElement?.addEventListener('scroll', handleScroll); + + return () => { + scrollWrapperHTMLElement?.removeEventListener('scroll', handleScroll); + }; + }, [ + scrollWrapperHTMLElement, + isScrolledVertically, + isScrolledHorizontally, + setIsRecordTableScrolledVertically, + setIsRecordTableScrolledHorizontally, + ]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx deleted file mode 100644 index a0f744beac..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const RecordTableStickyBottomEffect = () => { - return <>; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx deleted file mode 100644 index a06a937885..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect } from 'react'; - -import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; -import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; -import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState'; -import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; - -export const RecordTableStickyEffect = () => { - const scrollTop = useRecoilComponentValue( - scrollWrapperScrollTopComponentState, - ); - - const setIsRecordTableScrolledVertically = useSetRecoilComponentState( - isRecordTableScrolledVerticallyComponentState, - ); - - useEffect(() => { - setIsRecordTableScrolledVertically(scrollTop > 0); - }, [scrollTop, setIsRecordTableScrolledVertically]); - - const scrollLeft = useRecoilComponentValue( - scrollWrapperScrollLeftComponentState, - ); - - const setIsRecordTableScrolledHorizontally = useSetRecoilComponentState( - isRecordTableScrolledHorizontallyComponentState, - ); - - useEffect(() => { - setIsRecordTableScrolledHorizontally(scrollLeft > 0); - }, [scrollLeft, setIsRecordTableScrolledHorizontally]); - - return <>; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts index ce735819d0..0ef114ba5c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts @@ -1,37 +1,41 @@ export const TABLE_Z_INDEX = { base: 1, cell: { - default: 3, - sticky: 10, - editMode: 20, + default: 8, + sticky: 12, + editMode: 30, }, + headerColumnsSticky: 14, + headerColumnsNormal: 10, + firstCellWithoutVerticalScroll: 15, + firstCellWithVerticalScroll: 12, + noScrollAtAll: { + hoverPortalCellOnFirstScrollableColumn: 17, + hoverPortalCellOnNormalColumn: 17, + hoverPortalCellOnLabelIdentifierColumn: 17, + firstScrollableHeaderCell: 12, + }, + scrolledBothVerticallyAndHorizontally: { + hoverPortalCellOnNormalColumn: 2, + hoverPortalCellOnFirstScrollableColumn: 2, + hoverPortalCellOnLabelIdentifierColumn: 2, + firstScrollableHeaderCell: 12, + }, + scrolledHorizontallyOnly: { + hoverPortalCellOnLabelIdentifierColumn: 17, + hoverPortalCellOnNormalColumn: 11, + hoverPortalCellOnFirstScrollableColumn: 11, + firstScrollableHeaderCell: 10, + }, + scrolledVerticallyOnly: { + hoverPortalCellOnNormalColumn: 9, + hoverPortalCellOnFirstScrollableColumn: 13, + hoverPortalCellOnLabelIdentifierColumn: 13, + firstScrollableHeaderCell: 14, + }, + columnGrip: 30, footer: { default: 18, stickyColumn: 20, }, - noScrollAtAll: { - hoverPortalCell: 17, - headerRow: 13, - headerColumnsSticky: 16, - headerColumnsNormal: 15, - }, - scrolledBothVerticallyAndHorizontally: { - headerColumnsSticky: 13, - headerColumnsNormal: 12, - headerRow: 11, - hoverPortalCell: 2, - }, - scrolledHorizontallyOnly: { - headerColumnsSticky: 13, - headerColumnsNormal: 12, - headerRow: 11, - hoverPortalCell: 9, - }, - scrolledVerticallyOnly: { - headerColumnsSticky: 14, - headerColumnsNormal: 13, - headerRow: 12, - hoverPortalCell: 11, - }, - columnGrip: 30, }; 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 43356d772f..00595fe97d 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 @@ -1,35 +1,8 @@ import styled from '@emotion/styled'; -import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; -import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; - -const StyledTbody = styled.div` - // TODO: re-implement horizontal scroll here after table have been refactored to divs - div.table-cell:nth-of-type(1) { - position: sticky; - left: 0px; - z-index: ${TABLE_Z_INDEX.cell.sticky}; - } - - div.table-cell:nth-of-type(2) { - position: sticky; - left: 16px; - z-index: ${TABLE_Z_INDEX.cell.sticky}; - } - - div.table-cell:nth-of-type(3) { - position: sticky; - left: 48px; - z-index: ${TABLE_Z_INDEX.cell.sticky}; - - @media (max-width: ${MOBILE_VIEWPORT}px) { - width: ${38}px; - max-width: ${38}px; - } - } - +const StyledTableBody = styled.div` display: flex; flex-wrap: wrap; `; -export const RecordTableBody = StyledTbody; +export const RecordTableBody = StyledTableBody; 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 0593f5e6f3..24c8e89530 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 @@ -1,6 +1,8 @@ import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody'; import { RecordTableBodyDroppableContextProvider } from '@/object-record/record-table/record-table-body/contexts/RecordTableBodyDroppableContext'; import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; +import { isSomeCellInEditModeComponentSelector } from '@/object-record/record-table/states/selectors/isSomeCellInEditModeComponentSelector'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { Droppable } from '@hello-pangea/dnd'; import { type ReactNode, useState } from 'react'; @@ -24,6 +26,16 @@ export const RecordTableBodyDroppable = ({ recordTableHoverPositionComponentState, ); + const isSomeCellInEditMode = useRecoilComponentValue( + isSomeCellInEditModeComponentSelector, + ); + + const handleMouseLeave = () => { + if (!isSomeCellInEditMode) { + setRecordTableHoverPosition(null); + } + }; + return ( setRecordTableHoverPosition(null)} + onMouseLeave={handleMouseLeave} > { const { visibleRecordFields } = useRecordTableContextOrThrow(); return ( - + {Array.from({ length: 8 }).map((_, rowIndex) => ( { ))} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx index fdd195906c..033913d879 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx @@ -24,9 +24,10 @@ const StyledBaseContainer = styled.div<{ cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')}; display: flex; height: 32px; - position: relative; user-select: none; + position: relative; + &.focus-active { border-radius: ${BORDER_COMMON.radius.sm}; outline: 1px solid ${({ borderColorBlue }) => borderColorBlue}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx index c02a012137..4c0752676a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx @@ -3,7 +3,6 @@ import { RecordFieldComponentInstanceContext } from '@/object-record/record-fiel import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState'; -import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { useFocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useFocusRecordTableCell'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; @@ -28,7 +27,6 @@ const StyledEditableCellEditModeContainer = styled.div<{ height: 100%; position: absolute; width: calc(100% + 2px); - z-index: ${TABLE_Z_INDEX.cell.editMode}; `; const StyledInputModeOnlyContainer = styled.div` diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal.tsx index ac55d52a4a..018e2a73d8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal.tsx @@ -1,20 +1,13 @@ import { RecordTableCellPortalWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; 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 { RecordTableCellHotkeysEffect } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHotkeysEffect'; +import { RecordTableCellPortalRootContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer'; import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState'; import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState'; -import styled from '@emotion/styled'; - -const StyledRecordTableCellHoveredPortal = styled.div` - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; -`; export const RecordTableCellEditModePortal = () => { const focusedCellPosition = useRecoilComponentValue( @@ -32,11 +25,13 @@ export const RecordTableCellEditModePortal = () => { return ( {currentTableCellInEditModePosition && ( - + - + )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFirstRowFirstColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFirstRowFirstColumn.tsx new file mode 100644 index 0000000000..7d6a097ebb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFirstRowFirstColumn.tsx @@ -0,0 +1,69 @@ +import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; +import { + DEFAULT_RECORD_TABLE_TD_WIDTH, + StyledTd, +} from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; +import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import styled from '@emotion/styled'; +import { type DraggableProvidedDragHandleProps } from '@hello-pangea/dnd'; +import { useContext, type ReactNode } from 'react'; +import { ThemeContext } from 'twenty-ui/theme'; + +const StyledRecordTableTd = styled(StyledTd)<{ zIndex: number }>` + z-index: ${({ zIndex }) => zIndex}; +`; + +export const RecordTableCellFirstRowFirstColumn = ({ + children, + isSelected, + isDragging, + hasRightBorder = true, + hasBottomBorder = true, + width = DEFAULT_RECORD_TABLE_TD_WIDTH, + ...dragHandleProps +}: { + className?: string; + children?: ReactNode; + isSelected?: boolean; + isDragging?: boolean; + hasRightBorder?: boolean; + hasBottomBorder?: boolean; + width?: number; +} & (Partial | null)) => { + const { theme } = useContext(ThemeContext); + + const [isRecordTableScrolledVertically] = useRecoilComponentState( + isRecordTableScrolledVerticallyComponentState, + ); + + const zIndex = isRecordTableScrolledVertically + ? TABLE_Z_INDEX.firstCellWithVerticalScroll + : TABLE_Z_INDEX.firstCellWithoutVerticalScroll; + + const tdBackgroundColor = isSelected + ? theme.accent.quaternary + : theme.background.primary; + + const borderColor = theme.border.color.light; + + const fontColor = theme.font.color.primary; + + return ( + + {children} + + ); +}; 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 75db4572cb..9e8f43d3f0 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 @@ -1,137 +1,78 @@ import { RecordTableCellPortalWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper'; import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import styled from '@emotion/styled'; -import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay'; -import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; -import { useIsFieldInputOnly } from '@/object-record/record-field/ui/hooks/useIsFieldInputOnly'; import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; -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 { RecordTableCellHoveredPortalContent } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortalContent'; +import { RecordTableCellPortalRootContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer'; import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; -import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; -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; - zIndex: number; -}>` - align-items: center; - background: ${({ theme }) => theme.background.transparent.secondary}; - background-color: ${({ theme, isRowActive }) => - isRowActive ? theme.accent.quaternary : theme.background.primary}; - border-radius: ${({ isReadOnly }) => - !isReadOnly ? BORDER_COMMON.radius.sm : 'none'}; - box-sizing: border-box; - cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')}; - display: flex; - - height: 32px; - - outline: ${({ theme, isReadOnly, isRowActive }) => - isRowActive - ? 'none' - : isReadOnly - ? `1px solid ${theme.border.color.medium}` - : `1px solid ${theme.font.color.extraLight}`}; - - position: relative; - user-select: none; - - z-index: ${({ zIndex }) => zIndex}; -`; - -const RecordTableCellHoveredPortalContent = () => { - const hoverPosition = useRecoilComponentValue( - recordTableHoverPositionComponentState, - ); - - const isMobile = useIsMobile(); - - const isFirstColumn = hoverPosition?.column === 0; - - const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext); - - const isFieldInputOnly = useIsFieldInputOnly(); - - const showButton = - !isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn); - - const { rowIndex } = useRecordTableRowContextOrThrow(); - - const isRowActive = useRecoilComponentFamilyValue( - isRecordTableRowActiveComponentFamilyState, - rowIndex, - ); - - const isRecordTableScrolledHorizontally = useRecoilComponentValue( - isRecordTableScrolledHorizontallyComponentState, - ); - - const isRecordTableScrolledVertically = useRecoilComponentValue( - isRecordTableScrolledVerticallyComponentState, - ); - - const computedZIndex = - isRecordTableScrolledHorizontally && isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.hoverPortalCell - : isRecordTableScrolledHorizontally - ? TABLE_Z_INDEX.scrolledHorizontallyOnly.hoverPortalCell - : isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledVerticallyOnly.hoverPortalCell - : TABLE_Z_INDEX.noScrollAtAll.hoverPortalCell; - - return ( - - {isFieldInputOnly ? ( - - - - ) : ( - - - - )} - {showButton && } - - ); -}; - -const StyledRecordTableCellHoveredPortal = styled.div` - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; -`; export const RecordTableCellHoveredPortal = () => { const hoverPosition = useRecoilComponentValue( recordTableHoverPositionComponentState, ); + const isRecordTableScrolledVertically = useRecoilComponentValue( + isRecordTableScrolledVerticallyComponentState, + ); + + const isRecordTableScrolledHorizontally = useRecoilComponentValue( + isRecordTableScrolledHorizontallyComponentState, + ); + if (!hoverPosition) { return null; } + const isOnFirstScrollableColumn = hoverPosition.column === 1; + const isOnLabelIdentifierStickyColumn = hoverPosition.column === 0; + + const zIndexForFirstScrollableColumn = + isRecordTableScrolledHorizontally && isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally + .hoverPortalCellOnFirstScrollableColumn + : isRecordTableScrolledHorizontally + ? TABLE_Z_INDEX.scrolledHorizontallyOnly + .hoverPortalCellOnFirstScrollableColumn + : isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledVerticallyOnly + .hoverPortalCellOnFirstScrollableColumn + : TABLE_Z_INDEX.noScrollAtAll.hoverPortalCellOnFirstScrollableColumn; + + const zIndexForLabelIdentifierStickyColumn = + isRecordTableScrolledHorizontally && isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally + .hoverPortalCellOnLabelIdentifierColumn + : isRecordTableScrolledHorizontally + ? TABLE_Z_INDEX.scrolledHorizontallyOnly + .hoverPortalCellOnLabelIdentifierColumn + : isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledVerticallyOnly + .hoverPortalCellOnLabelIdentifierColumn + : TABLE_Z_INDEX.noScrollAtAll.hoverPortalCellOnLabelIdentifierColumn; + + const zIndexForNormalColumn = + isRecordTableScrolledHorizontally && isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally + .hoverPortalCellOnNormalColumn + : isRecordTableScrolledHorizontally + ? TABLE_Z_INDEX.scrolledHorizontallyOnly.hoverPortalCellOnNormalColumn + : isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledVerticallyOnly.hoverPortalCellOnNormalColumn + : TABLE_Z_INDEX.noScrollAtAll.hoverPortalCellOnNormalColumn; + + const zIndex = isOnFirstScrollableColumn + ? zIndexForFirstScrollableColumn + : isOnLabelIdentifierStickyColumn + ? zIndexForLabelIdentifierStickyColumn + : zIndexForNormalColumn; + return ( - + - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortalContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortalContent.tsx new file mode 100644 index 0000000000..352187df4c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortalContent.tsx @@ -0,0 +1,85 @@ +import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { useIsFieldInputOnly } from '@/object-record/record-field/ui/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 { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; +import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import styled from '@emotion/styled'; +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, isRowActive }) => + isRowActive ? theme.accent.quaternary : theme.background.primary}; + border-radius: ${({ isReadOnly }) => + !isReadOnly ? BORDER_COMMON.radius.sm : 'none'}; + box-sizing: border-box; + cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')}; + display: flex; + + height: 32px; + + outline: ${({ theme, isReadOnly, isRowActive }) => + isRowActive + ? 'none' + : isReadOnly + ? `1px solid ${theme.border.color.medium}` + : `1px solid ${theme.font.color.extraLight}`}; + + user-select: none; +`; + +export const RecordTableCellHoveredPortalContent = () => { + const hoverPosition = useRecoilComponentValue( + recordTableHoverPositionComponentState, + ); + + const isMobile = useIsMobile(); + + const isFirstColumn = hoverPosition?.column === 0; + + const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext); + + const isFieldInputOnly = useIsFieldInputOnly(); + + const showButton = + !isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn); + + const { rowIndex } = useRecordTableRowContextOrThrow(); + + const isRowActive = useRecoilComponentFamilyValue( + isRecordTableRowActiveComponentFamilyState, + rowIndex, + ); + + return ( + + {isFieldInputOnly ? ( + + + + ) : ( + + + + )} + {showButton && } + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalContexts.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalContexts.tsx new file mode 100644 index 0000000000..81142766ca --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalContexts.tsx @@ -0,0 +1,74 @@ +import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; +import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; +import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; +import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext'; +import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; +import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { isDefined } from 'twenty-shared/utils'; + +export const RecordTableCellPortalContexts = ({ + children, +}: { + children: React.ReactNode; +}) => { + const hoverPosition = useRecoilComponentValue( + recordTableHoverPositionComponentState, + ); + + const allRecordIds = useRecoilComponentValue( + recordIndexAllRecordIdsComponentSelector, + ); + + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, + ); + + const recordId = isDefined(hoverPosition) + ? allRecordIds.at(hoverPosition.row) + : null; + + const isRecordReadOnly = useIsRecordReadOnly({ + recordId: recordId ?? '', + objectMetadataId: objectMetadataItem.id, + }); + + if (!hoverPosition || !isDefined(recordId)) { + return null; + } + + return ( + + + + {children} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer.tsx new file mode 100644 index 0000000000..0a5d952230 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer.tsx @@ -0,0 +1,15 @@ +import styled from '@emotion/styled'; + +const StyledRecordTableCellPortalRootContainer = styled.div<{ + zIndex?: number; +}>` + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: ${({ zIndex }) => zIndex ?? 'auto'}; +`; + +export const RecordTableCellPortalRootContainer = + StyledRecordTableCellPortalRootContainer; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx index 71893c5f5c..6d89924a40 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx @@ -1,13 +1,5 @@ -import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; -import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; -import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly'; -import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; -import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; -import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; -import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext'; -import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; +import { RecordTableCellPortalContexts } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalContexts'; import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { createPortal } from 'react-dom'; import { isDefined } from 'twenty-shared/utils'; @@ -18,62 +10,21 @@ export const RecordTableCellPortalWrapper = ({ position: TableCellPosition; children: React.ReactNode; }) => { - const anchorElement = document.body.querySelector( + const tableCellAnchorElement = document.body.querySelector( `#record-table-cell-${position.column}-${position.row}`, ); - const allRecordIds = useRecoilComponentValue( - recordIndexAllRecordIdsComponentSelector, - ); - - const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); - - const visibleRecordFields = useRecoilComponentValue( - visibleRecordFieldsComponentSelector, - ); - - const recordId = allRecordIds.at(position.row); - - const isRecordReadOnly = useIsRecordReadOnly({ - recordId: recordId ?? '', - objectMetadataId: objectMetadataItem.id, - }); - - if (!isDefined(anchorElement) || !isDefined(recordId)) { + if (!isDefined(tableCellAnchorElement)) { return null; } return ( <> {createPortal( - - - - {children} - - - , - anchorElement, + + {children} + , + tableCellAnchorElement, )} ); 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 fa4d31d6e8..d9242014e6 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 @@ -16,7 +16,6 @@ export const RecordTableCellPortals = () => { return ( <> - {isRecordTableFocusActive && ( <> 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 034227018d..29060bebd4 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 @@ -5,7 +5,7 @@ import { ThemeContext } from 'twenty-ui/theme'; export const DEFAULT_RECORD_TABLE_TD_WIDTH = 32; -const StyledTd = styled.div<{ +export const StyledTd = styled.div<{ backgroundColor: string; borderColor: string; isDragging?: boolean; @@ -32,8 +32,6 @@ const StyledTd = styled.div<{ background: ${({ backgroundColor, isDragging }) => isDragging ? 'transparent' : backgroundColor}; - - // TODO: reimplement horizontal scroll here once we have refactored body with divs `; export const RecordTableTd = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx index dbe7bac4bd..c6b6f0aa69 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx @@ -1,12 +1,9 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; - import { fieldMetadataItemByIdSelector } from '@/object-metadata/states/fieldMetadataItemByIdSelector'; import { isFieldMetadataItemLabelIdentifierSelector } from '@/object-metadata/states/isFieldMetadataItemLabelIdentifierSelector'; import { type RecordField } from '@/object-record/record-field/types/RecordField'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilValue } from 'recoil'; import { useIcons } from 'twenty-ui/display'; import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; @@ -61,10 +58,6 @@ export const RecordTableColumnHead = ({ }), ); - const isRecordTableScrolledHorizontally = useRecoilComponentValue( - isRecordTableScrolledHorizontallyComponentState, - ); - const { getIcon } = useIcons(); const Icon = getIcon( correspondingFieldMetadataItem.foundFieldMetadataItem?.icon, @@ -76,10 +69,10 @@ export const RecordTableColumnHead = ({ }), ); + // TODO: fix previous behavior with isRecordTableScrolledHorizontally + return ( - + diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index 73f40907a3..176df3742b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -1,20 +1,39 @@ +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableHeaderAddColumnButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton'; import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell'; import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn'; import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn'; +import { RecordTableHeaderFirstScrollableCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell'; +import { RecordTableHeaderLabelIdentifierCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCell'; import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn'; +import { useResizeTableHeader } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader'; +import { filterOutByProperty } from 'twenty-shared/utils'; export const FIRST_TH_WIDTH = '10px'; export const RecordTableHeader = () => { const { visibleRecordFields } = useRecordTableContextOrThrow(); + const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow(); + + const recordFieldsWithoutLabelIdentifierAndFirstOne = visibleRecordFields + .filter( + filterOutByProperty( + 'fieldMetadataItemId', + labelIdentifierFieldMetadataItem?.id, + ), + ) + .slice(1); + + useResizeTableHeader(); return ( <> - {visibleRecordFields.map((recordField) => ( + + + {recordFieldsWithoutLabelIdentifierAndFirstOne.map((recordField) => ( theme.spacing(2)}; - position: absolute; - right: -9px; - top: 0; - width: 3px; - z-index: 1; -`; - const StyledColumnHeadContainer = styled.div` display: flex; flex-direction: row; @@ -101,10 +76,6 @@ const StyledColumnHeadContainer = styled.div` } `; -const StyledHeaderIcon = styled.div` - margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)}; -`; - type RecordTableHeaderCellProps = { recordField: RecordField; }; @@ -112,116 +83,7 @@ type RecordTableHeaderCellProps = { export const RecordTableHeaderCell = ({ recordField, }: RecordTableHeaderCellProps) => { - const { objectMetadataItem, objectPermissions } = - useRecordTableContextOrThrow(); - - const resizeFieldOffsetState = useRecoilComponentCallbackState( - resizeFieldOffsetComponentState, - ); - - const [resizeFieldOffset, setResizeFieldOffset] = useRecoilComponentState( - resizeFieldOffsetComponentState, - ); - - const [initialPointerPositionX, setInitialPointerPositionX] = useState< - number | null - >(null); - const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = useState< - string | null - >(null); - - const { saveRecordFields } = useSaveRecordFields(); - - const { updateRecordField } = useUpdateRecordField(); - - const handleResizeHandlerStart = useCallback( - ({ x }) => { - setInitialPointerPositionX(x); - }, - [], - ); - - const [iconVisibility, setIconVisibility] = useState(false); - - const handleResizeHandlerMove = useCallback( - ({ x }) => { - if (!initialPointerPositionX) return; - setResizeFieldOffset(x - initialPointerPositionX); - }, - [setResizeFieldOffset, initialPointerPositionX], - ); - - const handleResizeHandlerEnd = useRecoilCallback( - ({ snapshot, set }) => - async () => { - throwIfNotDefined(recordField, 'recordField'); - - if (!resizedFieldMetadataItemId) return; - - const resizeFieldOffset = getSnapshotValue( - snapshot, - resizeFieldOffsetState, - ); - - const nextWidth = Math.round( - Math.max(recordField.size + resizeFieldOffset, COLUMN_MIN_WIDTH), - ); - - set(resizeFieldOffsetState, 0); - setInitialPointerPositionX(null); - setResizedFieldMetadataItemId(null); - - if (nextWidth !== recordField.size) { - const updatedRecordField = updateRecordField( - resizedFieldMetadataItemId, - { - size: nextWidth, - }, - ); - - saveRecordFields([updatedRecordField]); - } - }, - [ - recordField, - saveRecordFields, - resizedFieldMetadataItemId, - resizeFieldOffsetState, - setResizedFieldMetadataItemId, - updateRecordField, - ], - ); - - useTrackPointer({ - shouldTrackPointer: resizedFieldMetadataItemId !== null, - onMouseDown: handleResizeHandlerStart, - onMouseMove: handleResizeHandlerMove, - onMouseUp: handleResizeHandlerEnd, - }); - - const isMobile = useIsMobile(); - - const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow(); - - const isLabelIdentifier = - recordField.fieldMetadataItemId === labelIdentifierFieldMetadataItem?.id; - - const disableColumnResize = isLabelIdentifier || isMobile; - - const { createNewIndexRecord } = useCreateNewIndexRecord({ - objectMetadataItem, - }); - - const handlePlusButtonClick = () => { - createNewIndexRecord(); - }; - - const isReadOnly = isObjectMetadataReadOnly({ - objectPermissions, - objectMetadataItem, - }); - - const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; + const { objectMetadataItem } = useRecordTableContextOrThrow(); const isFirstRowActive = useRecoilComponentFamilyValue( isRecordTableRowActiveComponentFamilyState, @@ -233,6 +95,14 @@ export const RecordTableHeaderCell = ({ 0, ); + const resizeFieldOffset = useRecoilComponentValue( + resizeFieldOffsetComponentState, + ); + + const resizedFieldMetadataItemId = useRecoilComponentValue( + resizedFieldMetadataIdComponentState, + ); + const widthOffsetWhileResizing = resizedFieldMetadataItemId === recordField.fieldMetadataItemId ? resizeFieldOffset @@ -254,8 +124,6 @@ export const RecordTableHeaderCell = ({ resizedFieldMetadataItemId === recordField.fieldMetadataItemId } columnWidth={columnWidth} - onMouseEnter={() => setIconVisibility(true)} - onMouseLeave={() => setIconVisibility(false)} isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} > @@ -263,29 +131,8 @@ export const RecordTableHeaderCell = ({ recordField={recordField} objectMetadataId={objectMetadataItem.id} /> - {(useIsMobile() || iconVisibility) && - !!isLabelIdentifier && - !isReadOnly && - hasObjectUpdatePermissions && ( - - - - )} - {!disableColumnResize && ( - { - setResizedFieldMetadataItemId(recordField.fieldMetadataItemId); - }} - /> - )} + ); }; 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 new file mode 100644 index 0000000000..3290e46764 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx @@ -0,0 +1,173 @@ +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 { 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'; +import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; +import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; +import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; +import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; +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(); + + const isFirstRowActive = useRecoilComponentFamilyValue( + isRecordTableRowActiveComponentFamilyState, + 0, + ); + + const isFirstRowFocused = useRecoilComponentFamilyValue( + isRecordTableRowFocusedComponentFamilyState, + 0, + ); + + const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow(); + + const recordField = visibleRecordFields.filter( + filterOutByProperty( + 'fieldMetadataItemId', + labelIdentifierFieldMetadataItem?.id, + ), + )[0] as RecordField | undefined; + + const resizeFieldOffset = useRecoilComponentValue( + resizeFieldOffsetComponentState, + ); + + const resizedFieldMetadataItemId = useRecoilComponentValue( + resizedFieldMetadataIdComponentState, + ); + + const widthOffsetWhileResizing = + resizedFieldMetadataItemId === recordField?.fieldMetadataItemId + ? resizeFieldOffset + : 0; + + const baseWidth = recordField?.size ?? 0; + + const computedDynamicWidth = baseWidth + widthOffsetWhileResizing; + + const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH); + + const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; + + const isRecordTableScrolledVertically = useRecoilComponentValue( + isRecordTableScrolledVerticallyComponentState, + ); + + const isRecordTableScrolledHorizontally = useRecoilComponentValue( + isRecordTableScrolledHorizontallyComponentState, + ); + + const zIndex = + isRecordTableScrolledHorizontally && isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally + .firstScrollableHeaderCell + : isRecordTableScrolledHorizontally + ? TABLE_Z_INDEX.scrolledHorizontallyOnly.firstScrollableHeaderCell + : isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledVerticallyOnly.firstScrollableHeaderCell + : TABLE_Z_INDEX.noScrollAtAll.firstScrollableHeaderCell; + + if (!recordField) { + return <>; + } + + return ( + + + + + + + ); +}; 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 new file mode 100644 index 0000000000..9bc43b03da --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCell.tsx @@ -0,0 +1,188 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; + +import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; + +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 { 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 { 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; + + & > :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 } = + useRecordTableContextOrThrow(); + + const [iconIsVisible, setIconIsVisible] = useState(false); + + const isMobile = useIsMobile(); + + const isFirstRowActive = useRecoilComponentFamilyValue( + isRecordTableRowActiveComponentFamilyState, + 0, + ); + + const isFirstRowFocused = useRecoilComponentFamilyValue( + isRecordTableRowFocusedComponentFamilyState, + 0, + ); + + const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow(); + + const recordField = visibleRecordFields.find( + findByProperty('fieldMetadataItemId', labelIdentifierFieldMetadataItem?.id), + ); + + const { createNewIndexRecord } = useCreateNewIndexRecord({ + objectMetadataItem, + }); + + const resizeFieldOffset = useRecoilComponentValue( + resizeFieldOffsetComponentState, + ); + + const resizedFieldMetadataItemId = useRecoilComponentValue( + resizedFieldMetadataIdComponentState, + ); + + if (!recordField) { + return <>; + } + + const handlePlusButtonClick = () => { + createNewIndexRecord(); + }; + + const isReadOnly = isObjectMetadataReadOnly({ + objectPermissions, + objectMetadataItem, + }); + + const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; + + const widthOffsetWhileResizing = + resizedFieldMetadataItemId === recordField.fieldMetadataItemId + ? resizeFieldOffset + : 0; + + const baseWidth = recordField?.size ?? 0; + + const computedDynamicWidth = baseWidth + widthOffsetWhileResizing; + + const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH); + + const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; + + return ( + setIconIsVisible(true)} + onMouseLeave={() => setIconIsVisible(false)} + isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} + > + + + {(isMobile || iconIsVisible) && + !isReadOnly && + hasObjectUpdatePermissions && ( + + + + )} + + + + ); +}; 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 new file mode 100644 index 0000000000..b5fb05ebba --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler.tsx @@ -0,0 +1,42 @@ +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 styled from '@emotion/styled'; +import { useIsMobile } from 'twenty-ui/utilities'; + +const StyledResizeHandler = styled.div` + bottom: 0; + cursor: col-resize; + padding: 0 ${({ theme }) => theme.spacing(2)}; + position: absolute; + right: -9px; + top: 0; + width: 3px; + z-index: 1; +`; + +export const RecordTableHeaderResizeHandler = ({ + recordField, +}: { + recordField: RecordField; +}) => { + const isMobile = useIsMobile(); + + const columnResizeDisabled = isMobile; + + const setResizedFieldMetadataItemId = useSetRecoilComponentState( + resizedFieldMetadataIdComponentState, + ); + + return ( + !columnResizeDisabled && ( + { + setResizedFieldMetadataItemId(recordField.fieldMetadataItemId); + }} + /> + ) + ); +}; 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 new file mode 100644 index 0000000000..0a1b6af91f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResizeTableHeader.ts @@ -0,0 +1,110 @@ +import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; +import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; +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'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useSaveRecordFields } from '@/views/hooks/useSaveRecordFields'; +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(); + + const resizeFieldOffsetCallbackState = useRecoilComponentCallbackState( + resizeFieldOffsetComponentState, + ); + + const setResizeFieldOffset = useSetRecoilComponentState( + resizeFieldOffsetComponentState, + ); + + const [initialPointerPositionX, setInitialPointerPositionX] = useState< + number | null + >(null); + + const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = + useRecoilComponentState(resizedFieldMetadataIdComponentState); + + const { saveRecordFields } = useSaveRecordFields(); + + const { updateRecordField } = useUpdateRecordField(); + + const handleResizeHandlerStart = useCallback( + ({ x }) => { + setInitialPointerPositionX(x); + }, + [], + ); + + const handleResizeHandlerMove = useCallback( + ({ x }) => { + if (!initialPointerPositionX) return; + + setResizeFieldOffset(x - initialPointerPositionX); + }, + [setResizeFieldOffset, initialPointerPositionX], + ); + + const handleResizeHandlerEnd = useRecoilCallback( + ({ snapshot, set }) => + async () => { + const recordField = visibleRecordFields.find( + findByProperty('fieldMetadataItemId', resizedFieldMetadataItemId), + ); + + throwIfNotDefined(recordField, 'recordField'); + + if (!resizedFieldMetadataItemId) return; + + const resizeFieldOffset = getSnapshotValue( + snapshot, + resizeFieldOffsetCallbackState, + ); + + const nextWidth = Math.round( + Math.max( + recordField.size + resizeFieldOffset, + COLUMN_RESIZE_MIN_WIDTH, + ), + ); + + set(resizeFieldOffsetCallbackState, 0); + setInitialPointerPositionX(null); + setResizedFieldMetadataItemId(null); + + if (nextWidth !== recordField.size) { + const updatedRecordField = updateRecordField( + resizedFieldMetadataItemId, + { + size: nextWidth, + }, + ); + + saveRecordFields([updatedRecordField]); + } + }, + [ + saveRecordFields, + resizedFieldMetadataItemId, + resizeFieldOffsetCallbackState, + setResizedFieldMetadataItemId, + updateRecordField, + visibleRecordFields, + ], + ); + + useTrackPointer({ + shouldTrackPointer: resizedFieldMetadataItemId !== null, + onMouseDown: handleResizeHandlerStart, + onMouseMove: handleResizeHandlerMove, + onMouseUp: handleResizeHandlerEnd, + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx index baf22cc37f..3bbb60b797 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx @@ -2,12 +2,13 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext'; import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell'; +import { RecordTableCellFirstRowFirstColumn } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFirstRowFirstColumn'; import { RecordTableCellWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellWrapper'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; export const RecordTableCellsVisible = () => { - const { isSelected } = useRecordTableRowContextOrThrow(); + const { isSelected, rowIndex } = useRecordTableRowContextOrThrow(); const { isDragging } = useRecordTableRowDraggableContextOrThrow(); @@ -19,19 +20,31 @@ export const RecordTableCellsVisible = () => { const recordFieldsAfterFirst = visibleRecordFields.slice(1); + const isFirstRow = rowIndex === 0; + return ( <> - - - + {isFirstRow ? ( + + + + ) : ( + + + + )} {recordFieldsAfterFirst.map((recordField, recordFieldIndex) => ( theme.border.color.medium}; border-top: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -49,7 +51,8 @@ const StyledTr = styled.div<{ } &[data-active='true'] { - div.table-cell { + div.table-cell, + div.table-cell-0-0 { &:not(:first-of-type) { border-bottom: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; border-top: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState.ts index 663b866979..105faa4151 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState.ts @@ -5,5 +5,5 @@ export const isRecordTableScrolledHorizontallyComponentState = createComponentState({ key: 'isRecordTableScrolledHorizontallyComponentState', componentInstanceContext: RecordTableComponentInstanceContext, - defaultValue: true, + defaultValue: false, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState.ts index 0e4e3ef5ff..683116ac4c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState.ts @@ -5,5 +5,5 @@ export const isRecordTableScrolledVerticallyComponentState = createComponentState({ key: 'isRecordTableScrolledVerticallyComponentState', componentInstanceContext: RecordTableComponentInstanceContext, - defaultValue: true, + defaultValue: false, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/resizedFieldMetadataIdComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/resizedFieldMetadataIdComponentState.ts new file mode 100644 index 0000000000..7ace7819d5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/resizedFieldMetadataIdComponentState.ts @@ -0,0 +1,11 @@ +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { type Nullable } from 'twenty-shared/types'; + +export const resizedFieldMetadataIdComponentState = createComponentState< + Nullable +>({ + key: 'resizedFieldMetadataIdComponentState', + defaultValue: null, + componentInstanceContext: RecordTableComponentInstanceContext, +}); diff --git a/packages/twenty-shared/src/types/Nullable.ts b/packages/twenty-shared/src/types/Nullable.ts new file mode 100644 index 0000000000..7b5ecba936 --- /dev/null +++ b/packages/twenty-shared/src/types/Nullable.ts @@ -0,0 +1 @@ +export type Nullable = T | null | undefined; \ No newline at end of file diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index d298fa231a..743ec5fefd 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -18,6 +18,7 @@ export { FieldMetadataType } from './FieldMetadataType'; export type { FromTo } from './FromToType'; export type { IsExactly } from './IsExactly'; export type { NonNullableRequired } from './NonNullableRequired'; +export type { Nullable } from './Nullable'; export type { NullablePartial } from './NullablePartial'; export type { ObjectPermissions } from './ObjectPermissions'; export type { ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated';