Improve aggregate footer cell display (#9124)

Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie 2024-12-19 15:36:14 +01:00 committed by GitHub
parent 7d8f895ae9
commit ed56a68b7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 268 additions and 114 deletions

View File

@ -1,7 +1,6 @@
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui'; import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui';
import { formatNumber } from '~/utils/format/number';
const StyledButton = styled(StyledHeaderDropdownButton)` const StyledButton = styled(StyledHeaderDropdownButton)`
padding: 0; padding: 0;
@ -19,10 +18,7 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
return ( return (
<div id={dropdownId}> <div id={dropdownId}>
<StyledButton> <StyledButton>
<Tag <Tag text={value ? value.toString() : '-'} color={'transparent'} />
text={value ? formatNumber(Number(value)) : '-'}
color={'transparent'}
/>
<AppTooltip <AppTooltip
anchorSelect={`#${dropdownId}`} anchorSelect={`#${dropdownId}`}
content={tooltip} content={tooltip}

View File

@ -74,7 +74,7 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
skip: !isAggregateQueryEnabled, skip: !isAggregateQueryEnabled,
}); });
const { value, label } = computeAggregateValueAndLabel({ const { value, labelWithFieldName } = computeAggregateValueAndLabel({
data, data,
objectMetadataItem, objectMetadataItem,
fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId, fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId,
@ -84,6 +84,6 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
return { return {
aggregateValue: isAggregateQueryEnabled ? value : recordCount, aggregateValue: isAggregateQueryEnabled ? value : recordCount,
aggregateLabel: isDefined(value) ? label : undefined, aggregateLabel: isDefined(value) ? labelWithFieldName : undefined,
}; };
}; };

View File

@ -47,8 +47,81 @@ describe('computeAggregateValueAndLabel', () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
value: 2, value: '2',
label: 'Sum of amount', 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({ expect(result).toEqual({
value: undefined, value: '-',
label: 'Sum of amount', label: 'Sum',
labelWithFieldName: 'Sum of amount',
}); });
}); });
}); });

View File

