From ed56a68b7c19713b3a60559fe7987fc75650ab4a Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:36:14 +0100 Subject: [PATCH] Improve aggregate footer cell display (#9124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémy Magrin Co-authored-by: Charles Bochet --- ...ardColumnHeaderAggregateDropdownButton.tsx | 6 +- ...useAggregateRecordsForRecordBoardColumn.ts | 4 +- .../computeAggregateValueAndLabel.test.ts | 82 ++++++++++++++++++- .../utils/computeAggregateValueAndLabel.ts | 34 ++++++-- ...ecordIndexRecordGroupHideComponentState.ts | 2 +- .../record-table/components/RecordTable.tsx | 6 +- .../components/RecordTableRecordGroupRows.tsx | 11 --- .../components/RecordTableBodyDroppable.tsx | 3 +- .../RecordTableRecordGroupsBody.tsx | 45 ++++++---- ...ter.tsx => RecordTableAggregateFooter.tsx} | 42 ++++------ ...tsx => RecordTableAggregateFooterCell.tsx} | 4 +- ...ordTableColumnAggregateFooterDropdown.tsx} | 9 +- ...RecordTableColumnAggregateFooterValue.tsx} | 51 +++++++----- ...ableColumnAggregateFooterWithDropdown.tsx} | 8 +- ...egateRecordsForRecordTableColumnFooter.tsx | 11 +-- .../components/RecordTableHeader.tsx | 4 +- .../RecordTableRecordGroupSection.tsx | 2 + .../RecordTableRecordGroupStickyEffect.tsx | 48 +++++++++++ .../scroll/contexts/ScrollWrapperContexts.tsx | 5 ++ ...ct-records-to-graphql-connection.helper.ts | 5 +- 20 files changed, 268 insertions(+), 114 deletions(-) rename packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/{RecordTableFooter.tsx => RecordTableAggregateFooter.tsx} (69%) rename packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/{RecordTableFooterCell.tsx => RecordTableAggregateFooterCell.tsx} (97%) rename packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/{RecordTableColumnFooterDropdown.tsx => RecordTableColumnAggregateFooterDropdown.tsx} (95%) rename packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/{RecordTableColumnFooterAggregateValue.tsx => RecordTableColumnAggregateFooterValue.tsx} (68%) rename packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/{RecordTableColumnFooterWithDropdown.tsx => RecordTableColumnAggregateFooterWithDropdown.tsx} (86%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx index dd12c11f72..5b945ba8b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx @@ -1,7 +1,6 @@ import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import styled from '@emotion/styled'; import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui'; -import { formatNumber } from '~/utils/format/number'; const StyledButton = styled(StyledHeaderDropdownButton)` padding: 0; @@ -19,10 +18,7 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({ return (
- + { skip: !isAggregateQueryEnabled, }); - const { value, label } = computeAggregateValueAndLabel({ + const { value, labelWithFieldName } = computeAggregateValueAndLabel({ data, objectMetadataItem, fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId, @@ -84,6 +84,6 @@ export const useAggregateRecordsForRecordBoardColumn = () => { return { aggregateValue: isAggregateQueryEnabled ? value : recordCount, - aggregateLabel: isDefined(value) ? label : undefined, + aggregateLabel: isDefined(value) ? labelWithFieldName : undefined, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts index 4440124861..5a03afa7b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts @@ -47,8 +47,81 @@ describe('computeAggregateValueAndLabel', () => { }); expect(result).toEqual({ - value: 2, - label: 'Sum of amount', + value: '2', + label: 'Sum', + labelWithFieldName: 'Sum of amount', + }); + }); + + it('should handle number field as percentage', () => { + const mockObjectMetadataWithPercentageField: ObjectMetadataItem = { + id: '123', + fields: [ + { + id: MOCK_FIELD_ID, + name: 'percentage', + type: FieldMetadataType.Number, + settings: { + type: 'percentage', + }, + } as FieldMetadataItem, + ], + } as ObjectMetadataItem; + + const mockData = { + percentage: { + [AGGREGATE_OPERATIONS.avg]: 0.3, + }, + }; + + const result = computeAggregateValueAndLabel({ + data: mockData, + objectMetadataItem: mockObjectMetadataWithPercentageField, + fieldMetadataId: MOCK_FIELD_ID, + aggregateOperation: AGGREGATE_OPERATIONS.avg, + fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + }); + + expect(result).toEqual({ + value: '30%', + label: 'Average', + labelWithFieldName: 'Average of percentage', + }); + }); + + it('should handle number field with decimals', () => { + const mockObjectMetadataWithDecimalsField: ObjectMetadataItem = { + id: '123', + fields: [ + { + id: MOCK_FIELD_ID, + name: 'decimals', + type: FieldMetadataType.Number, + settings: { + decimals: 2, + }, + } as FieldMetadataItem, + ], + } as ObjectMetadataItem; + + const mockData = { + decimals: { + [AGGREGATE_OPERATIONS.sum]: 0.009, + }, + }; + + const result = computeAggregateValueAndLabel({ + data: mockData, + objectMetadataItem: mockObjectMetadataWithDecimalsField, + fieldMetadataId: MOCK_FIELD_ID, + aggregateOperation: AGGREGATE_OPERATIONS.sum, + fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + }); + + expect(result).toEqual({ + value: '0.01', + label: 'Sum', + labelWithFieldName: 'Sum of decimals', }); }); @@ -86,8 +159,9 @@ describe('computeAggregateValueAndLabel', () => { }); expect(result).toEqual({ - value: undefined, - label: 'Sum of amount', + value: '-', + label: 'Sum', + labelWithFieldName: 'Sum of amount', }); }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts index b30c673c70..80beb0e39d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts @@ -4,6 +4,8 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record- import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import isEmpty from 'lodash.isempty'; import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { formatAmount } from '~/utils/format/formatAmount'; +import { formatNumber } from '~/utils/format/number'; import { isDefined } from '~/utils/isDefined'; export const computeAggregateValueAndLabel = ({ @@ -42,12 +44,33 @@ export const computeAggregateValueAndLabel = ({ const aggregateValue = data[field.name]?.[aggregateOperation]; - const value = - isDefined(aggregateValue) && field.type === FieldMetadataType.Currency - ? Number(aggregateValue) / 1_000_000 - : aggregateValue; + let value; - const label = + if (aggregateOperation === AGGREGATE_OPERATIONS.count) { + value = aggregateValue; + } else if (!isDefined(aggregateValue)) { + value = '-'; + } else { + value = Number(aggregateValue); + + switch (field.type) { + case FieldMetadataType.Currency: { + value = formatAmount(value / 1_000_000); + break; + } + + case FieldMetadataType.Number: { + const { decimals, type } = field.settings ?? {}; + value = + type === 'percentage' + ? `${formatNumber(value * 100, decimals)}%` + : formatNumber(value, decimals); + break; + } + } + } + const label = getAggregateOperationLabel(aggregateOperation); + const labelWithFieldName = aggregateOperation === AGGREGATE_OPERATIONS.count ? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}` : `${getAggregateOperationLabel(aggregateOperation)} of ${field.name}`; @@ -55,5 +78,6 @@ export const computeAggregateValueAndLabel = ({ return { value, label, + labelWithFieldName, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts index bc571f6755..3500e331e5 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts @@ -4,6 +4,6 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon export const recordIndexRecordGroupHideComponentState = createComponentStateV2({ key: 'recordIndexRecordGroupHideComponentState', - defaultValue: true, + defaultValue: false, componentInstanceContext: ViewComponentInstanceContext, }); 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 72d874499a..853a3623dc 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 @@ -13,7 +13,7 @@ import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/recor import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect'; import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects'; import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody'; -import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter'; +import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; @@ -88,7 +88,7 @@ export const RecordTable = () => { ) : ( <> - + {!hasRecordGroups ? ( @@ -97,7 +97,7 @@ export const RecordTable = () => { )} {isAggregateQueryEnabled && !hasRecordGroups && ( - + )} { - const isAggregateQueryEnabled = useIsFeatureEnabled( - 'IS_AGGREGATE_QUERY_ENABLED', - ); const currentRecordGroupId = useCurrentRecordGroupId(); const allRecordIds = useRecoilComponentValueV2( @@ -63,12 +58,6 @@ export const RecordTableRecordGroupRows = () => { })} - {isAggregateQueryEnabled && ( - - )} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx index f2e19d7506..8e731aaf5a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx @@ -15,6 +15,7 @@ export const RecordTableBodyDroppable = ({ isDropDisabled, }: RecordTableBodyDroppableProps) => { const [v4Persistable] = useState(v4()); + const recordTableBodyId = `record-table-body${recordGroupId ? '-' + recordGroupId : ''}`; return ( {(provided) => ( { + const isAggregateQueryEnabled = useIsFeatureEnabled( + 'IS_AGGREGATE_QUERY_ENABLED', + ); const allRecordIds = useRecoilComponentValueV2( recordIndexAllRecordIdsComponentSelector, ); @@ -29,21 +34,29 @@ export const RecordTableRecordGroupsBody = () => { } return ( - - {visibleRecordGroupIds.map((recordGroupId, index) => ( - - - - {index > 0 && } - - - - - - ))} - + <> + + {visibleRecordGroupIds.map((recordGroupId, index) => ( + + + + {index > 0 && } + + + + {isAggregateQueryEnabled && ( + + )} + + + ))} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx similarity index 69% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableFooter.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx index b0fb27dd64..31b7368da5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx @@ -1,17 +1,16 @@ import styled from '@emotion/styled'; import { MOBILE_VIEWPORT } from 'twenty-ui'; -import { TABLE_CELL_CHECKBOX_MIN_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox'; -import { TABLE_CELL_GRIP_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip'; -import { RecordTableFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableFooterCell'; +import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell'; +import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -const StyledTableFoot = styled.thead` +const StyledTableFoot = styled.thead<{ endOfTableSticky?: boolean }>` cursor: pointer; th:nth-of-type(1) { - width: 9px; + width: ${FIRST_TH_WIDTH}; left: 0; border-right-color: ${({ theme }) => theme.background.primary}; } @@ -59,31 +58,23 @@ const StyledTableFoot = styled.thead` } } - &.header-sticky { - th { - position: sticky; - top: 0; - z-index: 5; - } - } - - &.header-sticky.first-columns-sticky { - th:nth-of-type(1), - th:nth-of-type(2), - th:nth-of-type(3) { - z-index: 10; - } + tr { + position: sticky; + z-index: 5; + ${({ endOfTableSticky }) => endOfTableSticky && `bottom: 0;`} } `; -const StyledDiv = styled.div` - width: calc(${TABLE_CELL_GRIP_WIDTH} + ${TABLE_CELL_CHECKBOX_MIN_WIDTH}); +const StyledTh = styled.th` + background-color: ${({ theme }) => theme.background.primary}; `; -export const RecordTableFooter = ({ +export const RecordTableAggregateFooter = ({ currentRecordGroupId, + endOfTableSticky, }: { currentRecordGroupId?: string; + endOfTableSticky?: boolean; }) => { const visibleTableColumns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, @@ -93,12 +84,13 @@ export const RecordTableFooter = ({ - - + + {visibleTableColumns.map((column, index) => ( - viewField.fieldMetadataId === column.fieldMetadataId, ); - if (!currentViewField) { - throw new Error('ViewField not found'); - } - useScopedHotkeys( [Key.Escape], () => { @@ -56,6 +52,9 @@ export const RecordTableColumnFooterDropdown = ({ const handleAggregationChange = ( aggregateOperation: AGGREGATE_OPERATIONS, ) => { + if (!currentViewField) { + throw new Error('ViewField not found'); + } updateViewFieldRecords([ { ...currentViewField, aggregateOperation: aggregateOperation }, ]); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx similarity index 68% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx index a306c1029e..45f36eb02e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx @@ -1,12 +1,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useState } from 'react'; -import { - AppTooltip, - IconChevronDown, - isDefined, - TooltipDelay, -} from 'twenty-ui'; +import { IconChevronDown, isDefined } from 'twenty-ui'; const StyledCell = styled.div` align-items: center; @@ -37,6 +32,27 @@ const StyledText = styled.span` z-index: 1; `; +const StyledValueContainer = styled.div` + align-items: center; + display: flex; + flex: 1 0 0; + gap: 4px; + height: 32px; + justify-content: flex-end; + padding: 8px; +`; + +const StyledLabel = styled.div` + align-items: center; + display: flex; + gap: 4px; +`; + +const StyledValue = styled.div` + color: ${({ theme }) => theme.color.gray60}; + flex: 1 0 0; +`; + const StyledIcon = styled(IconChevronDown)` align-items: center; display: flex; @@ -46,7 +62,7 @@ const StyledIcon = styled(IconChevronDown)` padding-right: ${({ theme }) => theme.spacing(2)}; `; -export const RecordTableColumnFooterAggregateValue = ({ +export const RecordTableColumnAggregateFooterValue = ({ dropdownId, aggregateValue, aggregateLabel, @@ -70,20 +86,15 @@ export const RecordTableColumnFooterAggregateValue = ({ {isHovered || isDefined(aggregateValue) || isFirstCell ? ( <> - - {aggregateValue ?? 'Calculate'} - - - {isDefined(aggregateValue) && isDefined(aggregateLabel) && ( - + {isDefined(aggregateValue) ? ( + + {aggregateLabel} + {aggregateValue} + + ) : ( + Calculate )} + ) : ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnFooterWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx similarity index 86% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnFooterWithDropdown.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx index 6c7cf7db19..13c49fc30e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnFooterWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx @@ -1,6 +1,6 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { RecordTableColumnFooterAggregateValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue'; -import { RecordTableColumnFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterDropdown'; +import { RecordTableColumnAggregateFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdown'; +import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue'; import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -44,7 +44,7 @@ export const RecordTableColumnFooterWithDropdown = ({ onClose={handleDropdownClose} dropdownId={dropdownId} clickableComponent={ - } dropdownComponents={ - diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx index 96bb3d7ca1..c9851f04d1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx @@ -32,13 +32,10 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( recordIndexViewFilterGroups, ); - const viewFieldId = currentViewWithSavedFiltersAndSorts?.viewFields?.find( - (viewField) => viewField.fieldMetadataId === fieldMetadataId, - )?.id; - - if (!viewFieldId) { - throw new Error('ViewField not found'); - } + const viewFieldId = + currentViewWithSavedFiltersAndSorts?.viewFields?.find( + (viewField) => viewField.fieldMetadataId === fieldMetadataId, + )?.id ?? ''; const aggregateOperationForViewField = useRecoilValue( aggregateOperationForViewFieldState({ viewFieldId: viewFieldId }), diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index ae08ea612f..cf33fa7d71 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -7,11 +7,13 @@ import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/re import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn'; import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn'; +export const FIRST_TH_WIDTH = '9px'; + const StyledTableHead = styled.thead` cursor: pointer; th:nth-of-type(1) { - width: 9px; + width: ${FIRST_TH_WIDTH}; left: 0; border-right-color: ${({ theme }) => theme.background.primary}; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx index 9274eb501d..2fa9ff5906 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx @@ -9,6 +9,7 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; +import { RecordTableRecordGroupStickyEffect } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; @@ -107,6 +108,7 @@ export const RecordTableRecordGroupSection = () => { weight="medium" /> {recordIdsByGroup.length} + diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect.tsx new file mode 100644 index 0000000000..889730417c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect.tsx @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; + +import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; +import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; +import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; + +export const RecordTableRecordGroupStickyEffect = () => { + const scrollLeft = useRecoilComponentValueV2( + scrollWrapperScrollLeftComponentState, + ); + + const setIsRecordTableScrolledLeft = useSetRecoilComponentStateV2( + isRecordTableScrolledLeftComponentState, + ); + + const currentRecordGroupId = useCurrentRecordGroupId(); + + useEffect(() => { + setIsRecordTableScrolledLeft(scrollLeft === 0); + if (scrollLeft > 0) { + document + .getElementById( + `record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`, + ) + ?.classList.add('first-columns-sticky'); + document + .getElementById( + `record-table-body${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`, + ) + ?.classList.add('first-columns-sticky'); + } else { + document + .getElementById( + `record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`, + ) + ?.classList.remove('first-columns-sticky'); + document + .getElementById( + `record-table-body${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`, + ) + ?.classList.remove('first-columns-sticky'); + } + }, [currentRecordGroupId, scrollLeft, setIsRecordTableScrolledLeft]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx index cf8da62582..4d5a34a457 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx @@ -19,6 +19,7 @@ export type ContextProviderName = | 'test' | 'showPageActivityContainer' | 'navigationDrawer' + | 'aggregateFooterCell' | 'modalContent'; const createScrollWrapperContext = (id: string) => @@ -52,6 +53,8 @@ export const ShowPageActivityContainerScrollWrapperContext = export const NavigationDrawerScrollWrapperContext = createScrollWrapperContext('navigationDrawer'); export const TestScrollWrapperContext = createScrollWrapperContext('test'); +export const AggregateFooterCellScrollWrapperContext = + createScrollWrapperContext('aggregateFooterCell'); export const ModalContentScrollWrapperContext = createScrollWrapperContext('modalContent'); @@ -85,6 +88,8 @@ export const getContextByProviderName = ( return ShowPageActivityContainerScrollWrapperContext; case 'navigationDrawer': return NavigationDrawerScrollWrapperContext; + case 'aggregateFooterCell': + return AggregateFooterCellScrollWrapperContext; case 'modalContent': return ModalContentScrollWrapperContext; default: diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index b91b0301b2..777919d2f5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -20,6 +20,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { isDefined } from 'src/utils/is-defined'; import { isPlainObject } from 'src/utils/is-plain-object'; export class ObjectRecordsToGraphqlConnectionHelper { @@ -95,7 +96,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { selectedAggregatedFields: Record; objectRecordsAggregatedValues: Record; }) => { - if (!objectRecordsAggregatedValues) { + if (!isDefined(objectRecordsAggregatedValues)) { return {}; } @@ -104,7 +105,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { const aggregatedFieldValue = objectRecordsAggregatedValues[aggregatedFieldName]; - if (!aggregatedFieldValue) { + if (!isDefined(aggregatedFieldValue)) { return acc; }