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';