@ -4,6 +4,8 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { formatAmount } from '~/utils/format/formatAmount';
import { formatNumber } from '~/utils/format/number';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const computeAggregateValueAndLabel = ({ export const computeAggregateValueAndLabel = ({
@ -42,12 +44,33 @@ export const computeAggregateValueAndLabel = ({
const aggregateValue = data[field.name]?.[aggregateOperation]; const aggregateValue = data[field.name]?.[aggregateOperation];
const value = let value;
isDefined(aggregateValue) && field.type === FieldMetadataType.Currency
? Number(aggregateValue) / 1_000_000
: aggregateValue;
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 aggregateOperation === AGGREGATE_OPERATIONS.count
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}` ? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
: `${getAggregateOperationLabel(aggregateOperation)} of ${field.name}`; : `${getAggregateOperationLabel(aggregateOperation)} of ${field.name}`;
@ -55,5 +78,6 @@ export const computeAggregateValueAndLabel = ({
return { return {
value, value,
label, label,
labelWithFieldName,
}; };
}; };

View File

@ -4,6 +4,6 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon
export const recordIndexRecordGroupHideComponentState = export const recordIndexRecordGroupHideComponentState =
createComponentStateV2<boolean>({ createComponentStateV2<boolean>({
key: 'recordIndexRecordGroupHideComponentState', key: 'recordIndexRecordGroupHideComponentState',
defaultValue: true, defaultValue: false,
componentInstanceContext: ViewComponentInstanceContext, componentInstanceContext: ViewComponentInstanceContext,
}); });

View File

@ -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 { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects'; 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 { 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 { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
@ -88,7 +88,7 @@ export const RecordTable = () => {
<RecordTableEmptyState /> <RecordTableEmptyState />
) : ( ) : (
<> <>
<StyledTable className="entity-table-cell" ref={tableBodyRef}> <StyledTable ref={tableBodyRef}>
<RecordTableHeader /> <RecordTableHeader />
{!hasRecordGroups ? ( {!hasRecordGroups ? (
<RecordTableNoRecordGroupBody /> <RecordTableNoRecordGroupBody />
@ -97,7 +97,7 @@ export const RecordTable = () => {
)} )}
<RecordTableStickyEffect /> <RecordTableStickyEffect />
{isAggregateQueryEnabled && !hasRecordGroups && ( {isAggregateQueryEnabled && !hasRecordGroups && (
<RecordTableFooter /> <RecordTableAggregateFooter endOfTableSticky />
)} )}
</StyledTable> </StyledTable>
<DragSelect <DragSelect

View File

@ -1,7 +1,6 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter';
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow'; import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew'; import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
@ -9,14 +8,10 @@ import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-ta
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const RecordTableRecordGroupRows = () => { export const RecordTableRecordGroupRows = () => {
const isAggregateQueryEnabled = useIsFeatureEnabled(
'IS_AGGREGATE_QUERY_ENABLED',
);
const currentRecordGroupId = useCurrentRecordGroupId(); const currentRecordGroupId = useCurrentRecordGroupId();
const allRecordIds = useRecoilComponentValueV2( const allRecordIds = useRecoilComponentValueV2(
@ -63,12 +58,6 @@ export const RecordTableRecordGroupRows = () => {
})} })}
<RecordTablePendingRecordGroupRow /> <RecordTablePendingRecordGroupRow />
<RecordTableRecordGroupSectionAddNew /> <RecordTableRecordGroupSectionAddNew />
{isAggregateQueryEnabled && (
<RecordTableFooter
key={currentRecordGroupId}
currentRecordGroupId={currentRecordGroupId}
/>
)}
<RecordTableRecordGroupSectionLoadMore /> <RecordTableRecordGroupSectionLoadMore />
</> </>
); );

View File

@ -15,6 +15,7 @@ export const RecordTableBodyDroppable = ({
isDropDisabled, isDropDisabled,
}: RecordTableBodyDroppableProps) => { }: RecordTableBodyDroppableProps) => {
const [v4Persistable] = useState(v4()); const [v4Persistable] = useState(v4());
const recordTableBodyId = `record-table-body${recordGroupId ? '-' + recordGroupId : ''}`;
return ( return (
<Droppable <Droppable
@ -23,7 +24,7 @@ export const RecordTableBodyDroppable = ({
> >
{(provided) => ( {(provided) => (
<RecordTableBody <RecordTableBody
id="record-table-body" id={recordTableBodyId}
ref={provided.innerRef} ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps} {...provided.droppableProps}

View File

@ -6,12 +6,17 @@ import { RecordTableRecordGroupRows } from '@/object-record/record-table/compone
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider'; import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow'; import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow';
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection'; import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const RecordTableRecordGroupsBody = () => { export const RecordTableRecordGroupsBody = () => {
const isAggregateQueryEnabled = useIsFeatureEnabled(
'IS_AGGREGATE_QUERY_ENABLED',
);
const allRecordIds = useRecoilComponentValueV2( const allRecordIds = useRecoilComponentValueV2(
recordIndexAllRecordIdsComponentSelector, recordIndexAllRecordIdsComponentSelector,
); );
@ -29,6 +34,7 @@ export const RecordTableRecordGroupsBody = () => {
} }
return ( return (
<>
<RecordTableBodyRecordGroupDragDropContextProvider> <RecordTableBodyRecordGroupDragDropContextProvider>
{visibleRecordGroupIds.map((recordGroupId, index) => ( {visibleRecordGroupIds.map((recordGroupId, index) => (
<RecordTableRecordGroupBodyContextProvider <RecordTableRecordGroupBodyContextProvider
@ -41,9 +47,16 @@ export const RecordTableRecordGroupsBody = () => {
<RecordTableRecordGroupSection /> <RecordTableRecordGroupSection />
<RecordTableRecordGroupRows /> <RecordTableRecordGroupRows />
</RecordTableBodyDroppable> </RecordTableBodyDroppable>
{isAggregateQueryEnabled && (
<RecordTableAggregateFooter
key={recordGroupId}
currentRecordGroupId={recordGroupId}
/>
)}
</RecordGroupContext.Provider> </RecordGroupContext.Provider>
</RecordTableRecordGroupBodyContextProvider> </RecordTableRecordGroupBodyContextProvider>
))} ))}
</RecordTableBodyRecordGroupDragDropContextProvider> </RecordTableBodyRecordGroupDragDropContextProvider>
</>
); );
}; };

View File

@ -1,17 +1,16 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from 'twenty-ui'; import { MOBILE_VIEWPORT } from 'twenty-ui';
import { TABLE_CELL_CHECKBOX_MIN_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox'; import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell';
import { TABLE_CELL_GRIP_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip'; import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { RecordTableFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableFooterCell';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
const StyledTableFoot = styled.thead` const StyledTableFoot = styled.thead<{ endOfTableSticky?: boolean }>`
cursor: pointer; cursor: pointer;
th:nth-of-type(1) { th:nth-of-type(1) {
width: 9px; width: ${FIRST_TH_WIDTH};
left: 0; left: 0;
border-right-color: ${({ theme }) => theme.background.primary}; border-right-color: ${({ theme }) => theme.background.primary};
} }
@ -59,31 +58,23 @@ const StyledTableFoot = styled.thead`
} }
} }
&.header-sticky { tr {
th {
position: sticky; position: sticky;
top: 0;
z-index: 5; z-index: 5;
} ${({ endOfTableSticky }) => endOfTableSticky && `bottom: 0;`}
}
&.header-sticky.first-columns-sticky {
th:nth-of-type(1),
th:nth-of-type(2),
th:nth-of-type(3) {
z-index: 10;
}
} }
`; `;
const StyledDiv = styled.div` const StyledTh = styled.th`
width: calc(${TABLE_CELL_GRIP_WIDTH} + ${TABLE_CELL_CHECKBOX_MIN_WIDTH}); background-color: ${({ theme }) => theme.background.primary};
`; `;
export const RecordTableFooter = ({ export const RecordTableAggregateFooter = ({
currentRecordGroupId, currentRecordGroupId,
endOfTableSticky,
}: { }: {
currentRecordGroupId?: string; currentRecordGroupId?: string;
endOfTableSticky?: boolean;
}) => { }) => {
const visibleTableColumns = useRecoilComponentValueV2( const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector, visibleTableColumnsComponentSelector,
@ -93,12 +84,13 @@ export const RecordTableFooter = ({
<StyledTableFoot <StyledTableFoot
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`} id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
data-select-disable data-select-disable
endOfTableSticky={endOfTableSticky}
> >
<tr> <tr>
<th /> <StyledTh />
<StyledDiv /> <StyledTh />
{visibleTableColumns.map((column, index) => ( {visibleTableColumns.map((column, index) => (
<RecordTableFooterCell <RecordTableAggregateFooterCell
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`} key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
column={column} column={column}
currentRecordGroupId={currentRecordGroupId} currentRecordGroupId={currentRecordGroupId}

View File

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterWithDropdown'; import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -61,7 +61,7 @@ const StyledColumnFootContainer = styled.div`
width: 100%; width: 100%;
`; `;
export const RecordTableFooterCell = ({ export const RecordTableAggregateFooterCell = ({
column, column,
isFirstCell = false, isFirstCell = false,
currentRecordGroupId, currentRecordGroupId,

View File

@ -14,7 +14,7 @@ import { useMemo } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { MenuItem } from 'twenty-ui'; import { MenuItem } from 'twenty-ui';
export const RecordTableColumnFooterDropdown = ({ export const RecordTableColumnAggregateFooterDropdown = ({
column, column,
dropdownId, dropdownId,
}: { }: {
@ -30,10 +30,6 @@ export const RecordTableColumnFooterDropdown = ({
(viewField) => viewField.fieldMetadataId === column.fieldMetadataId, (viewField) => viewField.fieldMetadataId === column.fieldMetadataId,
); );
if (!currentViewField) {
throw new Error('ViewField not found');
}
useScopedHotkeys( useScopedHotkeys(
[Key.Escape], [Key.Escape],
() => { () => {
@ -56,6 +52,9 @@ export const RecordTableColumnFooterDropdown = ({
const handleAggregationChange = ( const handleAggregationChange = (
aggregateOperation: AGGREGATE_OPERATIONS, aggregateOperation: AGGREGATE_OPERATIONS,
) => { ) => {
if (!currentViewField) {
throw new Error('ViewField not found');
}
updateViewFieldRecords([ updateViewFieldRecords([
{ ...currentViewField, aggregateOperation: aggregateOperation }, { ...currentViewField, aggregateOperation: aggregateOperation },
]); ]);

View File

@ -1,12 +1,7 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react'; import { useState } from 'react';
import { import { IconChevronDown, isDefined } from 'twenty-ui';
AppTooltip,
IconChevronDown,
isDefined,
TooltipDelay,
} from 'twenty-ui';
const StyledCell = styled.div` const StyledCell = styled.div`
align-items: center; align-items: center;
@ -37,6 +32,27 @@ const StyledText = styled.span`
z-index: 1; 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)` const StyledIcon = styled(IconChevronDown)`
align-items: center; align-items: center;
display: flex; display: flex;
@ -46,7 +62,7 @@ const StyledIcon = styled(IconChevronDown)`
padding-right: ${({ theme }) => theme.spacing(2)}; padding-right: ${({ theme }) => theme.spacing(2)};
`; `;
export const RecordTableColumnFooterAggregateValue = ({ export const RecordTableColumnAggregateFooterValue = ({
dropdownId, dropdownId,
aggregateValue, aggregateValue,
aggregateLabel, aggregateLabel,
@ -70,20 +86,15 @@ export const RecordTableColumnFooterAggregateValue = ({
<StyledCell> <StyledCell>
{isHovered || isDefined(aggregateValue) || isFirstCell ? ( {isHovered || isDefined(aggregateValue) || isFirstCell ? (
<> <>
<StyledText id={sanitizedId}> {isDefined(aggregateValue) ? (
{aggregateValue ?? 'Calculate'} <StyledValueContainer>
</StyledText> <StyledLabel>{aggregateLabel}</StyledLabel>
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} /> <StyledValue>{aggregateValue}</StyledValue>
{isDefined(aggregateValue) && isDefined(aggregateLabel) && ( </StyledValueContainer>
<AppTooltip ) : (
anchorSelect={`#${sanitizedId}`} <StyledText id={sanitizedId}>Calculate</StyledText>
content={aggregateLabel}
noArrow
place="top-start"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
)} )}
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
</> </>
) : ( ) : (
<></> <></>

View File

@ -1,6 +1,6 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableColumnFooterAggregateValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue'; import { RecordTableColumnAggregateFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdown';
import { RecordTableColumnFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterDropdown'; 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 { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -44,7 +44,7 @@ export const RecordTableColumnFooterWithDropdown = ({
onClose={handleDropdownClose} onClose={handleDropdownClose}
dropdownId={dropdownId} dropdownId={dropdownId}
clickableComponent={ clickableComponent={
<RecordTableColumnFooterAggregateValue <RecordTableColumnAggregateFooterValue
aggregateLabel={aggregateLabel} aggregateLabel={aggregateLabel}
aggregateValue={aggregateValue} aggregateValue={aggregateValue}
dropdownId={dropdownId} dropdownId={dropdownId}
@ -52,7 +52,7 @@ export const RecordTableColumnFooterWithDropdown = ({
/> />
} }
dropdownComponents={ dropdownComponents={
<RecordTableColumnFooterDropdown <RecordTableColumnAggregateFooterDropdown
column={column} column={column}
dropdownId={dropdownId} dropdownId={dropdownId}
/> />

View File

@ -32,13 +32,10 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
recordIndexViewFilterGroups, recordIndexViewFilterGroups,
); );
const viewFieldId = currentViewWithSavedFiltersAndSorts?.viewFields?.find( const viewFieldId =
currentViewWithSavedFiltersAndSorts?.viewFields?.find(
(viewField) => viewField.fieldMetadataId === fieldMetadataId, (viewField) => viewField.fieldMetadataId === fieldMetadataId,
)?.id; )?.id ?? '';
if (!viewFieldId) {
throw new Error('ViewField not found');
}
const aggregateOperationForViewField = useRecoilValue( const aggregateOperationForViewField = useRecoilValue(
aggregateOperationForViewFieldState({ viewFieldId: viewFieldId }), aggregateOperationForViewFieldState({ viewFieldId: viewFieldId }),

View File

@ -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 { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn'; import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
export const FIRST_TH_WIDTH = '9px';
const StyledTableHead = styled.thead` const StyledTableHead = styled.thead`
cursor: pointer; cursor: pointer;
th:nth-of-type(1) { th:nth-of-type(1) {
width: 9px; width: ${FIRST_TH_WIDTH};
left: 0; left: 0;
border-right-color: ${({ theme }) => theme.background.primary}; border-right-color: ${({ theme }) => theme.background.primary};
} }

View File

@ -9,6 +9,7 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; 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 { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
@ -107,6 +108,7 @@ export const RecordTableRecordGroupSection = () => {
weight="medium" weight="medium"
/> />
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow> <StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
<RecordTableRecordGroupStickyEffect />
</StyledRecordGroupSection> </StyledRecordGroupSection>
<StyledEmptyTd colSpan={visibleColumns.length - 1} /> <StyledEmptyTd colSpan={visibleColumns.length - 1} />
<StyledEmptyTd /> <StyledEmptyTd />

View File

@ -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 <></>;
};

View File

@ -19,6 +19,7 @@ export type ContextProviderName =
| 'test' | 'test'
| 'showPageActivityContainer' | 'showPageActivityContainer'
| 'navigationDrawer' | 'navigationDrawer'
| 'aggregateFooterCell'
| 'modalContent'; | 'modalContent';
const createScrollWrapperContext = (id: string) => const createScrollWrapperContext = (id: string) =>
@ -52,6 +53,8 @@ export const ShowPageActivityContainerScrollWrapperContext =
export const NavigationDrawerScrollWrapperContext = export const NavigationDrawerScrollWrapperContext =
createScrollWrapperContext('navigationDrawer'); createScrollWrapperContext('navigationDrawer');
export const TestScrollWrapperContext = createScrollWrapperContext('test'); export const TestScrollWrapperContext = createScrollWrapperContext('test');
export const AggregateFooterCellScrollWrapperContext =
createScrollWrapperContext('aggregateFooterCell');
export const ModalContentScrollWrapperContext = export const ModalContentScrollWrapperContext =
createScrollWrapperContext('modalContent'); createScrollWrapperContext('modalContent');
@ -85,6 +88,8 @@ export const getContextByProviderName = (
return ShowPageActivityContainerScrollWrapperContext; return ShowPageActivityContainerScrollWrapperContext;
case 'navigationDrawer': case 'navigationDrawer':
return NavigationDrawerScrollWrapperContext; return NavigationDrawerScrollWrapperContext;
case 'aggregateFooterCell':
return AggregateFooterCellScrollWrapperContext;
case 'modalContent': case 'modalContent':
return ModalContentScrollWrapperContext; return ModalContentScrollWrapperContext;
default: default:

View File

@ -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 { 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 { 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 { 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'; import { isPlainObject } from 'src/utils/is-plain-object';
export class ObjectRecordsToGraphqlConnectionHelper { export class ObjectRecordsToGraphqlConnectionHelper {
@ -95,7 +96,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
selectedAggregatedFields: Record<string, AggregationField[]>; selectedAggregatedFields: Record<string, AggregationField[]>;
objectRecordsAggregatedValues: Record<string, any>; objectRecordsAggregatedValues: Record<string, any>;
}) => { }) => {
if (!objectRecordsAggregatedValues) { if (!isDefined(objectRecordsAggregatedValues)) {
return {}; return {};
} }
@ -104,7 +105,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
const aggregatedFieldValue = const aggregatedFieldValue =
objectRecordsAggregatedValues[aggregatedFieldName]; objectRecordsAggregatedValues[aggregatedFieldName];
if (!aggregatedFieldValue) { if (!isDefined(aggregatedFieldValue)) {
return acc; return acc;
} }