Table refactor : removed z-index dynamic logic completely and flex-wrap (#18466)

This PR removes the leftovers from the z-index dynamic logic removal.

It also removes the flex-wrap mechanism that was used to have all the
cells in the same div, and instead creates a container for each part of
the table : header, body and footer, so that z-index management becomes
straighforward.

We also fix some minor bugs.

## Demo 


https://github.com/user-attachments/assets/29dc4966-376d-4eb1-9e37-99769e77f4f4



https://github.com/user-attachments/assets/78218517-812a-4531-84c3-067700b46b59
This commit is contained in:
Lucas Bordeau
2026-03-10 14:31:43 +01:00
committed by GitHub
parent 045faf018a
commit dee55b635f
26 changed files with 131 additions and 465 deletions
@@ -15,12 +15,12 @@ import { recordTableHoverPositionComponentState } from '@/object-record/record-t
import { isSomeCellInEditModeComponentSelector } from '@/object-record/record-table/states/selectors/isSomeCellInEditModeComponentSelector';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateCallbackState';
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { styled } from '@linaria/react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useStore } from 'jotai';
import { useCallback, useMemo, useRef, useState } from 'react';
const StyledTableContainer = styled.div`
display: flex;
@@ -142,7 +142,6 @@ export const RecordTableContent = ({
id={RECORD_TABLE_HTML_ID}
onMouseMove={handleDelegatedMouseMove}
onMouseLeave={handleMouseLeave}
hasRecordGroups={hasRecordGroups}
>
<RecordTableHeader />
{hasRecordGroups ? (
@@ -1,4 +1,3 @@
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RecordTableColumnWidthEffect } from '@/object-record/record-table/components/RecordTableColumnWidthEffect';
import {
getRecordTableColumnWidthInlineStyles,
@@ -18,7 +17,6 @@ import { resizeFieldOffsetComponentState } from '@/object-record/record-table/st
import { shouldCompactRecordTableFirstColumnComponentState } from '@/object-record/record-table/states/shouldCompactRecordTableFirstColumnComponentState';
import { computeVisibleRecordFieldsWidthOnTable } from '@/object-record/record-table/utils/computeVisibleRecordFieldsWidthOnTable';
import { RecordTableVirtualizedDataChangedEffect } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedDataChangedEffect';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { useMemo } from 'react';
@@ -81,10 +79,6 @@ export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
emptyTableContainerComputedWidth,
);
const hasRecordGroups = useAtomComponentSelectorValue(
hasRecordGroupsComponentSelector,
);
const columnWidthStyles = useMemo(
() => getRecordTableColumnWidthInlineStyles(visibleRecordFields),
[visibleRecordFields],
@@ -96,7 +90,6 @@ export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
ref={tableBodyRef}
style={columnWidthStyles}
id={RECORD_TABLE_HTML_ID}
hasRecordGroups={hasRecordGroups}
>
<RecordTableHeader />
</RecordTableStyleWrapper>
@@ -5,8 +5,15 @@ import { RecordTableVirtualizedDebugHelper } from '@/object-record/record-table/
import { NUMBER_OF_VIRTUALIZED_ROWS } from '@/object-record/record-table/virtualization/constants/NumberOfVirtualizedRows';
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { getContiguousIncrementalValues } from 'twenty-shared/utils';
const StyledNoRecordGroupContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
export const RecordTableNoRecordGroupRows = () => {
const totalNumberOfRecordsToVirtualize =
useAtomComponentStateValue(
@@ -21,7 +28,7 @@ export const RecordTableNoRecordGroupRows = () => {
const virtualRowIndices = getContiguousIncrementalValues(numberOfRows);
return (
<>
<StyledNoRecordGroupContainer>
<RecordTableVirtualizedBodyPlaceholder />
{virtualRowIndices.map((virtualRowIndex) => {
return (
@@ -33,6 +40,6 @@ export const RecordTableNoRecordGroupRows = () => {
})}
<RecordTableNoRecordGroupAddNew />
<RecordTableVirtualizedDebugHelper />
</>
</StyledNoRecordGroupContainer>
);
};
@@ -60,7 +60,6 @@ export const RecordTableRecordGroupRows = () => {
recordId={recordId}
rowIndexForFocus={rowIndex}
rowIndexForDrag={rowIndexInGroup}
isFirstRowOfGroup={rowIndexInGroup === 0}
/>
);
})}
@@ -35,16 +35,17 @@ export const getRecordTableColumnWidthInlineStyles = (
visibleRecordFields: RecordField[],
): Record<string, string> => {
const style: Record<string, string> = {};
for (let i = 0; i < visibleRecordFields.length; i++) {
style[`--record-table-column-field-${i}`] =
`${visibleRecordFields[i].size}px`;
}
return style;
};
const StyledTable = styled.div<{
isDragging?: boolean;
hasRecordGroups: boolean;
}>`
& > * {
pointer-events: ${({ isDragging }) =>
@@ -52,23 +53,13 @@ const StyledTable = styled.div<{
}
display: flex;
flex-wrap: wrap;
flex-direction: column;
position: relative;
width: 100%;
div.header-cell {
position: sticky;
top: 0;
${VerticalScrollBoxShadowCSS}
}
div.header-cell:nth-of-type(n + 5) {
z-index: ${({ hasRecordGroups }) =>
hasRecordGroups
? TABLE_Z_INDEX.headerColumns.withGroups.headerColumnsNormal
: TABLE_Z_INDEX.headerColumns.withoutGroups.headerColumnsNormal};
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsNormal};
}
div.header-cell:nth-of-type(1) {
@@ -76,34 +67,30 @@ const StyledTable = styled.div<{
left: 0px;
z-index: ${({ hasRecordGroups }) =>
hasRecordGroups
? TABLE_Z_INDEX.headerColumns.withGroups.headerColumnsSticky
: TABLE_Z_INDEX.headerColumns.withoutGroups.headerColumnsSticky};
position: sticky;
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsSticky};
}
div.header-cell:nth-of-type(2) {
background-color: ${themeCssVariables.background.primary};
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
position: sticky;
top: 0;
z-index: ${({ hasRecordGroups }) =>
hasRecordGroups
? TABLE_Z_INDEX.headerColumns.withGroups.headerColumnsSticky
: TABLE_Z_INDEX.headerColumns.withoutGroups.headerColumnsSticky};
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsSticky};
}
div.header-cell:nth-of-type(3) {
background-color: ${themeCssVariables.background.primary};
left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
position: sticky;
right: 0;
z-index: ${({ hasRecordGroups }) =>
hasRecordGroups
? TABLE_Z_INDEX.headerColumns.withGroups.headerColumnsSticky
: TABLE_Z_INDEX.headerColumns.withoutGroups.headerColumnsSticky};
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsSticky};
${HorizontalScrollBoxShadowCSS}
}
@@ -111,19 +98,13 @@ const StyledTable = styled.div<{
div.table-cell:nth-of-type(1) {
left: 0px;
position: sticky;
z-index: ${({ hasRecordGroups }) =>
hasRecordGroups
? TABLE_Z_INDEX.cell.withGroups.sticky
: TABLE_Z_INDEX.cell.withoutGroups.sticky};
z-index: ${TABLE_Z_INDEX.cell.sticky};
}
div.table-cell:nth-of-type(2) {
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
position: sticky;
z-index: ${({ hasRecordGroups }) =>
hasRecordGroups
? TABLE_Z_INDEX.cell.withGroups.sticky
: TABLE_Z_INDEX.cell.withoutGroups.sticky};
z-index: ${TABLE_Z_INDEX.cell.sticky};
}
div.table-cell-0-0 {
@@ -136,10 +117,7 @@ const StyledTable = styled.div<{
div.table-cell:nth-of-type(3) {
left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
position: sticky;
z-index: ${({ hasRecordGroups }) =>
hasRecordGroups
? TABLE_Z_INDEX.cell.withGroups.sticky
: TABLE_Z_INDEX.cell.withoutGroups.sticky};
z-index: ${TABLE_Z_INDEX.cell.sticky};
${HorizontalScrollBoxShadowCSS}
}
@@ -1,63 +1,23 @@
export const TABLE_Z_INDEX = {
base: 1,
hoverPortal: 4,
headerRow: 10,
cell: {
withoutGroups: {
default: 3,
sticky: 12,
editMode: 30,
},
withGroups: {
default: 3,
sticky: 12,
editMode: 30,
},
default: 3,
sticky: 8,
editMode: 30,
},
headerColumns: {
withoutGroups: {
headerColumnsSticky: 14,
headerColumnsNormal: 10,
},
withGroups: {
headerColumnsSticky: 21,
headerColumnsNormal: 20,
},
headerColumnsSticky: 14,
headerColumnsNormal: 10,
},
groupSection: {
stickyCell: 10,
stickyCell: 9,
normalCell: 8,
},
columnGrip: 30,
footer: {
tableWithGroups: {
default: 17,
stickyColumn: 18,
},
tableWithoutGroups: {
default: 18,
stickyColumn: 20,
},
},
activeRows: {
firstRow: {
sticky: {
scrolledVertically: 10,
noVerticalScroll: 15,
},
normal: {
scrolledVertically: 8,
noVerticalScroll: 11,
},
},
afterFirstRow: {
sticky: {
scrolledVertically: 8,
noVerticalScroll: 8,
},
normal: {
scrolledVertically: 7,
noVerticalScroll: 7,
},
},
default: 8,
stickyColumn: 9,
},
};
@@ -37,7 +37,6 @@ export const RecordTableBodyLoading = () => {
isDragging={false}
data-testid={`row-id-${rowIndex}`}
data-selectable-id={`row-id-${rowIndex}`}
isFirstRowOfGroup={rowIndex === 0}
>
<RecordTableCellDragAndDrop />
<RecordTableCellCheckbox />
@@ -68,13 +68,13 @@ const StyledRowDraggableCloneCSSBridge = styled.div`
div.table-cell:nth-of-type(1) {
left: 0px;
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.withGroups.sticky};
z-index: ${TABLE_Z_INDEX.cell.sticky};
}
div.table-cell:nth-of-type(2) {
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.withoutGroups.sticky};
z-index: ${TABLE_Z_INDEX.cell.sticky};
}
div.table-cell-0-0 {
@@ -87,7 +87,7 @@ const StyledRowDraggableCloneCSSBridge = styled.div`
div.table-cell:nth-of-type(3) {
left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.withoutGroups.sticky};
z-index: ${TABLE_Z_INDEX.cell.sticky};
${HorizontalScrollBoxShadowCSS}
}
@@ -180,10 +180,8 @@ export const RecordTableBodyVirtualizedDraggableClone = ({
}}
isDragging={draggableSnapshot.isDragging}
data-testid={`row-id-${recordId}`}
data-virtualized-id={recordId}
data-selectable-id={recordId}
onClick={() => {}}
isFirstRowOfGroup={false}
>
<RecordTableRowDraggableContextProvider
value={{
@@ -22,10 +22,6 @@ const StyledContainer = styled.div`
width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH};
`;
const StyledRecordTableTdContainer = styled.div`
border-left: 1px solid transparent;
`;
export const RecordTableCellCheckbox = () => {
const { isSelected } = useRecordTableRowContextOrThrow();
@@ -42,16 +38,14 @@ export const RecordTableCellCheckbox = () => {
);
return (
<StyledRecordTableTdContainer>
<RecordTableCellStyleWrapper
isSelected={isSelected}
hasRightBorder={false}
widthClassName={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME}
>
<StyledContainer onClick={handleClick} data-select-disable>
<Checkbox hoverable checked={isSelected} />
</StyledContainer>
</RecordTableCellStyleWrapper>
</StyledRecordTableTdContainer>
<RecordTableCellStyleWrapper
isSelected={isSelected}
hasRightBorder={false}
widthClassName={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME}
>
<StyledContainer onClick={handleClick} data-select-disable>
<Checkbox hoverable checked={isSelected} />
</StyledContainer>
</RecordTableCellStyleWrapper>
);
};
@@ -1,8 +1,6 @@
import { RecordTableCellPortalWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
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';
@@ -22,10 +20,6 @@ export const RecordTableCellEditModePortal = () => {
recordTableCellEditModePositionComponentState,
);
const hasRecordGroups = useAtomComponentSelectorValue(
hasRecordGroupsComponentSelector,
);
const cellFocusId = useCurrentlyFocusedRecordTableCellFocusId();
if (!isDefined(recordTableFocusPosition) || !isDefined(cellFocusId)) {
@@ -36,11 +30,7 @@ export const RecordTableCellEditModePortal = () => {
<RecordTableCellPortalWrapper position={recordTableFocusPosition}>
{recordTableCellEditModePosition && (
<RecordTableCellPortalRootContainer
zIndex={
hasRecordGroups
? TABLE_Z_INDEX.cell.withGroups.editMode
: TABLE_Z_INDEX.cell.withoutGroups.editMode
}
zIndex={TABLE_Z_INDEX.cell.editMode}
>
<RecordTableCellEditMode>
<RecordTableCellFieldInput />
@@ -50,7 +50,7 @@ export const RecordTableCellFirstRowFirstColumn = ({
} & (Partial<DraggableProvidedDragHandleProps> | null)) => {
const { theme } = useContext(ThemeContext);
const zIndex = TABLE_Z_INDEX.cell.withoutGroups.sticky;
const zIndex = TABLE_Z_INDEX.cell.sticky;
const tdBackgroundColor = isSelected
? theme.accent.quaternary
@@ -8,47 +8,38 @@ import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZInde
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell';
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledPlaceholderDragAndDropFooterCell = styled.div<{
isTableWithGroups: boolean;
}>`
const StyledPlaceholderDragAndDropFooterCell = styled.div`
background-color: ${themeCssVariables.background.primary};
bottom: 0;
left: 0px;
position: sticky;
width: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
z-index: ${({ isTableWithGroups }) =>
isTableWithGroups
? TABLE_Z_INDEX.footer.tableWithGroups.stickyColumn
: TABLE_Z_INDEX.footer.tableWithoutGroups.stickyColumn};
z-index: ${TABLE_Z_INDEX.footer.stickyColumn};
`;
const StyledPlaceholderAddButtonPlaceholderFooterCell = styled.div<{
isTableWithGroups: boolean;
}>`
const StyledPlaceholderAddButtonPlaceholderFooterCell = styled.div`
background-color: ${themeCssVariables.background.primary};
bottom: 0;
position: sticky;
width: ${RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH}px;
z-index: ${({ isTableWithGroups }) =>
isTableWithGroups
? TABLE_Z_INDEX.footer.tableWithGroups.default
: TABLE_Z_INDEX.footer.tableWithoutGroups.default};
z-index: ${TABLE_Z_INDEX.footer.default};
`;
const StyledPlaceholderLastColumnEmptyFooterCell = styled.div<{
isTableWithGroups: boolean;
}>`
const StyledPlaceholderLastColumnEmptyFooterCell = styled.div`
background-color: ${themeCssVariables.background.primary};
bottom: 0;
position: sticky;
z-index: ${({ isTableWithGroups }) =>
isTableWithGroups
? TABLE_Z_INDEX.footer.tableWithGroups.default
: TABLE_Z_INDEX.footer.tableWithoutGroups.default};
z-index: ${TABLE_Z_INDEX.footer.default};
`;
const StyledAggregateFooterContainer = styled.div`
bottom: 0;
display: flex;
position: sticky;
z-index: ${TABLE_Z_INDEX.footer.default};
`;
export const RecordTableAggregateFooter = ({
@@ -58,13 +49,9 @@ export const RecordTableAggregateFooter = ({
}) => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const isTableWithGroups = isDefined(currentRecordGroupId);
return (
<>
<StyledPlaceholderDragAndDropFooterCell
isTableWithGroups={isTableWithGroups}
/>
<StyledAggregateFooterContainer>
<StyledPlaceholderDragAndDropFooterCell />
{visibleRecordFields.map((recordField, index) => {
return (
<RecordTableColumnAggregateFooterCellContext.Provider
@@ -81,13 +68,10 @@ export const RecordTableAggregateFooter = ({
</RecordTableColumnAggregateFooterCellContext.Provider>
);
})}
<StyledPlaceholderAddButtonPlaceholderFooterCell
isTableWithGroups={isTableWithGroups}
/>
<StyledPlaceholderAddButtonPlaceholderFooterCell />
<StyledPlaceholderLastColumnEmptyFooterCell
isTableWithGroups={isTableWithGroups}
className={RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME}
/>
</>
</StyledAggregateFooterContainer>
);
};
@@ -1,6 +1,6 @@
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { themeCssVariables, MOBILE_VIEWPORT } from 'twenty-ui/theme-constants';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
@@ -17,7 +17,6 @@ import { findByProperty, isDefined } from 'twenty-shared/utils';
const StyledColumnFooterCell = styled.div<{
columnWidth: number;
isFirstCell: boolean;
isTableWithGroups: boolean;
}>`
background-color: ${themeCssVariables.background.primary};
border-right: solid 1px ${themeCssVariables.background.primary};
@@ -31,6 +30,7 @@ const StyledColumnFooterCell = styled.div<{
isFirstCell
? `${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`
: 'auto'};
min-width: ${({ columnWidth }) => columnWidth}px;
&:hover {
background: ${themeCssVariables.background.secondary};
@@ -43,14 +43,11 @@ const StyledColumnFooterCell = styled.div<{
text-align: left;
width: ${({ columnWidth }) => columnWidth}px;
z-index: ${({ isFirstCell, isTableWithGroups }) =>
z-index: ${({ isFirstCell }) =>
isFirstCell
? isTableWithGroups
? TABLE_Z_INDEX.footer.tableWithGroups.stickyColumn
: TABLE_Z_INDEX.footer.tableWithoutGroups.stickyColumn
: isTableWithGroups
? TABLE_Z_INDEX.footer.tableWithGroups.default
: TABLE_Z_INDEX.footer.tableWithoutGroups.default};
? TABLE_Z_INDEX.footer.stickyColumn
: TABLE_Z_INDEX.footer.default};
@media (max-width: ${MOBILE_VIEWPORT}px) {
max-width: ${({ isFirstCell }) =>
@@ -91,8 +88,6 @@ export const RecordTableAggregateFooterCell = ({
findByProperty('fieldMetadataItemId', fieldMetadataId),
);
const isTableWithGroups = isDefined(currentRecordGroupId);
const isFirstCell = columnIndex === 0;
if (!isDefined(recordField)) {
@@ -107,7 +102,6 @@ export const RecordTableAggregateFooterCell = ({
'footer-cell',
getRecordTableColumnFieldWidthClassName(columnIndex),
)}
isTableWithGroups={isTableWithGroups}
>
<StyledColumnFootContainer>
<RecordTableColumnFooterWithDropdown
@@ -1,4 +1,5 @@
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 { RecordTableHeaderAddColumnButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton';
import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell';
@@ -8,8 +9,17 @@ import { RecordTableHeaderFirstCell } from '@/object-record/record-table/record-
import { RecordTableHeaderFirstScrollableCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell';
import { RecordTableHeaderLastEmptyColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastEmptyColumn';
import { useResizeTableHeader } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader';
import { styled } from '@linaria/react';
import { filterOutByProperty } from 'twenty-shared/utils';
const StyledHeaderContainer = styled.div`
display: flex;
flex-direction: row;
position: sticky;
top: 0;
z-index: ${TABLE_Z_INDEX.headerRow};
`;
export const RecordTableHeader = () => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
@@ -26,7 +36,7 @@ export const RecordTableHeader = () => {
useResizeTableHeader();
return (
<>
<StyledHeaderContainer>
<RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn />
<RecordTableHeaderFirstCell />
@@ -42,6 +52,6 @@ export const RecordTableHeader = () => {
)}
<RecordTableHeaderAddColumnButton />
<RecordTableHeaderLastEmptyColumn />
</>
</StyledHeaderContainer>
);
};
@@ -16,8 +16,8 @@ import { isRecordTableRowFocusActiveComponentState } from '@/object-record/recor
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { cx } from '@linaria/core';
import { Checkbox } from 'twenty-ui/input';
@@ -27,7 +27,7 @@ const StyledPlusButtonWrapper = styled.div`
position: absolute;
right: 0;
top: 0;
z-index: ${TABLE_Z_INDEX.headerColumns.withoutGroups.headerColumnsSticky};
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsSticky};
`;
export const RecordTableHeaderFirstCell = () => {
@@ -59,10 +59,6 @@ export const RecordTableHeaderFirstScrollableCell = () => {
hasRecordGroupsComponentSelector,
);
const zIndex = hasRecordGroups
? TABLE_Z_INDEX.headerColumns.withGroups.headerColumnsNormal
: TABLE_Z_INDEX.headerColumns.withoutGroups.headerColumnsNormal;
const shouldDisplayBorderBottom =
hasRecordGroups ||
!isFirstRowActiveOrFocused ||
@@ -83,7 +79,7 @@ export const RecordTableHeaderFirstScrollableCell = () => {
className={cx('header-cell', getRecordTableColumnFieldWidthClassName(1))}
key={recordField.fieldMetadataItemId}
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
zIndex={zIndex}
zIndex={TABLE_Z_INDEX.headerColumns.headerColumnsNormal}
isResizing={isResizingAnyColumn}
>
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="left" />
@@ -58,10 +58,8 @@ export const RecordTableDraggableTr = ({
}}
isDragging={draggableSnapshot.isDragging}
data-testid={`row-id-${recordId}`}
data-virtualized-id={recordId}
data-selectable-id={recordId}
onClick={onClick}
isFirstRowOfGroup={false}
>
<RecordTableRowDraggableContextProvider
value={{
@@ -1,87 +0,0 @@
import { Draggable } from '@hello-pangea/dnd';
import { type ReactNode, useContext } from 'react';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
import { useIsTableRowSecondaryDragged } from '@/object-record/record-table/record-table-row/hooks/useIsRecordSecondaryDragged';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
type RecordTableDraggableTrFirstRowOfGroupProps = {
className?: string;
recordId: string;
draggableIndex: number;
focusIndex: number;
isDragDisabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLTableRowElement>) => void;
children: ReactNode;
};
export const RecordTableDraggableTrFirstRowOfGroup = ({
className,
recordId,
draggableIndex,
focusIndex,
isDragDisabled,
onClick,
children,
}: RecordTableDraggableTrFirstRowOfGroupProps) => {
const { theme } = useContext(ThemeContext);
const { isSecondaryDragged } = useIsTableRowSecondaryDragged(recordId);
const isRecordTableScrolledVertically = useAtomComponentStateValue(
isRecordTableScrolledVerticallyComponentState,
);
return (
<Draggable
draggableId={recordId}
index={draggableIndex}
isDragDisabled={isDragDisabled}
>
{(draggableProvided, draggableSnapshot) => (
<>
<RecordTableTr
recordId={recordId}
focusIndex={focusIndex}
ref={draggableProvided.innerRef}
className={className}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
style={{
...draggableProvided.draggableProps.style,
background: draggableSnapshot.isDragging
? theme.background.transparent.light
: undefined,
borderColor: draggableSnapshot.isDragging
? `${theme.border.color.medium}`
: 'transparent',
opacity: isSecondaryDragged ? 0.3 : undefined,
}}
isDragging={draggableSnapshot.isDragging}
data-testid={`row-id-${recordId}`}
data-virtualized-id={recordId}
data-selectable-id={recordId}
onClick={onClick}
isFirstRowOfGroup={true}
isScrolledVertically={isRecordTableScrolledVertically}
>
<RecordTableRowDraggableContextProvider
value={{
isDragging: draggableSnapshot.isDragging,
dragHandleProps: draggableProvided.dragHandleProps,
}}
>
{children}
<RecordTableRowMultiDragPreview />
</RecordTableRowDraggableContextProvider>
</RecordTableTr>
</>
)}
</Draggable>
);
};
@@ -1,57 +1,26 @@
import { RecordTableRowDiv } from '@/object-record/record-table/record-table-row/components/RecordTableRowDiv';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { forwardRef, type ReactNode } from 'react';
type RecordTableTrProps = {
children: ReactNode;
recordId: string;
focusIndex: number;
isDragging?: boolean;
isRowFocusActive: boolean;
isFocused: boolean;
isNextRowActiveOrFocused: boolean;
} & Omit<
React.ComponentProps<typeof RecordTableRowDiv>,
'isActive' | 'isNextRowActiveOrFocused' | 'isFocused'
>;
};
export const RecordTableFirstRowOfGroup = forwardRef<
HTMLDivElement,
RecordTableTrProps
>(
(
{
children,
recordId,
isNextRowActiveOrFocused,
isDragging = false,
isRowFocusActive,
isFocused,
...props
},
ref,
) => {
const isRecordTableScrolledVertically = useAtomComponentStateValue(
isRecordTableScrolledVerticallyComponentState,
);
return (
<RecordTableRowDiv
className="table-row"
data-virtualized-id={recordId}
isDragging={isDragging}
ref={ref}
data-active={false}
data-focused={isRowFocusActive && isFocused}
data-next-row-active-or-focused={isNextRowActiveOrFocused}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...props}
isScrolledVertically={isRecordTableScrolledVertically}
isFirstRowOfGroup={true}
>
{children}
</RecordTableRowDiv>
);
},
);
>(({ children, recordId, isDragging = false, ...props }, ref) => {
return (
<RecordTableRowDiv
className="table-row"
data-virtualized-id={recordId}
isDragging={isDragging}
ref={ref}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</RecordTableRowDiv>
);
});
@@ -3,12 +3,11 @@ import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { RecordTableDraggableTrFirstRowOfGroup } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTrFirstRowOfGroup';
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
@@ -18,14 +17,12 @@ type RecordTableRowProps = {
recordId: string;
rowIndexForFocus: number;
rowIndexForDrag: number;
isFirstRowOfGroup: boolean;
};
export const RecordTableRow = ({
recordId,
rowIndexForFocus,
rowIndexForDrag,
isFirstRowOfGroup,
}: RecordTableRowProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
@@ -33,30 +30,13 @@ export const RecordTableRow = ({
isRecordTableRowFocusedComponentFamilyState,
rowIndexForFocus,
);
const isRecordTableRowFocusActive = useAtomComponentStateValue(
isRecordTableRowFocusActiveComponentState,
recordTableId,
);
return isFirstRowOfGroup ? (
<RecordTableDraggableTrFirstRowOfGroup
recordId={recordId}
draggableIndex={rowIndexForDrag}
focusIndex={rowIndexForFocus}
>
{isRecordTableRowFocusActive && isRecordTableRowFocused && (
<>
<RecordTableRowHotkeyEffect />
<RecordTableRowArrowKeysEffect />
</>
)}
<RecordTableCellDragAndDrop />
<RecordTableCellCheckbox />
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
</RecordTableDraggableTrFirstRowOfGroup>
) : (
return (
<RecordTableDraggableTr
recordId={recordId}
draggableIndex={rowIndexForDrag}
@@ -1,36 +1,9 @@
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledTr = styled.div<{
isDragging: boolean;
isFirstRowOfGroup?: boolean;
isScrolledVertically?: boolean;
}>`
--z-index-for-normal-cells: ${({
isFirstRowOfGroup,
isScrolledVertically,
}) =>
isFirstRowOfGroup === true
? isScrolledVertically
? TABLE_Z_INDEX.activeRows.firstRow.normal.scrolledVertically
: TABLE_Z_INDEX.activeRows.firstRow.normal.noVerticalScroll
: isScrolledVertically
? TABLE_Z_INDEX.activeRows.afterFirstRow.normal.scrolledVertically
: TABLE_Z_INDEX.activeRows.afterFirstRow.normal.noVerticalScroll};
--z-index-for-sticky-cells: ${({
isFirstRowOfGroup,
isScrolledVertically,
}) =>
isFirstRowOfGroup === true
? isScrolledVertically
? TABLE_Z_INDEX.activeRows.firstRow.sticky.scrolledVertically
: TABLE_Z_INDEX.activeRows.firstRow.sticky.noVerticalScroll
: isScrolledVertically
? TABLE_Z_INDEX.activeRows.afterFirstRow.sticky.scrolledVertically
: TABLE_Z_INDEX.activeRows.afterFirstRow.sticky.noVerticalScroll};
border-top: ${({ isDragging }) =>
isDragging ? `1px solid ${themeCssVariables.border.color.medium}` : 'none'};
@@ -2,42 +2,28 @@ import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShow
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableFirstRowOfGroup } from '@/object-record/record-table/record-table-row/components/RecordTableFirstRowOfGroup';
import { RecordTableRowDiv } from '@/object-record/record-table/record-table-row/components/RecordTableRowDiv';
import { isRecordIdFirstOfGroupComponentFamilySelector } from '@/object-record/record-table/record-table-row/states/isRecordIdFirstOfGroupComponentFamilySelector';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { recordIdByRealIndexComponentFamilySelector } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilySelector';
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { forwardRef, type ReactNode } from 'react';
type RecordTableTrProps = {
children: ReactNode;
recordId: string;
focusIndex: number;
isDragging?: boolean;
isFirstRowOfGroup?: boolean;
} & Omit<
React.ComponentProps<typeof RecordTableRowDiv>,
'isActive' | 'isNextRowActiveOrFocused' | 'isFocused'
>;
export const RecordTableTr = forwardRef<HTMLDivElement, RecordTableTrProps>(
(
{
children,
recordId,
focusIndex,
isDragging = false,
isFirstRowOfGroup,
...props
},
ref,
) => {
({ children, recordId, focusIndex, isDragging = false, ...props }, ref) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isRowSelected = useAtomComponentFamilyStateValue(
@@ -50,22 +36,6 @@ export const RecordTableTr = forwardRef<HTMLDivElement, RecordTableTrProps>(
focusIndex,
);
// oxlint-disable-next-line twenty/matching-state-variable
const isNextRecordTableRowActive = useAtomComponentFamilyStateValue(
isRecordTableRowActiveComponentFamilyState,
focusIndex + 1,
);
const nextRecordId = useAtomComponentFamilySelectorValue(
recordIdByRealIndexComponentFamilySelector,
focusIndex + 1,
);
const isNextRecordIdFirstOfGroup = useAtomComponentFamilySelectorValue(
isRecordIdFirstOfGroupComponentFamilySelector,
nextRecordId ?? '',
);
const isRecordTableRowFocused = useAtomComponentFamilyStateValue(
isRecordTableRowFocusedComponentFamilyState,
focusIndex,
@@ -75,17 +45,6 @@ export const RecordTableTr = forwardRef<HTMLDivElement, RecordTableTrProps>(
isRecordTableRowFocusActiveComponentState,
);
// oxlint-disable-next-line twenty/matching-state-variable
const isNextRecordTableRowFocused = useAtomComponentFamilyStateValue(
isRecordTableRowFocusedComponentFamilyState,
focusIndex + 1,
);
const isNextRowActiveOrFocused =
!isNextRecordIdFirstOfGroup &&
((isRecordTableRowFocusActive && isNextRecordTableRowFocused) ||
isNextRecordTableRowActive);
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
objectMetadataId: objectMetadataItem.id,
@@ -105,48 +64,21 @@ export const RecordTableTr = forwardRef<HTMLDivElement, RecordTableTrProps>(
isRecordReadOnly,
}}
>
{isFirstRowOfGroup ? (
<RecordTableFirstRowOfGroup
className="table-row"
data-virtualized-id={recordId}
isDragging={isDragging}
ref={ref}
data-active={isRecordTableRowActive}
data-focused={
isRecordTableRowFocusActive &&
isRecordTableRowFocused &&
!isRecordTableRowActive
}
data-next-row-active-or-focused={isNextRowActiveOrFocused}
isNextRowActiveOrFocused={isNextRowActiveOrFocused}
focusIndex={focusIndex}
isFocused={isRecordTableRowFocused}
isRowFocusActive={isRecordTableRowFocusActive}
recordId={recordId}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</RecordTableFirstRowOfGroup>
) : (
<RecordTableRowDiv
className="table-row"
data-virtualized-id={recordId}
isDragging={isDragging}
ref={ref}
data-active={isRecordTableRowActive}
data-focused={
isRecordTableRowFocusActive &&
isRecordTableRowFocused &&
!isRecordTableRowActive
}
data-next-row-active-or-focused={isNextRowActiveOrFocused}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</RecordTableRowDiv>
)}
<RecordTableRowDiv
className="table-row"
isDragging={isDragging}
ref={ref}
data-active={isRecordTableRowActive}
data-focused={
isRecordTableRowFocusActive &&
isRecordTableRowFocused &&
!isRecordTableRowActive
}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</RecordTableRowDiv>
</RecordTableRowContextProvider>
);
},
@@ -44,7 +44,7 @@ const StyledTrContainer = styled.div`
display: flex;
flex-direction: row;
div:not(:first-of-type) {
> div:not(:first-of-type) {
border-bottom: 1px solid ${themeCssVariables.border.color.light};
}
`;
@@ -18,7 +18,7 @@ export const computeLastRecordTableColumnWidth = ({
visibleRecordFields: recordFields,
});
const widthOfBorders = recordFields.length;
const widthOfBorders = recordFields.length + 1;
const fixedColumnsWidth =
RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH +
@@ -1,3 +1,4 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
@@ -6,7 +7,6 @@ import { RecordTableDraggableTr } from '@/object-record/record-table/record-tabl
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { RecordTableRowVirtualizedSkeleton } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedSkeleton';
@@ -52,17 +52,17 @@ export const RecordTableRowVirtualizedFullData = ({
draggableIndex={realIndex}
focusIndex={realIndex}
>
<RecordTableCellDragAndDrop />
<RecordTableCellCheckbox />
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
{isRecordTableRowFocusActive && isRecordTableRowFocused && (
<>
<RecordTableRowHotkeyEffect />
<RecordTableRowArrowKeysEffect />
</>
)}
<RecordTableCellDragAndDrop />
<RecordTableCellCheckbox />
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
</RecordTableDraggableTr>
);
};