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 dd12c11f72fb..5b945ba8b353 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 44401248616b..5a03afa7b36a 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 b30c673c708a..80beb0e39dec 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 bc571f675526..3500e331e57e 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 72d874499ae1..853a3623dc64 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 f2e19d7506af..8e731aaf5a1b 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 b0fb27dd6465..31b7368da550 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 = ({