Refactored z-index handling for hovered portal with a better UX (#14384)

This PR closes the first step of
https://github.com/twentyhq/core-team-issues/issues/1451, about z-index
fine-tuning on the table.

The core part was to succeed in having the hovered portal and the
surrounding cells switch their z-index to have the borders overlap
nicely in all scrolling cases.

See associated video for the new behavior.

This PR fixes two majors UX issues we had : 
- The sticky column and row were sliding a bit on the beginning of the
scroll
- Too many components were re-rendering while we only needed to isolate
those who presented a complex use case

In the end, we isolated some very specific components like the cell 0-0
and the first scrollable header cell, which were giving the most
problems with the hovered portal.

This allows to leave the rest of the cells and header cells alone with
those concerns because they weren't involved at all in z-index update,
once we isolated those components.

## Demo



https://github.com/user-attachments/assets/cbe630ed-63c3-4e86-a22e-a11662c6082c



https://github.com/user-attachments/assets/36e6947d-9f89-4ed2-ba6c-cbb13016d7d1
This commit is contained in:
Lucas Bordeau
2025-09-10 11:47:54 +02:00
committed by GitHub
parent cb0552a983
commit 2044be28ec
33 changed files with 1081 additions and 525 deletions
@@ -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 (
<StyledTableContainer ref={containerRef}>
<StyledTableWithPointerEvents
<StyledTable
ref={tableBodyRef}
isDragging={isDragging}
stickyColumnZIndex={computedStickyColumnZIndex}
normalColumnZIndex={computedNormalColumnZIndex}
id={RECORD_TABLE_HTML_ID}
>
<RecordTableHeader />
{hasRecordGroups ? (
@@ -188,15 +196,14 @@ export const RecordTableContent = ({
) : (
<RecordTableNoRecordGroupBody />
)}
<RecordTableStickyEffect />
<RecordTableStickyBottomEffect />
</StyledTableWithPointerEvents>
<RecordTableScrollAndZIndexEffect />
</StyledTable>
<DragSelect
selectableItemsContainerRef={containerRef}
onDragSelectionStart={handleDragStart}
onDragSelectionChange={handleDragSelectionChange}
onDragSelectionEnd={handleDragEnd}
scrollWrapperComponentInstanceId={`record-table-scroll-${recordTableId}`}
scrollWrapperComponentInstanceId={recordTableScrollWrapperId}
selectionBoundaryClass={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
/>
</StyledTableContainer>
@@ -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 <></>;
};
@@ -1,3 +0,0 @@
export const RecordTableStickyBottomEffect = () => {
return <></>;
};
@@ -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 <></>;
};
@@ -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,
};
@@ -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;
@@ -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 (
<Droppable
droppableId={recordGroupId ?? v4Persistable}
@@ -35,7 +47,7 @@ export const RecordTableBodyDroppable = ({
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
onMouseLeave={() => setRecordTableHoverPosition(null)}
onMouseLeave={handleMouseLeave}
>
<RecordTableBodyDroppableContextProvider
value={{ droppablePlaceholder: provided.placeholder }}
@@ -1,6 +1,7 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip';
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
@@ -10,7 +11,7 @@ export const RecordTableBodyLoading = () => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
return (
<tbody>
<RecordTableBody>
{Array.from({ length: 8 }).map((_, rowIndex) => (
<RecordTableRowContextProvider
key={rowIndex}
@@ -45,6 +46,6 @@ export const RecordTableBodyLoading = () => {
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
))}
</tbody>
</RecordTableBody>
);
};
@@ -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};
@@ -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`
@@ -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 (
<RecordTableCellPortalWrapper position={focusedCellPosition}>
{currentTableCellInEditModePosition && (
<StyledRecordTableCellHoveredPortal>
<RecordTableCellPortalRootContainer
zIndex={TABLE_Z_INDEX.cell.editMode}
>
<RecordTableCellEditMode>
<RecordTableCellFieldInput />
</RecordTableCellEditMode>
</StyledRecordTableCellHoveredPortal>
</RecordTableCellPortalRootContainer>
)}
<RecordTableCellHotkeysEffect />
</RecordTableCellPortalWrapper>
@@ -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<DraggableProvidedDragHandleProps> | 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 (
<StyledRecordTableTd
isDragging={isDragging}
backgroundColor={tdBackgroundColor}
borderColor={borderColor}
fontColor={fontColor}
hasRightBorder={hasRightBorder}
hasBottomBorder={hasBottomBorder}
width={width}
zIndex={zIndex}
// eslint-disable-next-line react/jsx-props-no-spreading
{...dragHandleProps}
className="table-cell-0-0"
>
{children}
</StyledRecordTableTd>
);
};
@@ -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 (
<StyledRecordTableCellHoveredPortalContent
isReadOnly={isReadOnly}
isRowActive={isRowActive}
zIndex={computedZIndex}
>
{isFieldInputOnly ? (
<RecordTableCellEditMode>
<RecordTableCellFieldInput />
</RecordTableCellEditMode>
) : (
<RecordTableCellDisplayMode>
<FieldDisplay />
</RecordTableCellDisplayMode>
)}
{showButton && <RecordTableCellEditButton />}
</StyledRecordTableCellHoveredPortalContent>
);
};
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 (
<RecordTableCellPortalWrapper position={hoverPosition}>
<StyledRecordTableCellHoveredPortal>
<RecordTableCellPortalRootContainer zIndex={zIndex}>
<RecordTableCellHoveredPortalContent />
</StyledRecordTableCellHoveredPortal>
</RecordTableCellPortalRootContainer>
</RecordTableCellPortalWrapper>
);
};
@@ -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 (
<StyledRecordTableCellHoveredPortalContent
isReadOnly={isReadOnly}
isRowActive={isRowActive}
>
{isFieldInputOnly ? (
<RecordTableCellEditMode>
<RecordTableCellFieldInput />
</RecordTableCellEditMode>
) : (
<RecordTableCellDisplayMode>
<FieldDisplay />
</RecordTableCellDisplayMode>
)}
{showButton && <RecordTableCellEditButton />}
</StyledRecordTableCellHoveredPortalContent>
);
};
@@ -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 (
<RecordTableRowContextProvider
value={{
recordId,
rowIndex: hoverPosition.row,
isSelected: false,
inView: true,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isRecordReadOnly,
}}
>
<RecordTableCellContext.Provider
value={{
recordField: visibleRecordFields[hoverPosition.column],
cellPosition: hoverPosition,
}}
>
<RecordTableCellFieldContextWrapper
recordField={visibleRecordFields[hoverPosition.column]}
>
{children}
</RecordTableCellFieldContextWrapper>
</RecordTableCellContext.Provider>
</RecordTableRowContextProvider>
);
};
@@ -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;
@@ -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<HTMLAnchorElement>(
const tableCellAnchorElement = document.body.querySelector<HTMLAnchorElement>(
`#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(
<RecordTableRowContextProvider
value={{
recordId,
rowIndex: position.row,
isSelected: false,
inView: true,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isRecordReadOnly,
}}
>
<RecordTableCellContext.Provider
value={{
recordField: visibleRecordFields[position.column],
cellPosition: position,
}}
>
<RecordTableCellFieldContextWrapper
recordField={visibleRecordFields[position.column]}
>
{children}
</RecordTableCellFieldContextWrapper>
</RecordTableCellContext.Provider>
</RecordTableRowContextProvider>,
anchorElement,
<RecordTableCellPortalContexts>
{children}
</RecordTableCellPortalContexts>,
tableCellAnchorElement,
)}
</>
);
@@ -16,7 +16,6 @@ export const RecordTableCellPortals = () => {
return (
<>
<RecordTableCellHoveredPortal />
{isRecordTableFocusActive && (
<>
<RecordTableCellEditModePortal />
@@ -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 = ({
@@ -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 (
<StyledTitle
hideTitle={isLabelIdentifier && isRecordTableScrolledHorizontally}
>
<StyledTitle hideTitle={isLabelIdentifier}>
<StyledIcon>
<Icon size={theme.icon.size.md} />
</StyledIcon>
@@ -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 (
<>
<RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn />
{visibleRecordFields.map((recordField) => (
<RecordTableHeaderLabelIdentifierCell />
<RecordTableHeaderFirstScrollableCell />
{recordFieldsWithoutLabelIdentifierAndFirstOne.map((recordField) => (
<RecordTableHeaderCell
key={recordField.fieldMetadataItemId}
recordField={recordField}
@@ -1,29 +1,15 @@
import styled from '@emotion/styled';
import { useCallback, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
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 { 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 { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useSaveRecordFields } from '@/views/hooks/useSaveRecordFields';
import { throwIfNotDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
const COLUMN_MIN_WIDTH = 48;
@@ -80,17 +66,6 @@ const StyledColumnHeaderCell = styled.div<{
}
`;
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;
`;
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<PointerEventListener>(
({ x }) => {
setInitialPointerPositionX(x);
},
[],
);
const [iconVisibility, setIconVisibility] = useState(false);
const handleResizeHandlerMove = useCallback<PointerEventListener>(
({ 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}
>
<StyledColumnHeadContainer>
@@ -263,29 +131,8 @@ export const RecordTableHeaderCell = ({
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
{(useIsMobile() || iconVisibility) &&
!!isLabelIdentifier &&
!isReadOnly &&
hasObjectUpdatePermissions && (
<StyledHeaderIcon>
<LightIconButton
Icon={IconPlus}
size="small"
accent="tertiary"
onClick={handlePlusButtonClick}
/>
</StyledHeaderIcon>
)}
</StyledColumnHeadContainer>
{!disableColumnResize && (
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId);
}}
/>
)}
<RecordTableHeaderResizeHandler recordField={recordField} />
</StyledColumnHeaderCell>
);
};
@@ -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 (
<StyledColumnHeaderCell
className="header-cell"
key={recordField.fieldMetadataItemId}
isResizing={
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
}
columnWidth={columnWidth}
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
zIndex={zIndex}
>
<StyledColumnHeadContainer>
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
</StyledColumnHeadContainer>
<RecordTableHeaderResizeHandler recordField={recordField} />
</StyledColumnHeaderCell>
);
};
@@ -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 (
<StyledColumnHeaderCell
className="header-cell"
key={recordField.fieldMetadataItemId}
isResizing={
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
}
columnWidth={columnWidth}
onMouseEnter={() => setIconIsVisible(true)}
onMouseLeave={() => setIconIsVisible(false)}
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
>
<StyledColumnHeadContainer>
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
{(isMobile || iconIsVisible) &&
!isReadOnly &&
hasObjectUpdatePermissions && (
<StyledHeaderIcon>
<LightIconButton
Icon={IconPlus}
size="small"
accent="tertiary"
onClick={handlePlusButtonClick}
/>
</StyledHeaderIcon>
)}
</StyledColumnHeadContainer>
<RecordTableHeaderResizeHandler recordField={recordField} />
</StyledColumnHeaderCell>
);
};
@@ -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 && (
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId);
}}
/>
)
);
};
@@ -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<PointerEventListener>(
({ x }) => {
setInitialPointerPositionX(x);
},
[],
);
const handleResizeHandlerMove = useCallback<PointerEventListener>(
({ 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,
});
};
@@ -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 (
<>
<RecordTableCellWrapper
recordField={visibleRecordFields[0]}
recordFieldIndex={0}
>
<RecordTableTd
isSelected={isSelected}
isDragging={isDragging}
width={visibleRecordFields[0].size}
>
<RecordTableCell />
</RecordTableTd>
{isFirstRow ? (
<RecordTableCellFirstRowFirstColumn
isSelected={isSelected}
isDragging={isDragging}
width={visibleRecordFields[0].size}
>
<RecordTableCell />
</RecordTableCellFirstRowFirstColumn>
) : (
<RecordTableTd
isSelected={isSelected}
isDragging={isDragging}
width={visibleRecordFields[0].size}
>
<RecordTableCell />
</RecordTableTd>
)}
</RecordTableCellWrapper>
{recordFieldsAfterFirst.map((recordField, recordFieldIndex) => (
<RecordTableCellWrapper
@@ -22,13 +22,15 @@ const StyledTr = styled.div<{
flex-direction: row;
&[data-next-row-active-or-focused='true'] {
div.table-cell {
div.table-cell,
div.table-cell-0-0 {
border-bottom: none;
}
}
&[data-focused='true'] {
div.table-cell {
div.table-cell,
div.table-cell-0-0 {
&:not(:first-of-type) {
border-bottom: 1px solid ${({ theme }) => 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};
@@ -5,5 +5,5 @@ export const isRecordTableScrolledHorizontallyComponentState =
createComponentState<boolean>({
key: 'isRecordTableScrolledHorizontallyComponentState',
componentInstanceContext: RecordTableComponentInstanceContext,
defaultValue: true,
defaultValue: false,
});
@@ -5,5 +5,5 @@ export const isRecordTableScrolledVerticallyComponentState =
createComponentState<boolean>({
key: 'isRecordTableScrolledVerticallyComponentState',
componentInstanceContext: RecordTableComponentInstanceContext,
defaultValue: true,
defaultValue: false,
});
@@ -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<string>
>({
key: 'resizedFieldMetadataIdComponentState',
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -0,0 +1 @@
export type Nullable<T> = T | null | undefined;
@@ -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';