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
@@ -1121,7 +1121,7 @@ type AggregateChartConfiguration {
|
||||
aggregateOperation: AggregateOperations!
|
||||
label: String
|
||||
displayDataLabel: Boolean
|
||||
format: String
|
||||
numberFormat: ChartNumberFormat
|
||||
description: String
|
||||
filter: JSON
|
||||
timezone: String
|
||||
@@ -1156,6 +1156,12 @@ enum WidgetConfigurationType {
|
||||
EMAIL_THREAD
|
||||
}
|
||||
|
||||
"""Format used to display the chart value"""
|
||||
enum ChartNumberFormat {
|
||||
SHORT
|
||||
FULL
|
||||
}
|
||||
|
||||
type StandaloneRichTextConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
body: RichTextBody!
|
||||
|
||||
@@ -809,7 +809,7 @@ export interface AggregateChartConfiguration {
|
||||
aggregateOperation: AggregateOperations
|
||||
label?: Scalars['String']
|
||||
displayDataLabel?: Scalars['Boolean']
|
||||
format?: Scalars['String']
|
||||
numberFormat?: ChartNumberFormat
|
||||
description?: Scalars['String']
|
||||
filter?: Scalars['JSON']
|
||||
timezone?: Scalars['String']
|
||||
@@ -822,6 +822,10 @@ export interface AggregateChartConfiguration {
|
||||
|
||||
export type WidgetConfigurationType = 'AGGREGATE_CHART' | 'PIE_CHART' | 'BAR_CHART' | 'LINE_CHART' | 'IFRAME' | 'STANDALONE_RICH_TEXT' | 'VIEW' | 'FIELD' | 'FIELDS' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE' | 'EMAIL_THREAD'
|
||||
|
||||
|
||||
/** Format used to display the chart value */
|
||||
export type ChartNumberFormat = 'SHORT' | 'FULL'
|
||||
|
||||
export interface StandaloneRichTextConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
body: RichTextBody
|
||||
@@ -3817,7 +3821,7 @@ export interface AggregateChartConfigurationGenqlSelection{
|
||||
aggregateOperation?: boolean | number
|
||||
label?: boolean | number
|
||||
displayDataLabel?: boolean | number
|
||||
format?: boolean | number
|
||||
numberFormat?: boolean | number
|
||||
description?: boolean | number
|
||||
filter?: boolean | number
|
||||
timezone?: boolean | number
|
||||
@@ -8824,6 +8828,11 @@ export const enumWidgetConfigurationType = {
|
||||
EMAIL_THREAD: 'EMAIL_THREAD' as const
|
||||
}
|
||||
|
||||
export const enumChartNumberFormat = {
|
||||
SHORT: 'SHORT' as const,
|
||||
FULL: 'FULL' as const
|
||||
}
|
||||
|
||||
export const enumObjectRecordGroupByDateGranularity = {
|
||||
DAY: 'DAY' as const,
|
||||
MONTH: 'MONTH' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,41 @@
|
||||
import { builtinModules } from 'node:module';
|
||||
|
||||
import { lingui } from '@lingui/vite-plugin';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import * as path from 'path';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, type Plugin } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
// twenty-emails bundles its deps (the Node server that consumes it doesn't have
|
||||
// @react-email et al.) but runs in Node. A plain client/lib build under Vite 8
|
||||
// (rolldown) resolves node builtins to browser stubs that throw at runtime
|
||||
// (e.g. object-inspect reading util.inspect.custom), crashing the server on
|
||||
// import. Vite only externalizes builtins as real `require('node:*')` in an
|
||||
// SSR/node build, so this is built with `build.ssr` + `ssr.target: 'node'`,
|
||||
// while `ssr.noExternal: true` keeps the npm deps bundled in. React stays
|
||||
// external as the host app provides it.
|
||||
const EXTERNAL_DEPENDENCIES = ['react', 'react-dom', 'react/jsx-runtime'];
|
||||
|
||||
const nodeBuiltins = new Set([
|
||||
...builtinModules,
|
||||
...builtinModules.map((moduleName) => `node:${moduleName}`),
|
||||
]);
|
||||
|
||||
const externalizeNodeBuiltins = (): Plugin => ({
|
||||
name: 'externalize-node-builtins',
|
||||
enforce: 'pre',
|
||||
resolveId(source) {
|
||||
const specifier = source.startsWith('node:') ? source : `node:${source}`;
|
||||
|
||||
if (nodeBuiltins.has(source) || nodeBuiltins.has(specifier)) {
|
||||
return { id: specifier, external: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/packages/twenty-emails',
|
||||
@@ -16,7 +47,14 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
|
||||
ssr: {
|
||||
target: 'node',
|
||||
noExternal: true,
|
||||
external: EXTERNAL_DEPENDENCIES,
|
||||
},
|
||||
|
||||
plugins: [
|
||||
externalizeNodeBuiltins(),
|
||||
react({
|
||||
plugins: [['@lingui/swc-plugin', {}]],
|
||||
}),
|
||||
@@ -30,6 +68,7 @@ export default defineConfig({
|
||||
|
||||
build: {
|
||||
outDir: './dist',
|
||||
ssr: true,
|
||||
reportCompressedSize: false,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
@@ -49,7 +88,7 @@ export default defineConfig({
|
||||
formats: ['es', 'cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||||
external: EXTERNAL_DEPENDENCIES,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
+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 });
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
aggregateOperation
|
||||
label
|
||||
displayDataLabel
|
||||
format
|
||||
numberFormat
|
||||
description
|
||||
filter
|
||||
prefix
|
||||
|
||||
+1
@@ -158,6 +158,7 @@ export const useGraphWidgetAggregateQuery = ({
|
||||
localeCatalog: dateLocale.localeCatalog,
|
||||
timeFormat,
|
||||
timeZone,
|
||||
numberFormat: configuration.numberFormat ?? undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { getChartNumberFormatLabel } from '@/side-panel/pages/page-layout/utils/getChartNumberFormatLabel';
|
||||
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { MenuItemSelect } from 'twenty-ui-deprecated/navigation';
|
||||
import { ChartNumberFormat } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ChartNumberFormatSelectionDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
|
||||
if (
|
||||
!isWidgetConfigurationOfType(configuration, 'AggregateChartConfiguration')
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentNumberFormat = configuration.numberFormat;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const numberFormatOptions: ChartNumberFormat[] = [
|
||||
ChartNumberFormat.SHORT,
|
||||
ChartNumberFormat.FULL,
|
||||
];
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleSelectNumberFormatOption = (
|
||||
numberFormatOption: ChartNumberFormat,
|
||||
) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
numberFormat: numberFormatOption,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={numberFormatOptions}
|
||||
>
|
||||
{numberFormatOptions.map((option) => (
|
||||
<SelectableListItem
|
||||
key={option}
|
||||
itemId={option}
|
||||
onEnter={() => {
|
||||
handleSelectNumberFormatOption(option);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getChartNumberFormatLabel(option)}
|
||||
selected={currentNumberFormat === option}
|
||||
focused={selectedItemId === option}
|
||||
onClick={() => {
|
||||
handleSelectNumberFormatOption(option);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
+2
-1
@@ -2,6 +2,7 @@ import { CHART_SETTINGS_HEADINGS } from '@/side-panel/pages/page-layout/constant
|
||||
import { CHART_DATA_SOURCE_SETTING } from '@/side-panel/pages/page-layout/constants/settings/ChartDataSourceSetting';
|
||||
import { DATA_DISPLAY_AGGREGATE_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DataDisplayAggregateSetting';
|
||||
import { FILTER_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FilterSetting';
|
||||
import { FORMAT_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FormatSetting';
|
||||
import { PREFIX_SETTING } from '@/side-panel/pages/page-layout/constants/settings/PrefixSetting';
|
||||
import { SUFFIX_SETTING } from '@/side-panel/pages/page-layout/constants/settings/SuffixSetting';
|
||||
import { type ChartSettingsGroup } from '@/side-panel/pages/page-layout/types/ChartSettingsGroup';
|
||||
@@ -17,6 +18,6 @@ export const AGGREGATE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
},
|
||||
{
|
||||
heading: CHART_SETTINGS_HEADINGS.STYLE,
|
||||
items: [PREFIX_SETTING, SUFFIX_SETTING],
|
||||
items: [FORMAT_SETTING, PREFIX_SETTING, SUFFIX_SETTING],
|
||||
},
|
||||
];
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ export const CHART_CONFIGURATION_SETTING_LABELS = {
|
||||
DATE_GRANULARITY_Y: msg`Date granularity`,
|
||||
DATE_GRANULARITY: msg`Date granularity`,
|
||||
SHOW_LEGEND: msg`Legend`,
|
||||
FORMAT: msg`Format`,
|
||||
PREFIX: msg`Prefix`,
|
||||
SUFFIX: msg`Suffix`,
|
||||
CUMULATIVE: msg`Cumulative`,
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { ChartNumberFormatSelectionDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartNumberFormatSelectionDropdownContent';
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/side-panel/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/side-panel/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/side-panel/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { Icon123 } from 'twenty-ui-deprecated/display';
|
||||
|
||||
export const FORMAT_SETTING: ChartSettingsItem = {
|
||||
isBoolean: false,
|
||||
Icon: Icon123,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.FORMAT,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.FORMAT,
|
||||
DropdownContent: ChartNumberFormatSelectionDropdownContent,
|
||||
};
|
||||
+6
@@ -5,6 +5,7 @@ import { CHART_CONFIGURATION_SETTING_IDS } from '@/side-panel/pages/page-layout/
|
||||
import { findChartGroupByFieldMetadataItem } from '@/side-panel/pages/page-layout/utils/findChartGroupByFieldMetadataItem';
|
||||
import { getChartAxisNameDisplayOptions } from '@/side-panel/pages/page-layout/utils/getChartAxisNameDisplayOptions';
|
||||
import { getChartFilterRulesCount } from '@/side-panel/pages/page-layout/utils/getChartFilterRulesCount';
|
||||
import { getChartNumberFormatLabel } from '@/side-panel/pages/page-layout/utils/getChartNumberFormatLabel';
|
||||
import { getDateGranularityLabel } from '@/side-panel/pages/page-layout/utils/getDateGranularityLabel';
|
||||
import { getFieldLabelWithSubField } from '@/side-panel/pages/page-layout/utils/getFieldLabelWithSubField';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
@@ -298,6 +299,11 @@ export const useChartSettingsValues = ({
|
||||
return isBarOrLineChart || isPieChart
|
||||
? (configuration.displayLegend ?? true)
|
||||
: true;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.FORMAT:
|
||||
return configuration.__typename === 'AggregateChartConfiguration' &&
|
||||
isDefined(configuration.numberFormat)
|
||||
? getChartNumberFormatLabel(configuration.numberFormat)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.PREFIX:
|
||||
return configuration.__typename === 'AggregateChartConfiguration'
|
||||
? (configuration.prefix ?? '')
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export enum CHART_CONFIGURATION_SETTING_IDS {
|
||||
DATE_GRANULARITY_Y = 'DATE_GRANULARITY_Y',
|
||||
DATE_GRANULARITY = 'DATE_GRANULARITY',
|
||||
SHOW_LEGEND = 'SHOW_LEGEND',
|
||||
FORMAT = 'FORMAT',
|
||||
PREFIX = 'PREFIX',
|
||||
SUFFIX = 'SUFFIX',
|
||||
CUMULATIVE = 'CUMULATIVE',
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { ChartNumberFormat } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getChartNumberFormatLabel = (
|
||||
chartNumberFormat: ChartNumberFormat,
|
||||
) => {
|
||||
switch (chartNumberFormat) {
|
||||
case ChartNumberFormat.SHORT:
|
||||
return t`Short`;
|
||||
case ChartNumberFormat.FULL:
|
||||
return t`Full`;
|
||||
default:
|
||||
assertUnreachable(chartNumberFormat);
|
||||
}
|
||||
};
|
||||
+4
-3
@@ -26,6 +26,7 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { RatioAggregateConfigDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/ratio-aggregate-config.dto';
|
||||
import { ChartNumberFormat } from 'src/engine/metadata-modules/page-layout-widget/enums/chart-number-format.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
|
||||
@ObjectType('AggregateChartConfiguration')
|
||||
@@ -55,10 +56,10 @@ export class AggregateChartConfigurationDTO implements AggregateChartConfigurati
|
||||
@IsOptional()
|
||||
displayDataLabel?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@Field(() => ChartNumberFormat, { nullable: true })
|
||||
@IsEnum(ChartNumberFormat)
|
||||
@IsOptional()
|
||||
format?: string;
|
||||
numberFormat?: ChartNumberFormat;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
// Controls how a Number widget renders its aggregate value:
|
||||
// SHORT abbreviates (1.3m), FULL shows the complete number with separators.
|
||||
export enum ChartNumberFormat {
|
||||
SHORT = 'SHORT',
|
||||
FULL = 'FULL',
|
||||
}
|
||||
|
||||
registerEnumType(ChartNumberFormat, {
|
||||
name: 'ChartNumberFormat',
|
||||
description: 'Format used to display the chart value',
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { z } from 'zod';
|
||||
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
|
||||
import { BarChartGroupMode } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-group-mode.enum';
|
||||
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
|
||||
import { ChartNumberFormat } from 'src/engine/metadata-modules/page-layout-widget/enums/chart-number-format.enum';
|
||||
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
@@ -64,6 +65,11 @@ const AXIS_NAME_DISPLAY_OPTIONS = Object.values(AxisNameDisplay) as [
|
||||
...AxisNameDisplay[],
|
||||
];
|
||||
|
||||
const CHART_NUMBER_FORMAT_OPTIONS = Object.values(ChartNumberFormat) as [
|
||||
ChartNumberFormat,
|
||||
...ChartNumberFormat[],
|
||||
];
|
||||
|
||||
const BAR_CHART_GROUP_MODE_OPTIONS = Object.values(BarChartGroupMode) as [
|
||||
BarChartGroupMode,
|
||||
...BarChartGroupMode[],
|
||||
@@ -207,6 +213,12 @@ const aggregateChartConfigSchemaBase = z.object({
|
||||
.describe('Aggregation operation: COUNT, SUM, AVG, MIN, MAX, etc.'),
|
||||
label: z.string().optional(),
|
||||
displayDataLabel: displayDataLabelSchema,
|
||||
numberFormat: z
|
||||
.enum(CHART_NUMBER_FORMAT_OPTIONS)
|
||||
.optional()
|
||||
.describe(
|
||||
'Display format for the value: SHORT abbreviates large numbers (1.3m), FULL shows the complete number (1,300,090)',
|
||||
),
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
ratioAggregateConfig: ratioAggregateConfigSchema.optional(),
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ import { type PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-
|
||||
import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
|
||||
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
|
||||
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
|
||||
import { ChartNumberFormat } from 'src/engine/metadata-modules/page-layout-widget/enums/chart-number-format.enum';
|
||||
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
|
||||
@@ -103,7 +104,7 @@ export const TEST_NUMBER_CHART_CONFIG: AggregateChartConfigurationDTO = {
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
label: 'Total Records',
|
||||
description: 'Count of all records',
|
||||
format: '0,0',
|
||||
numberFormat: ChartNumberFormat.SHORT,
|
||||
displayDataLabel: true,
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Page layout widget creation should succeed should create a page layout widget with AGGREGATE_CHART full configuration 1`] = `
|
||||
{
|
||||
@@ -10,8 +10,8 @@ exports[`Page layout widget creation should succeed should create a page layout
|
||||
"displayDataLabel": true,
|
||||
"filter": null,
|
||||
"firstDayOfTheWeek": 1,
|
||||
"format": "0,0",
|
||||
"label": "Total Records",
|
||||
"numberFormat": "SHORT",
|
||||
"prefix": null,
|
||||
"suffix": null,
|
||||
"timezone": "UTC",
|
||||
@@ -43,8 +43,8 @@ exports[`Page layout widget creation should succeed should create a page layout
|
||||
"displayDataLabel": false,
|
||||
"filter": null,
|
||||
"firstDayOfTheWeek": 1,
|
||||
"format": null,
|
||||
"label": null,
|
||||
"numberFormat": null,
|
||||
"prefix": null,
|
||||
"suffix": null,
|
||||
"timezone": "UTC",
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Page layout widget update should succeed from a GRAPH widget should update page layout widget to AGGREGATE_CHART configuration 1`] = `
|
||||
{
|
||||
@@ -10,8 +10,8 @@ exports[`Page layout widget update should succeed from a GRAPH widget should upd
|
||||
"displayDataLabel": true,
|
||||
"filter": null,
|
||||
"firstDayOfTheWeek": 1,
|
||||
"format": "0,0",
|
||||
"label": "Total Records",
|
||||
"numberFormat": "SHORT",
|
||||
"prefix": null,
|
||||
"suffix": null,
|
||||
"timezone": "UTC",
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ export const WIDGET_CONFIGURATION_GQL_FIELDS = `
|
||||
aggregateOperation
|
||||
label
|
||||
displayDataLabel
|
||||
format
|
||||
numberFormat
|
||||
description
|
||||
filter
|
||||
prefix
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@ import { AggregateOperations } from 'twenty-shared/types';
|
||||
|
||||
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
|
||||
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
|
||||
import { ChartNumberFormat } from 'src/engine/metadata-modules/page-layout-widget/enums/chart-number-format.enum';
|
||||
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
@@ -125,7 +126,7 @@ describe('Page layout widget creation should succeed', () => {
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
label: 'Total Records',
|
||||
description: 'Count of all records',
|
||||
format: '0,0',
|
||||
numberFormat: ChartNumberFormat.SHORT,
|
||||
displayDataLabel: true,
|
||||
}),
|
||||
},
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ import { AggregateOperations } from 'twenty-shared/types';
|
||||
|
||||
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
|
||||
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
|
||||
import { ChartNumberFormat } from 'src/engine/metadata-modules/page-layout-widget/enums/chart-number-format.enum';
|
||||
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
@@ -230,7 +231,7 @@ describe('Page layout widget update should succeed', () => {
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
label: 'Total Records',
|
||||
description: 'Count of all records',
|
||||
format: '0,0',
|
||||
numberFormat: ChartNumberFormat.SHORT,
|
||||
displayDataLabel: true,
|
||||
}),
|
||||
},
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Page layout widget restore via bulk update should succeed should create, delete, and restore a widget via bulk update 1`] = `
|
||||
{
|
||||
@@ -30,8 +30,8 @@ exports[`Page layout widget restore via bulk update should succeed should create
|
||||
"displayDataLabel": true,
|
||||
"filter": null,
|
||||
"firstDayOfTheWeek": 1,
|
||||
"format": null,
|
||||
"label": null,
|
||||
"numberFormat": null,
|
||||
"prefix": null,
|
||||
"suffix": null,
|
||||
"timezone": "UTC",
|
||||
@@ -109,8 +109,8 @@ exports[`Page layout widget restore via bulk update should succeed should handle
|
||||
"displayDataLabel": true,
|
||||
"filter": null,
|
||||
"firstDayOfTheWeek": 1,
|
||||
"format": null,
|
||||
"label": null,
|
||||
"numberFormat": null,
|
||||
"prefix": null,
|
||||
"suffix": null,
|
||||
"timezone": "UTC",
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ type BaseChartConfiguration = {
|
||||
export type AggregateChartConfiguration = BaseChartConfiguration & {
|
||||
configurationType: 'AGGREGATE_CHART';
|
||||
label?: string;
|
||||
format?: string;
|
||||
numberFormat?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
ratioAggregateConfig?: RatioAggregateConfig;
|
||||
|
||||
Reference in New Issue
Block a user