diff --git a/packages/twenty-front/nyc.config.cjs b/packages/twenty-front/nyc.config.cjs index 35ca9cb24..29f6d82cb 100644 --- a/packages/twenty-front/nyc.config.cjs +++ b/packages/twenty-front/nyc.config.cjs @@ -10,7 +10,7 @@ const globalCoverage = { const modulesCoverage = { branches: 25, statements: 44, - lines: 45, + lines: 44, functions: 38, include: ['src/modules/**/*'], exclude: ['src/**/*.ts'], 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 2bc92e711..35a3188ae 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,3 +1,4 @@ +import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import styled from '@emotion/styled'; import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui'; @@ -6,7 +7,7 @@ const StyledTag = styled(Tag)` width: 100%; `; -const StyledContainer = styled.div` +const StyledHeader = styled(StyledHeaderDropdownButton)` padding: 0; `; @@ -22,8 +23,8 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({ const { isDropdownOpen } = useDropdown(dropdownId); return ( -
- + + <> )} - -
+ + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts index 664bf184f..7c4d89988 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts @@ -1,28 +1,14 @@ -import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -import { buildRecordGqlFieldsAggregateForRecordBoard } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard'; -import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; -import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; -import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; -import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; -import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; -import { UserContext } from '@/users/contexts/UserContext'; +import { useAggregateRecordsForHeader } from '@/object-record/record-table/hooks/useAggregateRecordsForHeader'; import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; export const useAggregateRecordsForRecordBoardColumn = () => { const { columnDefinition } = useContext(RecordBoardColumnContext); - const { objectMetadataItem } = useContext(RecordBoardContext); - - const recordIndexKanbanAggregateOperation = useRecoilValue( - recordIndexKanbanAggregateOperationState, - ); - const recordIndexKanbanFieldMetadataId = useRecoilValue( recordIndexKanbanFieldMetadataIdState, ); @@ -37,56 +23,16 @@ export const useAggregateRecordsForRecordBoardColumn = () => { ); } - const recordGqlFieldsAggregate = buildRecordGqlFieldsAggregateForRecordBoard({ - objectMetadataItem: objectMetadataItem, - recordIndexKanbanAggregateOperation: recordIndexKanbanAggregateOperation, - kanbanFieldName: kanbanFieldName, - }); - - const recordIndexViewFilterGroups = useRecoilValue( - recordIndexViewFilterGroupsState, - ); - - const recordIndexFilters = useRecoilValue(recordIndexFiltersState); - - const { filterValueDependencies } = useFilterValueDependencies(); - - const requestFilters = computeViewRecordGqlOperationFilter( - filterValueDependencies, - recordIndexFilters, - objectMetadataItem.fields, - recordIndexViewFilterGroups, - ); - - const filter = { - ...requestFilters, + const additionalFilters = { [kanbanFieldName]: columnDefinition.value === null ? { is: 'NULL' } : { eq: columnDefinition.value }, }; - const { data } = useAggregateRecords({ - objectNameSingular: objectMetadataItem.nameSingular, - recordGqlFieldsAggregate, - filter, - }); - - const { dateFormat, timeFormat, timeZone } = useContext(UserContext); - - const { value, labelWithFieldName } = computeAggregateValueAndLabel({ - data, + return useAggregateRecordsForHeader({ objectMetadataItem, - fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId, - aggregateOperation: recordIndexKanbanAggregateOperation?.operation, + additionalFilters, fallbackFieldName: kanbanFieldName, - dateFormat, - timeFormat, - timeZone, }); - - return { - aggregateValue: value, - aggregateLabel: isDefined(value) ? labelWithFieldName : undefined, - }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForRecordBoard.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts similarity index 84% rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForRecordBoard.test.ts rename to packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts index 2afbf90e0..45a3db3a8 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForRecordBoard.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts @@ -1,6 +1,6 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { buildRecordGqlFieldsAggregateForRecordBoard } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard'; +import { buildRecordGqlFieldsAggregateForView } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForView'; import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -8,7 +8,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a'; const MOCK_KANBAN_FIELD = 'stage'; -describe('buildRecordGqlFieldsAggregateForRecordBoard', () => { +describe('buildRecordGqlFieldsAggregateForView', () => { const mockObjectMetadata: ObjectMetadataItem = { id: '123', nameSingular: 'opportunity', @@ -50,10 +50,10 @@ describe('buildRecordGqlFieldsAggregateForRecordBoard', () => { operation: AGGREGATE_OPERATIONS.sum, }; - const result = buildRecordGqlFieldsAggregateForRecordBoard({ + const result = buildRecordGqlFieldsAggregateForView({ objectMetadataItem: mockObjectMetadata, recordIndexKanbanAggregateOperation: kanbanAggregateOperation, - kanbanFieldName: MOCK_KANBAN_FIELD, + fieldNameForCount: MOCK_KANBAN_FIELD, }); expect(result).toEqual({ @@ -67,10 +67,10 @@ describe('buildRecordGqlFieldsAggregateForRecordBoard', () => { operation: AGGREGATE_OPERATIONS.count, }; - const result = buildRecordGqlFieldsAggregateForRecordBoard({ + const result = buildRecordGqlFieldsAggregateForView({ objectMetadataItem: mockObjectMetadata, recordIndexKanbanAggregateOperation: operation, - kanbanFieldName: MOCK_KANBAN_FIELD, + fieldNameForCount: MOCK_KANBAN_FIELD, }); expect(result).toEqual({ @@ -85,10 +85,10 @@ describe('buildRecordGqlFieldsAggregateForRecordBoard', () => { }; expect(() => - buildRecordGqlFieldsAggregateForRecordBoard({ + buildRecordGqlFieldsAggregateForView({ objectMetadataItem: mockObjectMetadata, recordIndexKanbanAggregateOperation: operation, - kanbanFieldName: MOCK_KANBAN_FIELD, + fieldNameForCount: MOCK_KANBAN_FIELD, }), ).toThrow( `No field found to compute aggregate operation ${AGGREGATE_OPERATIONS.sum} on object ${mockObjectMetadata.nameSingular}`, diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForView.ts similarity index 90% rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard.ts rename to packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForView.ts index 7a3f44a04..0c0b1b82d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForView.ts @@ -4,14 +4,14 @@ import { KanbanAggregateOperation } from '@/object-record/record-index/states/re import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { isDefined } from '~/utils/isDefined'; -export const buildRecordGqlFieldsAggregateForRecordBoard = ({ +export const buildRecordGqlFieldsAggregateForView = ({ objectMetadataItem, recordIndexKanbanAggregateOperation, - kanbanFieldName, + fieldNameForCount, }: { objectMetadataItem: ObjectMetadataItem; recordIndexKanbanAggregateOperation: KanbanAggregateOperation; - kanbanFieldName: string; + fieldNameForCount: string; }): RecordGqlFieldsAggregate => { let recordGqlFieldsAggregate = {}; @@ -31,7 +31,7 @@ export const buildRecordGqlFieldsAggregateForRecordBoard = ({ ); } else { recordGqlFieldsAggregate = { - [kanbanFieldName]: [AGGREGATE_OPERATIONS.count], + [fieldNameForCount]: [AGGREGATE_OPERATIONS.count], }; } } else { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts new file mode 100644 index 000000000..42d5cff5d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts @@ -0,0 +1,74 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; +import { buildRecordGqlFieldsAggregateForView } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForView'; +import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; +import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; +import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; +import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; +import { UserContext } from '@/users/contexts/UserContext'; +import { useContext } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; + +type UseAggregateRecordsProps = { + objectMetadataItem: ObjectMetadataItem; + additionalFilters?: Record; + fallbackFieldName: string; +}; + +export const useAggregateRecordsForHeader = ({ + objectMetadataItem, + additionalFilters = {}, + fallbackFieldName, +}: UseAggregateRecordsProps) => { + const recordIndexViewFilterGroups = useRecoilValue( + recordIndexViewFilterGroupsState, + ); + + const recordIndexFilters = useRecoilValue(recordIndexFiltersState); + + const recordIndexKanbanAggregateOperation = useRecoilValue( + recordIndexKanbanAggregateOperationState, + ); + + const { filterValueDependencies } = useFilterValueDependencies(); + + const { dateFormat, timeFormat, timeZone } = useContext(UserContext); + + const requestFilters = computeViewRecordGqlOperationFilter( + filterValueDependencies, + recordIndexFilters, + objectMetadataItem.fields, + recordIndexViewFilterGroups, + ); + + const recordGqlFieldsAggregate = buildRecordGqlFieldsAggregateForView({ + objectMetadataItem, + recordIndexKanbanAggregateOperation, + fieldNameForCount: fallbackFieldName, + }); + + const { data } = useAggregateRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + recordGqlFieldsAggregate, + filter: { ...requestFilters, ...additionalFilters }, + }); + + const { value, labelWithFieldName } = computeAggregateValueAndLabel({ + data, + objectMetadataItem, + fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId, + aggregateOperation: recordIndexKanbanAggregateOperation?.operation, + fallbackFieldName, + dateFormat, + timeFormat, + timeZone, + }); + + return { + aggregateValue: value, + aggregateLabel: isDefined(value) ? labelWithFieldName : undefined, + }; +}; 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 7503b4a50..1d192d690 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 @@ -8,16 +8,17 @@ import { Tag, } from 'twenty-ui'; +import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; 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 { useAggregateRecordsForRecordTableSection } from '@/object-record/record-table/record-table-section/hooks/useAggregateRecordsForRecordTableSection'; 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'; -import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilValue } from 'recoil'; @@ -37,24 +38,22 @@ const StyledAnimatedLightIconButton = styled(AnimatedLightIconButton)` margin: auto; `; -const StyledTotalRow = styled.span` - color: ${({ theme }) => theme.font.color.tertiary}; - margin-left: ${({ theme }) => theme.spacing(2)}; - text-align: center; - vertical-align: middle; -`; - const StyledRecordGroupSection = styled(RecordTableTd)` border-right: none; height: 32px; display: flex; align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; `; const StyledEmptyTd = styled.td` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; `; +const StyledTag = styled(Tag)` + flex-shrink: 0; +`; + export const RecordTableRecordGroupSection = () => { const theme = useTheme(); @@ -64,10 +63,10 @@ export const RecordTableRecordGroupSection = () => { visibleTableColumnsComponentSelector, ); - const recordIdsByGroup = useRecoilComponentFamilyValueV2( - recordIndexRecordIdsByGroupComponentFamilyState, - currentRecordGroupId, - ); + const { objectMetadataItem } = useRecordTableContextOrThrow(); + + const { aggregateValue, aggregateLabel } = + useAggregateRecordsForRecordTableSection(); const [ isRecordGroupTableSectionToggled, @@ -102,7 +101,7 @@ export const RecordTableRecordGroupSection = () => { /> - { text={recordGroup.title} weight="medium" /> - {recordIdsByGroup.length} + diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/hooks/useAggregateRecordsForRecordTableSection.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/hooks/useAggregateRecordsForRecordTableSection.tsx new file mode 100644 index 000000000..d8b20f046 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/hooks/useAggregateRecordsForRecordTableSection.tsx @@ -0,0 +1,16 @@ +import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useAggregateRecordsForHeader } from '@/object-record/record-table/hooks/useAggregateRecordsForHeader'; + +const DEFAULT_FIELD_NAME_FOR_COUNT = 'name'; + +export const useAggregateRecordsForRecordTableSection = () => { + const { objectMetadataItem } = useRecordTableContextOrThrow(); + const { recordGroupFilter } = useRecordGroupFilter(objectMetadataItem.fields); + + return useAggregateRecordsForHeader({ + objectMetadataItem, + additionalFilters: recordGroupFilter, + fallbackFieldName: DEFAULT_FIELD_NAME_FOR_COUNT, + }); +};