From 9dda6a8fa117efd6b690925da01b90a74c163184 Mon Sep 17 00:00:00 2001 From: Kanav Arora Date: Mon, 25 Mar 2024 23:35:56 +0530 Subject: [PATCH] 4162-Sticky-Header (#4627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial commit * functionality added * Suggested changes fixed * Fix broken shadow * Unrelated fix (input stuck under container) * Performance improvement --------- Co-authored-by: Félix Malfait --- .../record-table/components/RecordTable.tsx | 24 ++- .../components/RecordTableCellContainer.tsx | 70 +++------ .../components/RecordTableHeader.tsx | 3 +- .../components/RecordTableCellContainer.tsx | 140 ++++++++++++------ 4 files changed, 134 insertions(+), 103 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 80dbedafd..019a8c150 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -12,8 +12,12 @@ import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTabl import { MOBILE_VIEWPORT } from '@/ui/theme/constants/MobileViewport'; import { RGBA } from '@/ui/theme/constants/Rgba'; import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState'; +import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState'; -const StyledTable = styled.table<{ freezeFirstColumns?: boolean }>` +const StyledTable = styled.table<{ + freezeFirstColumns?: boolean; + freezeHeaders?: boolean; +}>` border-radius: ${({ theme }) => theme.border.radius.sm}; border-spacing: 0; margin-right: ${({ theme }) => theme.table.horizontalCellMargin}; @@ -65,12 +69,24 @@ const StyledTable = styled.table<{ freezeFirstColumns?: boolean }>` border-right: none; } - thead th:nth-of-type(1), tbody td:nth-of-type(1) { left: 0; } // Label identifier column + thead th:nth-of-type(1), + thead th:nth-of-type(2) { + left: 0; + top: 0; + z-index: 6; + } + + thead th:nth-child(n + 3) { + top: 0; + z-index: 5; + position: sticky; + } + thead th:nth-of-type(2), tbody td:nth-of-type(2) { left: calc(${({ theme }) => theme.table.checkboxColumnWidth} - 2px); @@ -88,9 +104,9 @@ const StyledTable = styled.table<{ freezeFirstColumns?: boolean }>` content: ''; height: calc(100% + 1px); position: absolute; - top: 0; width: 4px; right: -4px; + top: 0; ${({ freezeFirstColumns, theme }) => freezeFirstColumns && @@ -123,6 +139,7 @@ export const RecordTable = ({ }: RecordTableProps) => { const { scopeId } = useRecordTableStates(recordTableId); const scrollLeft = useRecoilValue(scrollLeftState); + const scrollTop = useRecoilValue(scrollTopState); const { objectMetadataItem } = useObjectMetadataItemOnly({ objectNameSingular, @@ -141,6 +158,7 @@ export const RecordTable = ({ > 0} + freezeHeaders={scrollTop > 0} className="entity-table-cell" > diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx index a879ed19a..407a58a13 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx @@ -1,6 +1,4 @@ import { useContext } from 'react'; -import styled from '@emotion/styled'; -import { useSetRecoilState } from 'recoil'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; @@ -10,39 +8,14 @@ import { RecordTableCellContext } from '@/object-record/record-table/contexts/Re import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell'; -import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; -import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState'; -import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -const StyledContainer = styled.td<{ isSelected: boolean }>` - background: ${({ isSelected, theme }) => - isSelected ? theme.accent.quaternary : theme.background.primary}; -`; - export const RecordTableCellContainer = () => { - const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); - const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); - - const { setCurrentRowSelected } = useSetCurrentRowSelected(); - - const handleContextMenu = (event: React.MouseEvent) => { - event.preventDefault(); - setCurrentRowSelected(true); - setContextMenuPosition({ - x: event.clientX, - y: event.clientY, - }); - setContextMenuOpenState(true); - }; - const { objectMetadataItem } = useContext(RecordTableContext); const { columnDefinition } = useContext(RecordTableCellContext); - const { recordId, pathToShowPage, isSelected } = useContext( - RecordTableRowContext, - ); + const { recordId, pathToShowPage } = useContext(RecordTableRowContext); const updateRecord = useContext(RecordUpdateContext); @@ -55,29 +28,24 @@ export const RecordTableCellContainer = () => { : TableHotkeyScope.CellEditMode; return ( - handleContextMenu(event)} + [updateRecord, {}], + hotkeyScope: customHotkeyScope, + basePathToShowPage: pathToShowPage, + isLabelIdentifier: isLabelIdentifierField({ + fieldMetadataItem: { + id: columnDefinition.fieldMetadataId, + name: columnDefinition.metadata.fieldName, + }, + objectMetadataItem, + }), + }} > - [updateRecord, {}], - hotkeyScope: customHotkeyScope, - basePathToShowPage: pathToShowPage, - isLabelIdentifier: isLabelIdentifierField({ - fieldMetadataItem: { - id: columnDefinition.fieldMetadataId, - name: columnDefinition.metadata.fieldName, - }, - objectMetadataItem, - }), - }} - > - - - + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx index 90611c0eb..452d707f0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx @@ -28,8 +28,7 @@ const StyledPlusIconHeaderCell = styled.th<{ isTableWiderThanScreen: boolean }>` min-width: 32px; ${({ isTableWiderThanScreen, theme }) => isTableWiderThanScreen && - `position: relative; - right: 0; + ` width: 32px; border-right: none !important; background-color: ${theme.background.primary}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx index 344988c94..7250ea9fe 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx @@ -1,16 +1,21 @@ import { ReactElement, useContext, useState } from 'react'; import styled from '@emotion/styled'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon'; import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; +import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useGetIsSomeCellInEditModeState } from '@/object-record/record-table/hooks/internal/useGetIsSomeCellInEditMode'; import { useOpenRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell'; +import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected'; import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState'; import { IconArrowUpRight } from '@/ui/display/icon'; +import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState'; +import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext'; import { TableHotkeyScope } from '../../types/TableHotkeyScope'; @@ -24,6 +29,12 @@ import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode'; import { RecordTableCellEditMode } from './RecordTableCellEditMode'; import { RecordTableCellSoftFocusMode } from './RecordTableCellSoftFocusMode'; +const StyledTd = styled.td<{ isSelected: boolean; isInEditMode: boolean }>` + background: ${({ isSelected, theme }) => + isSelected ? theme.accent.quaternary : theme.background.primary}; + z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : '3')}; +`; + const StyledCellBaseContainer = styled.div` align-items: center; box-sizing: border-box; @@ -61,7 +72,6 @@ export const RecordTableCellContainer = ({ const { isCurrentTableCellInEditMode } = useCurrentTableCellEditMode(); const isSomeCellInEditModeState = useGetIsSomeCellInEditModeState(); - const isSomeCellInEditMode = useRecoilValue(isSomeCellInEditModeState()); const setIsSoftFocusUsingMouseState = useSetRecoilState( isSoftFocusUsingMouseState, @@ -80,14 +90,44 @@ export const RecordTableCellContainer = ({ openTableCell(); }; - const handleContainerMouseEnter = () => { - if (!isHovered && !isSomeCellInEditMode) { - setIsHovered(true); - moveSoftFocusToCurrentCellOnHover(); - setIsSoftFocusUsingMouseState(true); - } + const { isSelected } = useContext(RecordTableRowContext); + + const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); + const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); + + const { setCurrentRowSelected } = useSetCurrentRowSelected(); + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + setCurrentRowSelected(true); + setContextMenuPosition({ + x: event.clientX, + y: event.clientY, + }); + setContextMenuOpenState(true); }; + const handleContainerMouseEnter = useRecoilCallback( + ({ snapshot }) => + () => { + const isSomeCellInEditMode = getSnapshotValue( + snapshot, + isSomeCellInEditModeState(), + ); + if (!isHovered && !isSomeCellInEditMode) { + setIsHovered(true); + moveSoftFocusToCurrentCellOnHover(); + setIsSoftFocusUsingMouseState(true); + } + }, + [ + isHovered, + isSomeCellInEditModeState, + moveSoftFocusToCurrentCellOnHover, + setIsSoftFocusUsingMouseState, + ], + ); + const handleContainerMouseLeave = () => { setIsHovered(false); }; @@ -109,46 +149,52 @@ export const RecordTableCellContainer = ({ (!isFirstColumn || !isEmpty); return ( - handleContextMenu(event)} + isInEditMode={isCurrentTableCellInEditMode} > - - {isCurrentTableCellInEditMode ? ( - - {editModeContent} - - ) : hasSoftFocus ? ( - <> - {showButton && ( - - )} - - {editModeContentOnly ? editModeContent : nonEditModeContent} - - - ) : ( - <> - {showButton && ( - - )} - - {editModeContentOnly ? editModeContent : nonEditModeContent} - - - )} - - + + {isCurrentTableCellInEditMode ? ( + + {editModeContent} + + ) : hasSoftFocus ? ( + <> + {showButton && ( + + )} + + {editModeContentOnly ? editModeContent : nonEditModeContent} + + + ) : ( + <> + {showButton && ( + + )} + + {editModeContentOnly ? editModeContent : nonEditModeContent} + + + )} + + + ); };