diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx index 31b7368da..d4bf5358f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import { MOBILE_VIEWPORT } from 'twenty-ui'; 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 { 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'; @@ -90,12 +91,18 @@ export const RecordTableAggregateFooter = ({ {visibleTableColumns.map((column, index) => ( - + value={{ + viewFieldId: column.viewFieldId || '', + fieldMetadataId: column.fieldMetadataId, + }} + > + + ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx index 2abb223ee..f6f66b4dc 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; -import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; @@ -12,20 +11,19 @@ const COLUMN_MIN_WIDTH = 104; const StyledColumnFooterCell = styled.th<{ columnWidth: number; - isResizing?: boolean; }>` - color: ${({ theme }) => theme.font.color.tertiary}; - padding: 0; - text-align: left; - transition: 0.3s ease; - background-color: ${({ theme }) => theme.background.primary}; + color: ${({ theme }) => theme.font.color.tertiary}; + overflow: hidden; + padding: 0; + + position: relative; ${({ columnWidth }) => ` min-width: ${columnWidth}px; width: ${columnWidth}px; `} - position: relative; - user-select: none; + text-align: left; + transition: 0.3s ease; ${({ theme }) => { return ` &:hover { @@ -36,23 +34,14 @@ const StyledColumnFooterCell = styled.th<{ }; `; }}; - ${({ isResizing, theme }) => { - if (isResizing === true) { - return `&:after { - background-color: ${theme.color.blue}; - bottom: 0; - content: ''; - display: block; - position: absolute; - right: -1px; - top: 0; - width: 2px; - }`; - } - }}; - // TODO: refactor this, each component should own its CSS + user-select: none; overflow: auto; + scrollbar-width: none; + -ms-overflow-style: none; + *::-webkit-scrollbar { + display: none; + } `; const StyledColumnFootContainer = styled.div` @@ -62,11 +51,9 @@ const StyledColumnFootContainer = styled.div` `; export const RecordTableAggregateFooterCell = ({ - column, isFirstCell = false, currentRecordGroupId, }: { - column: ColumnDefinition; isFirstCell?: boolean; currentRecordGroupId?: string; }) => { @@ -76,18 +63,19 @@ export const RecordTableAggregateFooterCell = ({ mapArrayToObject(tableColumns, ({ fieldMetadataId }) => fieldMetadataId), [tableColumns], ); + const { fieldMetadataId } = useContext( + RecordTableColumnAggregateFooterCellContext, + ); return ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext.tsx new file mode 100644 index 000000000..f5d1db5c3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext.tsx @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +export type RecordTableColumnAggregateFooterCellValue = { + viewFieldId: string; + fieldMetadataId: string; +}; + +export const RecordTableColumnAggregateFooterCellContext = + createContext( + {} as RecordTableColumnAggregateFooterCellValue, + ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx index 800f551de..1675620c4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx @@ -1,23 +1,6 @@ -import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { useTheme } from '@emotion/react'; +import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter'; import styled from '@emotion/styled'; -import { useState } from 'react'; -import { IconChevronDown, isDefined } from 'twenty-ui'; - -const StyledCell = styled.div` - align-items: center; - display: flex; - flex-direction: row; - flex-shrink: 0; - font-weight: ${({ theme }) => theme.font.weight.medium}; - - gap: ${({ theme }) => theme.spacing(1)}; - height: ${({ theme }) => theme.spacing(7)}; - justify-content: space-between; - min-width: ${({ theme }) => theme.spacing(7)}; - flex-grow: 1; - width: 100%; -`; +import { isDefined } from 'twenty-ui'; const StyledText = styled.span` overflow: hidden; @@ -54,56 +37,34 @@ const StyledValue = styled.div` flex: 1 0 0; `; -const StyledIcon = styled(IconChevronDown)` - align-items: center; - display: flex; - height: 20px; - justify-content: center; - flex-grow: 0; - padding-right: ${({ theme }) => theme.spacing(2)}; -`; - export const RecordTableColumnAggregateFooterValue = ({ dropdownId, - aggregateValue, - aggregateLabel, - isFirstCell, + fieldMetadataId, }: { dropdownId: string; - isFirstCell: boolean; - aggregateValue?: string | number | null; - aggregateLabel?: string; + fieldMetadataId: string; }) => { - const [isHovered, setIsHovered] = useState(false); - const { isDropdownOpen } = useDropdown(dropdownId); const sanitizedId = `tooltip-${dropdownId.replace(/[^a-zA-Z0-9-_]/g, '-')}`; - const theme = useTheme(); - const shouldShowValue = - isHovered || isDropdownOpen || isDefined(aggregateValue) || isFirstCell; + + const { aggregateValue, aggregateLabel, isLoading } = + useAggregateRecordsForRecordTableColumnFooter(fieldMetadataId); + return ( -
{ - setIsHovered(true); - }} - onMouseLeave={() => setIsHovered(false)} - > - - {shouldShowValue ? ( - <> - {isDefined(aggregateValue) ? ( - - {aggregateLabel} - {aggregateValue} - - ) : ( - Calculate - )} - - - ) : ( - <> - )} - -
+ <> + {isDefined(aggregateValue) || isLoading ? ( + + {isLoading ? ( + <> + ) : ( + <> + {aggregateLabel} + {aggregateValue} + + )} + + ) : ( + Calculate + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx new file mode 100644 index 000000000..151da5908 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx @@ -0,0 +1,80 @@ +import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; +import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue'; +import { hasAggregateOperationForViewFieldFamilySelector } from '@/object-record/record-table/record-table-footer/states/hasAggregateOperationForViewFieldFamilySelector'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useContext, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { IconChevronDown } from 'twenty-ui'; + +const StyledCell = styled.div` + align-items: center; + display: flex; + flex-direction: row; + flex-shrink: 0; + font-weight: ${({ theme }) => theme.font.weight.medium}; + + gap: ${({ theme }) => theme.spacing(1)}; + height: ${({ theme }) => theme.spacing(7)}; + justify-content: space-between; + min-width: ${({ theme }) => theme.spacing(7)}; + flex-grow: 1; + width: 100%; +`; + +const StyledIcon = styled(IconChevronDown)` + align-items: center; + display: flex; + height: 20px; + justify-content: center; + flex-grow: 0; + padding-right: ${({ theme }) => theme.spacing(2)}; +`; + +export const RecordTableColumnAggregateFooterValueCell = ({ + dropdownId, + isFirstCell, +}: { + dropdownId: string; + isFirstCell: boolean; +}) => { + const [isHovered, setIsHovered] = useState(false); + const { isDropdownOpen } = useDropdown(dropdownId); + const theme = useTheme(); + const { viewFieldId, fieldMetadataId } = useContext( + RecordTableColumnAggregateFooterCellContext, + ); + + const hasAggregateOperationForViewField = useRecoilValue( + hasAggregateOperationForViewFieldFamilySelector({ + viewFieldId, + }), + ); + + return ( +
{ + setIsHovered(true); + }} + onMouseLeave={() => setIsHovered(false)} + > + + {isHovered || + isDropdownOpen || + hasAggregateOperationForViewField || + isFirstCell ? ( + <> + + + + ) : ( + <> + )} + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx index d205d21b5..a0062384c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx @@ -1,29 +1,29 @@ import { useCurrentContentId } from '@/dropdown/hooks/useCurrentContentId'; -import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { RecordTableColumnAggregateFooterDropdownContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent'; import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; -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 { RecordTableColumnAggregateFooterValueCell } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell'; import { RecordTableFooterAggregateContentId } from '@/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId'; -import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper'; -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; type RecordTableColumnFooterWithDropdownProps = { - column: ColumnDefinition; isFirstCell: boolean; currentRecordGroupId?: string; }; export const RecordTableColumnFooterWithDropdown = ({ - column, currentRecordGroupId, isFirstCell, }: RecordTableColumnFooterWithDropdownProps) => { const { currentContentId, handleContentChange, handleResetContent } = useCurrentContentId(); + const { fieldMetadataId } = useContext( + RecordTableColumnAggregateFooterCellContext, + ); + const { toggleScrollXWrapper, toggleScrollYWrapper } = useToggleScrollWrapper(); @@ -38,12 +38,9 @@ export const RecordTableColumnFooterWithDropdown = ({ toggleScrollYWrapper(true); }, [handleResetContent, toggleScrollXWrapper, toggleScrollYWrapper]); - const { aggregateValue, aggregateLabel } = - useAggregateRecordsForRecordTableColumnFooter(column.fieldMetadataId); - const dropdownId = currentRecordGroupId - ? `${column.fieldMetadataId}-footer-${currentRecordGroupId}` - : `${column.fieldMetadataId}-footer`; + ? `${fieldMetadataId}-footer-${currentRecordGroupId}` + : `${fieldMetadataId}-footer`; return ( @@ -65,7 +60,7 @@ export const RecordTableColumnFooterWithDropdown = ({ onContentChange: handleContentChange, resetContent: handleResetContent, dropdownId: dropdownId, - fieldMetadataId: column.fieldMetadataId, + fieldMetadataId: fieldMetadataId, }} > 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 d7c66b3e6..03b774545 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 @@ -6,9 +6,10 @@ import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useReco import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; -import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; @@ -21,7 +22,6 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( const { objectMetadataItem } = useRecordTableContextOrThrow(); const { recordGroupFilter } = useRecordGroupFilter(objectMetadataItem.fields); - const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView(); const recordIndexViewFilterGroups = useRecoilValue( recordIndexViewFilterGroupsState, ); @@ -37,13 +37,11 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( recordIndexViewFilterGroups, ); - const viewFieldId = - currentViewWithSavedFiltersAndSorts?.viewFields?.find( - (viewField) => viewField.fieldMetadataId === fieldMetadataId, - )?.id ?? ''; - + const { viewFieldId } = useContext( + RecordTableColumnAggregateFooterCellContext, + ); const aggregateOperationForViewField = useRecoilValue( - viewFieldAggregateOperationState({ viewFieldId: viewFieldId }), + viewFieldAggregateOperationState({ viewFieldId }), ); const fieldName = objectMetadataItem.fields.find( @@ -57,7 +55,7 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( } : {}; - const { data } = useAggregateRecords({ + const { data, loading } = useAggregateRecords({ objectNameSingular: objectMetadataItem.nameSingular, recordGqlFieldsAggregate, filter: { ...requestFilters, ...recordGroupFilter }, @@ -75,5 +73,6 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( return { aggregateValue: value, aggregateLabel: isDefined(value) ? label : undefined, + isLoading: loading, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/hasAggregateOperationForViewFieldFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/hasAggregateOperationForViewFieldFamilySelector.ts new file mode 100644 index 000000000..4b7884046 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/hasAggregateOperationForViewFieldFamilySelector.ts @@ -0,0 +1,21 @@ +import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; +import { selectorFamily } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; + +export const hasAggregateOperationForViewFieldFamilySelector = selectorFamily< + boolean, + { viewFieldId: string } +>({ + key: 'hasAggregateOperationForViewField', + get: + ({ viewFieldId }) => + ({ get }) => { + const aggregateOperation = get( + viewFieldAggregateOperationState({ + viewFieldId, + }), + ); + + return isDefined(aggregateOperation); + }, +});