feat(page-layout): add number format option to aggregate chart widget (#21521)
## Context Closes #21522 Large values in the dashboard **Number** widget are always abbreviated (e.g. `1300090` → `1.3m`) with no way to display the full number. Following a discussion with the core team who were interested in this feature (https://discordapp.com/channels/1130383047699738754/1509604545381142649) , this adds a **Format** option in the **Style** section of the Number (aggregate chart) widget, letting users choose between **Short** (abbreviated, current behavior) and **Full** (complete number with thousand separators). Only the displayed value of the Number widget is affected — axes, labels and tooltips of other chart types are intentionally left untouched. ## What's inside **Server** - New `ChartNumberFormat` GraphQL enum (`SHORT` / `FULL`), following the `AxisNameDisplay` pattern - The existing — and previously unused — `format` field on `AggregateChartConfigurationDTO` is now typed with this enum and validated with `@IsEnum` - The dashboard AI tool schema (`widget.schema.ts`) accepts the new `format` option - Regenerated GraphQL types and the `twenty-client-sdk` metadata client to reflect the enum **Front** - New **Format** setting in the Style section of the Number widget settings, with a Short/Full selection dropdown (same pattern as the Axis name setting) - `transformAggregateRawValueIntoAggregateDisplayValue` takes an optional `numberFormat`: - `FULL` → full number via `formatNumber` (currency values keep up to 2 decimals) - `SHORT` → abbreviated via `formatToShortNumber` - not set → behavior unchanged (currency short, number full), so existing widgets and the record table/board footers render exactly as before ## Screenshots | Full UI Look | <img width="1917" height="955" alt="Twenty_Showcas_FullShort" src="https://github.com/user-attachments/assets/05d05779-395d-4e1a-8ff0-964f6fbef182" /> | Menu UI Look | <img width="291" height="308" alt="Screenshot_2" src="https://github.com/user-attachments/assets/82b5a1de-32fe-46ec-a9b8-add11ab4c6cd" /> ## Tests - Extended `transformAggregateRawValueIntoAggregateDisplayValue` unit tests with SHORT/FULL cases for currency and number fields - Updated the page-layout-widget creation/update integration tests and snapshots to use `ChartNumberFormat.SHORT` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21521?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
f869ce87b1
commit
fefb6cdb94
+74
-1
@@ -6,7 +6,10 @@ import { AggregateOperations } from '@/object-record/record-table/constants/Aggr
|
||||
import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import { findByProperty } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
ChartNumberFormat,
|
||||
FieldMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
|
||||
|
||||
describe('transformAggregateRawValueIntoAggregateDisplayValue', () => {
|
||||
@@ -120,6 +123,76 @@ describe('transformAggregateRawValueIntoAggregateDisplayValue', () => {
|
||||
).toBe('100,000,000');
|
||||
});
|
||||
|
||||
it('should return full currency formatted value with FULL number format', () => {
|
||||
const mockCurrencyFieldMetadataItem = {
|
||||
...mockCompanyEmployeesFieldMetadataItem,
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
expect(
|
||||
transformAggregateRawValueIntoAggregateDisplayValue({
|
||||
aggregateFieldMetadataItem: mockCurrencyFieldMetadataItem,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateRawValue: 230440000000,
|
||||
dateFormat: DateFormat.DAY_FIRST,
|
||||
timeFormat: TimeFormat.HOUR_24,
|
||||
localeCatalog: enUS,
|
||||
timeZone: 'UTC',
|
||||
numberFormat: ChartNumberFormat.FULL,
|
||||
}),
|
||||
).toBe('230,440');
|
||||
});
|
||||
|
||||
it('should return short currency formatted value with SHORT number format', () => {
|
||||
const mockCurrencyFieldMetadataItem = {
|
||||
...mockCompanyEmployeesFieldMetadataItem,
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
expect(
|
||||
transformAggregateRawValueIntoAggregateDisplayValue({
|
||||
aggregateFieldMetadataItem: mockCurrencyFieldMetadataItem,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateRawValue: 230440000000,
|
||||
dateFormat: DateFormat.DAY_FIRST,
|
||||
timeFormat: TimeFormat.HOUR_24,
|
||||
localeCatalog: enUS,
|
||||
timeZone: 'UTC',
|
||||
numberFormat: ChartNumberFormat.SHORT,
|
||||
}),
|
||||
).toBe('230.4k');
|
||||
});
|
||||
|
||||
it('should return short number formatted value with SHORT number format', () => {
|
||||
expect(
|
||||
transformAggregateRawValueIntoAggregateDisplayValue({
|
||||
aggregateFieldMetadataItem: mockCompanyEmployeesFieldMetadataItem,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateRawValue: 100000000,
|
||||
dateFormat: DateFormat.DAY_FIRST,
|
||||
timeFormat: TimeFormat.HOUR_24,
|
||||
localeCatalog: enUS,
|
||||
timeZone: 'UTC',
|
||||
numberFormat: ChartNumberFormat.SHORT,
|
||||
}),
|
||||
).toBe('100m');
|
||||
});
|
||||
|
||||
it('should return full number formatted value with FULL number format', () => {
|
||||
expect(
|
||||
transformAggregateRawValueIntoAggregateDisplayValue({
|
||||
aggregateFieldMetadataItem: mockCompanyEmployeesFieldMetadataItem,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateRawValue: 100000000,
|
||||
dateFormat: DateFormat.DAY_FIRST,
|
||||
timeFormat: TimeFormat.HOUR_24,
|
||||
localeCatalog: enUS,
|
||||
timeZone: 'UTC',
|
||||
numberFormat: ChartNumberFormat.FULL,
|
||||
}),
|
||||
).toBe('100,000,000');
|
||||
});
|
||||
|
||||
it('should return correct DATE formatted value', () => {
|
||||
const mockDateFieldMetadataItem = {
|
||||
...mockCompanyEmployeesFieldMetadataItem,
|
||||
|
||||
+15
-4
@@ -7,7 +7,10 @@ import { type ExtendedAggregateOperations } from '@/object-record/record-table/t
|
||||
|
||||
import { FieldMetadataType, type Nullable } from 'twenty-shared/types';
|
||||
import { formatToShortNumber, isDefined } from 'twenty-shared/utils';
|
||||
import { type AggregateOperations } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
type AggregateOperations,
|
||||
ChartNumberFormat,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { formatNumber } from '~/utils/format/formatNumber';
|
||||
import { formatDateString } from '~/utils/string/formatDateString';
|
||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||
@@ -20,6 +23,7 @@ export const transformAggregateRawValueIntoAggregateDisplayValue = ({
|
||||
timeFormat,
|
||||
timeZone,
|
||||
localeCatalog,
|
||||
numberFormat,
|
||||
}: {
|
||||
aggregateFieldMetadataItem: Nullable<FieldMetadataItem>;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
@@ -28,6 +32,7 @@ export const transformAggregateRawValueIntoAggregateDisplayValue = ({
|
||||
timeFormat: TimeFormat;
|
||||
timeZone: string;
|
||||
localeCatalog: Locale;
|
||||
numberFormat?: ChartNumberFormat;
|
||||
}): string => {
|
||||
if (!isDefined(aggregateRawValue)) {
|
||||
return '-';
|
||||
@@ -48,14 +53,20 @@ export const transformAggregateRawValueIntoAggregateDisplayValue = ({
|
||||
} else {
|
||||
switch (aggregateFieldMetadataItem.type) {
|
||||
case FieldMetadataType.CURRENCY: {
|
||||
return formatToShortNumber(Number(aggregateRawValue) / 1_000_000);
|
||||
const amount = Number(aggregateRawValue) / 1_000_000;
|
||||
return numberFormat === ChartNumberFormat.FULL
|
||||
? formatNumber(amount, { decimals: 2 })
|
||||
: formatToShortNumber(amount);
|
||||
}
|
||||
|
||||
case FieldMetadataType.NUMBER: {
|
||||
const castedValue = Number(aggregateRawValue);
|
||||
const { decimals, type } = aggregateFieldMetadataItem.settings ?? {};
|
||||
return type === 'percentage'
|
||||
? `${formatNumber(castedValue * 100, { decimals })}%`
|
||||
if (type === 'percentage') {
|
||||
return `${formatNumber(castedValue * 100, { decimals })}%`;
|
||||
}
|
||||
return numberFormat === ChartNumberFormat.SHORT
|
||||
? formatToShortNumber(castedValue)
|
||||
: formatNumber(castedValue, { decimals });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user