[Dashboards]: polish week on date granularity (#16128)

https://github.com/user-attachments/assets/2b7ef230-49e2-4882-9029-6df3d01f5f20
This commit is contained in:
nitin
2025-11-27 19:34:44 +05:30
committed by GitHub
parent da626f70b7
commit 26ec6729c0
9 changed files with 190 additions and 11 deletions
@@ -45,7 +45,7 @@ export const generateDateGroupsInRange = ({
break;
case ObjectRecordGroupByDateGranularity.WEEK:
currentDateCursor.setDate(currentDateCursor.getDate() + 1);
currentDateCursor.setDate(currentDateCursor.getDate() + 7);
break;
case ObjectRecordGroupByDateGranularity.MONTH:
@@ -1,3 +1,4 @@
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { generateGroupByAggregateQuery } from '@/object-record/record-aggregate/utils/generateGroupByAggregateQuery';
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
@@ -24,6 +25,8 @@ export const useGraphWidgetGroupByQuery = ({
configuration: GroupByChartConfiguration;
limit?: number;
}) => {
const { calendarStartDay } = useDateTimeFormat();
const { objectMetadataItem, aggregateField, gqlOperationFilter } =
useGraphWidgetQueryCommon({
objectMetadataItemId,
@@ -63,6 +66,7 @@ export const useGraphWidgetGroupByQuery = ({
chartConfiguration: configuration,
aggregateOperation: aggregateOperation,
limit,
firstDayOfTheWeek: calendarStartDay,
})
: generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
objectMetadataItem,
@@ -71,6 +75,7 @@ export const useGraphWidgetGroupByQuery = ({
| LineChartConfiguration,
aggregateOperation: aggregateOperation,
limit,
firstDayOfTheWeek: calendarStartDay,
});
const variables = {
@@ -16,4 +16,6 @@ exports[`formatDateByGranularity should format date for NONE granularity 1`] = `
exports[`formatDateByGranularity should format date for QUARTER granularity 1`] = `"Q1 2024"`;
exports[`formatDateByGranularity should format date for WEEK granularity 1`] = `"Mar 20, 2024 20 - 26, 2024"`;
exports[`formatDateByGranularity should format date for YEAR granularity 1`] = `"2024"`;
@@ -1,4 +1,5 @@
import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject';
import { CalendarStartDay } from 'twenty-shared';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -101,4 +102,82 @@ describe('buildGroupByFieldObject', () => {
expect(result).toEqual({ status: true });
});
it('should include weekStartDay for WEEK granularity with MONDAY', () => {
const field = {
name: 'createdAt',
type: FieldMetadataType.DATE,
} as any;
const result = buildGroupByFieldObject({
field,
dateGranularity: ObjectRecordGroupByDateGranularity.WEEK,
firstDayOfTheWeek: CalendarStartDay.MONDAY,
});
expect(result).toEqual({
createdAt: {
granularity: ObjectRecordGroupByDateGranularity.WEEK,
weekStartDay: 'MONDAY',
},
});
});
it('should include weekStartDay for WEEK granularity with SUNDAY', () => {
const field = {
name: 'createdAt',
type: FieldMetadataType.DATE,
} as any;
const result = buildGroupByFieldObject({
field,
dateGranularity: ObjectRecordGroupByDateGranularity.WEEK,
firstDayOfTheWeek: CalendarStartDay.SUNDAY,
});
expect(result).toEqual({
createdAt: {
granularity: ObjectRecordGroupByDateGranularity.WEEK,
weekStartDay: 'SUNDAY',
},
});
});
it('should not include weekStartDay for WEEK granularity with SYSTEM', () => {
const field = {
name: 'createdAt',
type: FieldMetadataType.DATE,
} as any;
const result = buildGroupByFieldObject({
field,
dateGranularity: ObjectRecordGroupByDateGranularity.WEEK,
firstDayOfTheWeek: CalendarStartDay.SYSTEM,
});
expect(result).toEqual({
createdAt: {
granularity: ObjectRecordGroupByDateGranularity.WEEK,
},
});
});
it('should not include weekStartDay for non-WEEK granularity', () => {
const field = {
name: 'createdAt',
type: FieldMetadataType.DATE,
} as any;
const result = buildGroupByFieldObject({
field,
dateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
firstDayOfTheWeek: CalendarStartDay.MONDAY,
});
expect(result).toEqual({
createdAt: {
granularity: ObjectRecordGroupByDateGranularity.MONTH,
},
});
});
});
@@ -27,12 +27,14 @@ describe('formatDateByGranularity', () => {
const testCases: {
granularity:
| ObjectRecordGroupByDateGranularity.DAY
| ObjectRecordGroupByDateGranularity.WEEK
| ObjectRecordGroupByDateGranularity.MONTH
| ObjectRecordGroupByDateGranularity.QUARTER
| ObjectRecordGroupByDateGranularity.YEAR
| ObjectRecordGroupByDateGranularity.NONE;
}[] = [
{ granularity: ObjectRecordGroupByDateGranularity.DAY },
{ granularity: ObjectRecordGroupByDateGranularity.WEEK },
{ granularity: ObjectRecordGroupByDateGranularity.MONTH },
{ granularity: ObjectRecordGroupByDateGranularity.QUARTER },
{ granularity: ObjectRecordGroupByDateGranularity.YEAR },
@@ -46,6 +48,53 @@ describe('formatDateByGranularity', () => {
},
);
describe('week calculations', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
afterEach(() => {
jest
.spyOn(Date.prototype, 'toLocaleDateString')
.mockImplementation((_locales, options) => {
if (options?.weekday === 'long') return 'Wednesday';
if (options?.month === 'long' && !isDefined(options?.year))
return 'March';
if (options?.month === 'long' && isDefined(options?.year))
return 'March 2024';
if (options?.month === 'short') return 'Mar 20, 2024';
return '3/20/2024';
});
});
it('should format week within same month', () => {
const date = new Date('2024-05-06');
const result = formatDateByGranularity(
date,
ObjectRecordGroupByDateGranularity.WEEK,
);
expect(result).toBe('May 6 - 12, 2024');
});
it('should format week crossing months', () => {
const date = new Date('2024-05-27');
const result = formatDateByGranularity(
date,
ObjectRecordGroupByDateGranularity.WEEK,
);
expect(result).toBe('May 27 - Jun 2, 2024');
});
it('should format week crossing years', () => {
const date = new Date('2024-12-30');
const result = formatDateByGranularity(
date,
ObjectRecordGroupByDateGranularity.WEEK,
);
expect(result).toBe('Dec 30, 2024 - Jan 5, 2025');
});
});
describe('quarter calculations', () => {
const quarterTestCases: {
date: Date;
@@ -3,7 +3,11 @@ import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/uti
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { CalendarStartDay } from 'twenty-shared';
import {
type FirstDayOfTheWeek,
ObjectRecordGroupByDateGranularity,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -12,10 +16,12 @@ export const buildGroupByFieldObject = ({
field,
subFieldName,
dateGranularity,
firstDayOfTheWeek,
}: {
field: FieldMetadataItem;
subFieldName?: string | null;
dateGranularity?: ObjectRecordGroupByDateGranularity;
firstDayOfTheWeek?: number | null;
}): Record<string, boolean | Record<string, boolean | string>> => {
const isRelation = isFieldRelation(field) || isFieldMorphRelation(field);
const isComposite = isCompositeFieldType(field.type);
@@ -41,11 +47,22 @@ export const buildGroupByFieldObject = ({
}
if (isDateField) {
return {
[field.name]: {
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
},
};
const granularity = dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY;
const result: Record<string, string> = { granularity };
if (
granularity === ObjectRecordGroupByDateGranularity.WEEK &&
isDefined(firstDayOfTheWeek) &&
firstDayOfTheWeek !== CalendarStartDay.SYSTEM
) {
const weekStartDay = CalendarStartDay[
firstDayOfTheWeek
] as FirstDayOfTheWeek;
result.weekStartDay = weekStartDay;
}
return { [field.name]: result };
}
return { [field.name]: true };
@@ -17,12 +17,32 @@ export const formatDateByGranularity = (
month: 'short',
day: 'numeric',
});
case ObjectRecordGroupByDateGranularity.WEEK:
return date.toLocaleDateString(undefined, {
year: 'numeric',
case ObjectRecordGroupByDateGranularity.WEEK: {
const weekStart = new Date(date);
const weekEnd = new Date(date);
weekEnd.setDate(weekEnd.getDate() + 6);
const startMonth = weekStart.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
});
const endMonth = weekEnd.toLocaleDateString(undefined, {
month: 'short',
});
const startDay = weekStart.getDate();
const endDay = weekEnd.getDate();
const startYear = weekStart.getFullYear();
const endYear = weekEnd.getFullYear();
if (startYear !== endYear) {
return `${startMonth} ${startDay}, ${startYear} - ${endMonth} ${endDay}, ${endYear}`;
}
if (startMonth !== endMonth) {
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${endYear}`;
}
return `${startMonth} ${startDay} - ${endDay}, ${endYear}`;
}
case ObjectRecordGroupByDateGranularity.MONTH:
return date.toLocaleDateString(undefined, {
year: 'numeric',
@@ -18,11 +18,13 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
chartConfiguration,
aggregateOperation,
limit,
firstDayOfTheWeek,
}: {
objectMetadataItem: ObjectMetadataItem;
chartConfiguration: BarChartConfiguration | LineChartConfiguration;
aggregateOperation?: string;
limit?: number;
firstDayOfTheWeek?: number;
}) => {
const groupByFieldXId = chartConfiguration.primaryAxisGroupByFieldMetadataId;
@@ -59,6 +61,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
subFieldName: groupBySubFieldNameX,
dateGranularity:
chartConfiguration.primaryAxisDateGranularity ?? undefined,
firstDayOfTheWeek,
}),
);
@@ -69,6 +72,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
subFieldName: groupBySubFieldNameY,
dateGranularity:
chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined,
firstDayOfTheWeek,
}),
);
}
@@ -15,11 +15,13 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
chartConfiguration,
aggregateOperation,
limit,
firstDayOfTheWeek,
}: {
objectMetadataItem: ObjectMetadataItem;
chartConfiguration: PieChartConfiguration;
aggregateOperation?: string;
limit?: number;
firstDayOfTheWeek?: number;
}) => {
const groupByFieldId = chartConfiguration.groupByFieldMetadataId;
const groupBySubFieldName =
@@ -43,6 +45,7 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
field: groupByField,
subFieldName: groupBySubFieldName,
dateGranularity,
firstDayOfTheWeek,
}),
];