[DASHBOARDS] Use a time scale when the primary axis is a date on the Bar Chart (#15932)

Closes https://github.com/twentyhq/core-team-issues/issues/1891

Create empty buckets according to the date granularity

Video QA:


https://github.com/user-attachments/assets/86c0f817-35b3-4bab-b093-d11491684b82

Note: We would also need to create empty buckets for cyclic
granularities (DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR).
TODO:
- Always order the cyclic granularities Monday -> Sunday (take
firstDayOfTheWeek into account), January -> December, Q1 -> Q4. For now
they are returned by the backend in alphabetical order, which doesn't
make much sense
- Remove the translation into the user's locale of these granularities
from the backend because otherwise we can't reconstruct the missing days
or month in the frontend since they will be translated

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Raphaël Bosi
2025-11-21 10:24:19 +01:00
committed by GitHub
parent c737042209
commit fdb4e4ef10
24 changed files with 567 additions and 13 deletions
@@ -27,7 +27,11 @@ import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { SidePanelInformationBanner } from 'twenty-ui/display';
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
import {
FieldMetadataType,
GraphType,
type PageLayoutWidget,
} from '~/generated/graphql';
const StyledSidePanelInformationBanner = styled(SidePanelInformationBanner)`
margin-top: ${({ theme }) => theme.spacing(2)};
@@ -118,6 +122,20 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
.map((item) => item.id),
);
const primaryAxisFieldMetadataId =
configuration.__typename === 'BarChartConfiguration' ||
configuration.__typename === 'LineChartConfiguration'
? configuration.primaryAxisGroupByFieldMetadataId
: null;
const primaryAxisField = objectMetadataItem?.fields?.find(
(field) => field.id === primaryAxisFieldMetadataId,
);
const isPrimaryAxisDate =
primaryAxisField?.type === FieldMetadataType.DATE ||
primaryAxisField?.type === FieldMetadataType.DATE_TIME;
return (
<CommandMenuList commandGroups={[]} selectableItemIds={visibleItemIds}>
<ChartTypeSelectionSection
@@ -128,9 +146,15 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
<StyledSidePanelInformationBanner
message={
currentGraphType === GraphType.LINE
? t`Max ${LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS} data points per chart. Consider adding a filter`
: t`Max ${BAR_CHART_MAXIMUM_NUMBER_OF_BARS} bars per chart. Consider adding a filter`
? t`Undisplayed data: max ${LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS} data points per chart.`
: t`Undisplayed data: max ${BAR_CHART_MAXIMUM_NUMBER_OF_BARS} bars per chart.`
}
tooltipMessage={
isPrimaryAxisDate
? t`Consider adding a filter or changing the date granularity to display more data.`
: t`Consider adding a filter to display more data.`
}
variant="warning"
/>
)}
{chartSettings.map((group) => {
@@ -0,0 +1,8 @@
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
export const BAR_CHART_DATE_GRANULARITIES_WITHOUT_GAP_FILLING = new Set([
ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK,
ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR,
ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR,
ObjectRecordGroupByDateGranularity.NONE,
]);
@@ -1,9 +1,10 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { transformGroupByDataToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData';
import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery';
import { transformGroupByDataToBarChartData } from '@/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData';
import { useMemo } from 'react';
import { type BarChartConfiguration } from '~/generated/graphql';
@@ -26,6 +27,9 @@ type UseGraphBarChartWidgetDataResult = {
hasTooManyGroups: boolean;
};
// TODO: Remove this once backend returns total group count
const EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS = 1;
export const useGraphBarChartWidgetData = ({
objectMetadataItemId,
configuration,
@@ -42,6 +46,8 @@ export const useGraphBarChartWidgetData = ({
} = useGraphWidgetGroupByQuery({
objectMetadataItemId,
configuration,
limit:
BAR_CHART_MAXIMUM_NUMBER_OF_BARS + EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
});
const transformedData = useMemo(
@@ -0,0 +1,6 @@
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
export type FillDateGapsResult = {
data: GroupByRawResult[];
wasTruncated: boolean;
};
@@ -0,0 +1,96 @@
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { fillDateGapsInBarChartData } from '../fillDateGapsInBarChartData';
describe('fillDateGapsInBarChartData', () => {
describe('one-dimensional data', () => {
it('fills gaps in date data with zero values', () => {
const data = [
{
groupByDimensionValues: ['2024-01-01T00:00:00.000Z'],
count: 5,
},
{
groupByDimensionValues: ['2024-01-03T00:00:00.000Z'],
count: 3,
},
];
const result = fillDateGapsInBarChartData({
data,
keys: ['count'],
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
});
expect(result.data).toHaveLength(3);
expect(result.data[0]).toEqual({
groupByDimensionValues: ['2024-01-01T00:00:00.000Z'],
count: 5,
});
expect(result.data[1]).toEqual({
groupByDimensionValues: ['2024-01-02T00:00:00.000Z'],
count: 0,
});
expect(result.data[2]).toEqual({
groupByDimensionValues: ['2024-01-03T00:00:00.000Z'],
count: 3,
});
expect(result.wasTruncated).toBe(false);
});
it('returns empty data unchanged', () => {
const result = fillDateGapsInBarChartData({
data: [],
keys: ['count'],
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
});
expect(result.data).toEqual([]);
expect(result.wasTruncated).toBe(false);
});
});
describe('two-dimensional data', () => {
it('fills gaps for all second dimension values', () => {
const data = [
{
groupByDimensionValues: ['2024-01-01T00:00:00.000Z', 'A'],
count: 5,
},
{
groupByDimensionValues: ['2024-01-03T00:00:00.000Z', 'A'],
count: 3,
},
{
groupByDimensionValues: ['2024-01-01T00:00:00.000Z', 'B'],
count: 2,
},
];
const result = fillDateGapsInBarChartData({
data,
keys: ['count'],
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
hasSecondDimension: true,
});
expect(result.data).toHaveLength(6);
expect(
result.data.filter((r) => r.groupByDimensionValues[1] === 'A'),
).toHaveLength(3);
expect(
result.data.filter((r) => r.groupByDimensionValues[1] === 'B'),
).toHaveLength(3);
expect(
result.data.find(
(r) =>
r.groupByDimensionValues[0] === '2024-01-02T00:00:00.000Z' &&
r.groupByDimensionValues[1] === 'A',
),
).toEqual({
groupByDimensionValues: ['2024-01-02T00:00:00.000Z', 'A'],
count: 0,
});
expect(result.wasTruncated).toBe(false);
});
});
});
@@ -0,0 +1,71 @@
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { generateDateGroupsInRange } from '../generateDateGroupsInRange';
describe('generateDateGroupsInRange', () => {
it('generates daily date groups', () => {
const result = generateDateGroupsInRange({
startDate: new Date('2024-01-01'),
endDate: new Date('2024-01-07'),
granularity: ObjectRecordGroupByDateGranularity.DAY,
});
expect(result.dates).toHaveLength(7);
expect(result.dates[0]).toEqual(new Date('2024-01-01'));
expect(result.dates[6]).toEqual(new Date('2024-01-07'));
expect(result.wasTruncated).toBe(false);
});
it('generates monthly date groups', () => {
const result = generateDateGroupsInRange({
startDate: new Date('2024-01-01'),
endDate: new Date('2024-06-01'),
granularity: ObjectRecordGroupByDateGranularity.MONTH,
});
expect(result.dates).toHaveLength(6);
expect(result.dates[0]).toEqual(new Date('2024-01-01'));
expect(result.dates[5]).toEqual(new Date('2024-06-01'));
expect(result.wasTruncated).toBe(false);
});
it('generates quarterly date groups', () => {
const result = generateDateGroupsInRange({
startDate: new Date('2024-01-01'),
endDate: new Date('2024-12-31'),
granularity: ObjectRecordGroupByDateGranularity.QUARTER,
});
expect(result.dates).toHaveLength(4);
expect(result.dates[0]).toEqual(new Date('2024-01-01'));
expect(result.dates[3]).toEqual(new Date('2024-10-01'));
expect(result.wasTruncated).toBe(false);
});
it('generates yearly date groups', () => {
const result = generateDateGroupsInRange({
startDate: new Date('2020-01-01'),
endDate: new Date('2024-12-31'),
granularity: ObjectRecordGroupByDateGranularity.YEAR,
});
expect(result.dates).toHaveLength(5);
expect(result.dates[0]).toEqual(new Date('2020-01-01'));
expect(result.dates[4]).toEqual(new Date('2024-01-01'));
expect(result.wasTruncated).toBe(false);
});
it('truncates when exceeding maximum number of bars', () => {
const result = generateDateGroupsInRange({
startDate: new Date('2024-01-01'),
endDate: new Date('2025-12-31'),
granularity: ObjectRecordGroupByDateGranularity.DAY,
});
expect(result.dates.length).toBeLessThanOrEqual(
BAR_CHART_MAXIMUM_NUMBER_OF_BARS,
);
expect(result.dates.length).toBe(BAR_CHART_MAXIMUM_NUMBER_OF_BARS);
expect(result.wasTruncated).toBe(true);
});
});
@@ -0,0 +1,20 @@
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
export type DimensionValue = string | Date | number | null;
export const createEmptyDateGroup = (
dimensionValues: DimensionValue[],
keys: string[],
): GroupByRawResult => {
const newItem: GroupByRawResult = {
groupByDimensionValues: dimensionValues.map((value) =>
value instanceof Date ? value.toISOString() : value,
),
};
for (const key of keys) {
newItem[key] = 0;
}
return newItem;
};
@@ -0,0 +1,42 @@
import { BAR_CHART_DATE_GRANULARITIES_WITHOUT_GAP_FILLING } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartDateGranularitiesWithoutGapFilling.constant';
import { fillDateGapsInOneDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInOneDimensionalBarChartData';
import { fillDateGapsInTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInTwoDimensionalBarChartData';
import { type SupportedDateGranularity } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
type FillDateGapsParams = {
data: GroupByRawResult[];
keys: string[];
dateGranularity: ObjectRecordGroupByDateGranularity;
hasSecondDimension?: boolean;
};
export const fillDateGapsInBarChartData = ({
data,
keys,
dateGranularity,
hasSecondDimension = false,
}: FillDateGapsParams): { data: GroupByRawResult[]; wasTruncated: boolean } => {
if (data.length === 0) {
return { data, wasTruncated: false };
}
if (BAR_CHART_DATE_GRANULARITIES_WITHOUT_GAP_FILLING.has(dateGranularity)) {
return { data, wasTruncated: false };
}
if (hasSecondDimension) {
return fillDateGapsInTwoDimensionalBarChartData({
data,
keys,
dateGranularity: dateGranularity as SupportedDateGranularity,
});
}
return fillDateGapsInOneDimensionalBarChartData({
data,
keys,
dateGranularity: dateGranularity as SupportedDateGranularity,
});
};
@@ -0,0 +1,60 @@
import { type FillDateGapsResult } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/FillDateGapsResult';
import { createEmptyDateGroup } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/createEmptyDateGroup';
import {
getDateGroupsFromData,
type SupportedDateGranularity,
} from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { isDefined } from 'twenty-shared/utils';
type OneDimensionalFillParams = {
data: GroupByRawResult[];
keys: string[];
dateGranularity: SupportedDateGranularity;
};
export const fillDateGapsInOneDimensionalBarChartData = ({
data,
keys,
dateGranularity,
}: OneDimensionalFillParams): FillDateGapsResult => {
const existingDateGroupsMap = new Map<string, GroupByRawResult>();
const parsedDates: Date[] = [];
for (const item of data) {
const dateValue = item.groupByDimensionValues?.[0];
if (!isDefined(dateValue)) {
continue;
}
const parsedDate = new Date(String(dateValue));
if (isNaN(parsedDate.getTime())) {
continue;
}
parsedDates.push(parsedDate);
existingDateGroupsMap.set(parsedDate.toISOString(), item);
}
if (parsedDates.length === 0) {
return { data, wasTruncated: false };
}
const { dates: allDates, wasTruncated } = getDateGroupsFromData(
parsedDates,
dateGranularity,
);
const filledData = allDates.map((date) => {
const key = date.toISOString();
const existingDateGroup = existingDateGroupsMap.get(key);
return isDefined(existingDateGroup)
? existingDateGroup
: createEmptyDateGroup([date], keys);
});
return { data: filledData, wasTruncated };
};
@@ -0,0 +1,72 @@
import { type FillDateGapsResult } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/FillDateGapsResult';
import {
createEmptyDateGroup,
type DimensionValue,
} from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/createEmptyDateGroup';
import {
getDateGroupsFromData,
type SupportedDateGranularity,
} from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { isDefined } from 'twenty-shared/utils';
type TwoDimensionalFillParams = {
data: GroupByRawResult[];
keys: string[];
dateGranularity: SupportedDateGranularity;
};
export const fillDateGapsInTwoDimensionalBarChartData = ({
data,
keys,
dateGranularity,
}: TwoDimensionalFillParams): FillDateGapsResult => {
const existingDateGroupsMap = new Map<string, GroupByRawResult>();
const parsedDates: Date[] = [];
const uniqueSecondDimensionValues = new Set<DimensionValue>();
for (const item of data) {
const dateValue = item.groupByDimensionValues?.[0];
if (!isDefined(dateValue)) {
continue;
}
const parsedDate = new Date(String(dateValue));
if (isNaN(parsedDate.getTime())) {
continue;
}
parsedDates.push(parsedDate);
const secondDimensionValue = (item.groupByDimensionValues?.[1] ??
null) as DimensionValue;
uniqueSecondDimensionValues.add(secondDimensionValue);
const key = `${parsedDate.toISOString()}_${String(secondDimensionValue)}`;
existingDateGroupsMap.set(key, item);
}
if (parsedDates.length === 0) {
return { data, wasTruncated: false };
}
const { dates: allDates, wasTruncated } = getDateGroupsFromData(
parsedDates,
dateGranularity,
);
const filledData = allDates.flatMap((date) =>
Array.from(uniqueSecondDimensionValues).map((secondDimensionValue) => {
const key = `${date.toISOString()}_${String(secondDimensionValue)}`;
const existingDateGroup = existingDateGroupsMap.get(key);
return isDefined(existingDateGroup)
? existingDateGroup
: createEmptyDateGroup([date, secondDimensionValue], keys);
}),
);
return { data: filledData, wasTruncated };
};
@@ -0,0 +1,64 @@
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
type GenerateDateRangeParams = {
startDate: Date;
endDate: Date;
granularity:
| ObjectRecordGroupByDateGranularity.DAY
| ObjectRecordGroupByDateGranularity.MONTH
| ObjectRecordGroupByDateGranularity.QUARTER
| ObjectRecordGroupByDateGranularity.YEAR;
};
type GenerateDateRangeResult = {
dates: Date[];
wasTruncated: boolean;
};
export const generateDateGroupsInRange = ({
startDate,
endDate,
granularity,
}: GenerateDateRangeParams): GenerateDateRangeResult => {
const dates: Date[] = [];
let iterations = 0;
let wasTruncated = false;
let currentDateCursor = new Date(startDate);
while (currentDateCursor <= endDate) {
if (iterations >= BAR_CHART_MAXIMUM_NUMBER_OF_BARS) {
wasTruncated = true;
break;
}
dates.push(new Date(currentDateCursor));
iterations++;
switch (granularity) {
case ObjectRecordGroupByDateGranularity.DAY:
currentDateCursor.setDate(currentDateCursor.getDate() + 1);
break;
case ObjectRecordGroupByDateGranularity.MONTH:
currentDateCursor.setMonth(currentDateCursor.getMonth() + 1);
break;
case ObjectRecordGroupByDateGranularity.QUARTER:
currentDateCursor.setMonth(currentDateCursor.getMonth() + 3);
break;
case ObjectRecordGroupByDateGranularity.YEAR:
currentDateCursor.setFullYear(currentDateCursor.getFullYear() + 1);
break;
default:
assertUnreachable(granularity);
}
}
return { dates, wasTruncated };
};
@@ -0,0 +1,25 @@
import { generateDateGroupsInRange } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/generateDateGroupsInRange';
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
export type SupportedDateGranularity =
| ObjectRecordGroupByDateGranularity.DAY
| ObjectRecordGroupByDateGranularity.MONTH
| ObjectRecordGroupByDateGranularity.QUARTER
| ObjectRecordGroupByDateGranularity.YEAR;
export const getDateGroupsFromData = (
parsedDates: Date[],
dateGranularity: SupportedDateGranularity,
): { dates: Date[]; wasTruncated: boolean } => {
const timestamps = parsedDates.map((date) => date.getTime());
const minDate = new Date(Math.min(...timestamps));
const maxDate = new Date(Math.max(...timestamps));
const result = generateDateGroupsInRange({
startDate: minDate,
endDate: maxDate,
granularity: dateGranularity,
});
return { dates: result.dates, wasTruncated: result.wasTruncated };
};
@@ -3,18 +3,21 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { getGroupByQueryName } from '@/page-layout/utils/getGroupByQueryName';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { fillDateGapsInBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData';
import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData';
import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData';
import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData';
import { isDefined } from 'twenty-shared/utils';
import { GraphType } from '~/generated-metadata/graphql';
import {
AxisNameDisplay,
FieldMetadataType,
type BarChartConfiguration,
} from '~/generated/graphql';
@@ -140,9 +143,27 @@ export const transformGroupByDataToBarChartData = ({
const showDataLabels = configuration.displayDataLabel ?? false;
const isDateField =
groupByFieldX.type === FieldMetadataType.DATE ||
groupByFieldX.type === FieldMetadataType.DATE_TIME;
const dateGapFillResult = isDateField
? fillDateGapsInBarChartData({
data: filteredResults,
keys: [aggregateField.name],
dateGranularity:
configuration.primaryAxisDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY,
hasSecondDimension: isDefined(groupByFieldY),
})
: { data: filteredResults, wasTruncated: false };
const filteredResultsWithDateGaps = dateGapFillResult.data;
const dateRangeWasTruncated = dateGapFillResult.wasTruncated;
const baseResult = isDefined(groupByFieldY)
? transformTwoDimensionalGroupByToBarChartData({
rawResults: filteredResults,
rawResults: filteredResultsWithDateGaps,
groupByFieldX,
groupByFieldY,
aggregateField,
@@ -152,7 +173,7 @@ export const transformGroupByDataToBarChartData = ({
primaryAxisSubFieldName,
})
: transformOneDimensionalGroupByToBarChartData({
rawResults: filteredResults,
rawResults: filteredResultsWithDateGaps,
groupByFieldX,
aggregateField,
configuration,
@@ -172,5 +193,6 @@ export const transformGroupByDataToBarChartData = ({
yAxisLabel,
showDataLabels,
layout,
hasTooManyGroups: baseResult.hasTooManyGroups || dateRangeWasTruncated,
};
};
@@ -4,13 +4,13 @@ import { type ExtendedAggregateOperations } from '@/object-record/record-table/t
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortBarChartDataBySecondaryDimensionSum';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
import { getSortedKeys } from '@/page-layout/widgets/graph/utils/getSortedKeys';
import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/utils/sortBarChartDataBySecondaryDimensionSum';
import { isDefined } from 'twenty-shared/utils';
import {
BarChartGroupMode,
@@ -12,9 +12,11 @@ import { isDefined } from 'twenty-shared/utils';
export const useGraphWidgetGroupByQuery = ({
objectMetadataItemId,
configuration,
limit = DEFAULT_NUMBER_OF_GROUPS_LIMIT,
}: {
objectMetadataItemId: string;
configuration: GroupByChartConfiguration;
limit?: number;
}) => {
const { objectMetadataItem, aggregateField, gqlOperationFilter } =
useGraphWidgetQueryCommon({
@@ -48,12 +50,12 @@ export const useGraphWidgetGroupByQuery = ({
objectMetadataItem,
chartConfiguration: configuration,
aggregateOperation: aggregateOperation,
limit,
});
const variables = {
...groupByQueryVariables,
filter: gqlOperationFilter,
limit: DEFAULT_NUMBER_OF_GROUPS_LIMIT,
};
const query = generateGroupByQuery({
@@ -7,12 +7,14 @@ exports[`generateGroupByQuery should generate valid GraphQL query for empty aggr
$filter: PersonFilterInput
$orderBy: [PersonOrderByWithGroupByInput!]
$viewId: UUID
$limit: Int
) {
peopleGroupBy(
groupBy: $groupBy
filter: $filter
orderBy: $orderBy
viewId: $viewId
limit: $limit
) {
groupByDimensionValues
}
@@ -27,12 +29,14 @@ exports[`generateGroupByQuery should generate valid GraphQL query for multiple a
$filter: OpportunityFilterInput
$orderBy: [OpportunityOrderByWithGroupByInput!]
$viewId: UUID
$limit: Int
) {
opportunitiesGroupBy(
groupBy: $groupBy
filter: $filter
orderBy: $orderBy
viewId: $viewId
limit: $limit
) {
groupByDimensionValues
totalCount
@@ -50,12 +54,14 @@ exports[`generateGroupByQuery should generate valid GraphQL query for single agg
$filter: OpportunityFilterInput
$orderBy: [OpportunityOrderByWithGroupByInput!]
$viewId: UUID
$limit: Int
) {
opportunitiesGroupBy(
groupBy: $groupBy
filter: $filter
orderBy: $orderBy
viewId: $viewId
limit: $limit
) {
groupByDimensionValues
sumAmountAmountMicros
@@ -21,12 +21,14 @@ export const generateGroupByQuery = ({
$filter: ${capitalizedSingular}FilterInput
$orderBy: [${capitalizedSingular}OrderByWithGroupByInput!]
$viewId: UUID
$limit: Int
) {
${queryFieldName}(
groupBy: $groupBy
filter: $filter
orderBy: $orderBy
viewId: $viewId
limit: $limit
) {
groupByDimensionValues${aggregateOperations.length > 0 ? `\n ${aggregateOperations.join('\n ')}` : ''}
}
@@ -14,10 +14,12 @@ export const generateGroupByQueryVariablesFromChartConfiguration = ({
objectMetadataItem,
chartConfiguration,
aggregateOperation,
limit,
}: {
objectMetadataItem: ObjectMetadataItem;
chartConfiguration: GroupByChartConfiguration;
aggregateOperation?: string;
limit?: number;
}) => {
const groupByFieldXId = chartConfiguration.primaryAxisGroupByFieldMetadataId;
@@ -107,5 +109,6 @@ export const generateGroupByQueryVariablesFromChartConfiguration = ({
return {
groupBy,
...(orderBy.length > 0 && { orderBy }),
...(isDefined(limit) && { limit }),
};
};
@@ -64,6 +64,7 @@ export interface GroupByResolverArgs<Filter = ObjectRecordFilter> {
viewId?: string;
orderBy?: OrderByWithGroupBy;
orderByForRecords?: ObjectRecordOrderBy;
limit?: number;
}
export interface UpdateOneResolverArgs<
@@ -1,5 +1,9 @@
import styled from '@emotion/styled';
import { IconInfoCircle } from '../../icon/components/TablerIcons';
import {
IconAlertTriangle,
IconInfoCircle,
} from '../../icon/components/TablerIcons';
import { AppTooltip } from '../../tooltip/AppTooltip';
const StyledBanner = styled.div`
align-items: center;
@@ -37,18 +41,38 @@ const StyledMessage = styled.p`
export type SidePanelInformationBannerProps = {
message: string;
className?: string;
variant?: 'default' | 'warning';
tooltipMessage?: string;
};
export const SidePanelInformationBanner = ({
message,
className,
variant = 'default',
tooltipMessage,
}: SidePanelInformationBannerProps) => {
const tooltipId = 'side-panel-information-banner-tooltip';
return (
<StyledBanner className={className}>
<StyledBanner
className={className}
data-tooltip-id={tooltipMessage ? tooltipId : undefined}
>
<StyledIconContainer>
<IconInfoCircle size={16} />
{variant === 'default' ? (
<IconInfoCircle size={16} />
) : (
<IconAlertTriangle size={16} />
)}
</StyledIconContainer>
<StyledMessage>{message}</StyledMessage>
{tooltipMessage && (
<AppTooltip
anchorSelect={`[data-tooltip-id='${tooltipId}']`}
content={tooltipMessage}
place="bottom"
/>
)}
</StyledBanner>
);
};