From 1739ee05951fc8c6432ab5521c319d46f9992c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:27:47 +0100 Subject: [PATCH] Add date granularity and timezone and first day of the week to graphs (#15543) - Allow users to choose the date granularity of the x axis and the group by of the y axis on a bar chart - Display those options conditionally - Store timezones in graphs: each graph has its own timezone, defaults to the user timezone. There will be a picker in the v2 to choose the timezone. For now the timezone is not used by the backend, but it will be used in filters and in group by queries. - Store first day of the week https://github.com/user-attachments/assets/66a5d156-dd93-4ebe-8c8f-d172f93e25be --- .../src/generated-metadata/graphql.ts | 27 ++ .../twenty-front/src/generated/graphql.ts | 44 ++- .../page-layout/components/ChartSettings.tsx | 8 + ...ateGranularitySelectionDropdownContent.tsx | 143 ++++++++++ ...teGranularityXSelectionDropdownContent.tsx | 5 + ...teGranularityYSelectionDropdownContent.tsx | 5 + .../constants/LineChartSettings.ts | 4 + .../page-layout/constants/PieChartSettings.ts | 2 + .../ChartConfigurationSettingLabels.ts | 3 + .../settings/DateGranularitySetting.ts | 13 + .../settings/DateGranularityXSetting.ts | 14 + .../settings/DateGranularityYSetting.ts | 14 + .../hooks/useChartSettingsValues.ts | 18 ++ .../types/ChartConfigurationSettingIds.ts | 8 + .../__tests__/getBarChartSettings.test.ts | 34 ++- .../__tests__/shouldHideChartSetting.test.ts | 261 ++++++++++++++++++ .../page-layout/utils/getBarChartSettings.ts | 4 + .../utils/getDateGranularityLabel.ts | 27 ++ .../utils/shouldHideChartSetting.ts | 61 ++++ .../__tests__/useDateTimeFormat.test.tsx | 2 +- .../__tests__/useFormatPreferences.test.tsx | 2 +- .../hooks/useFormatPreferences.ts | 2 +- .../workspaceMemberFormatPreferencesState.ts | 2 +- .../utils/detection/detectCalendarStartDay.ts | 2 +- .../getCalendarStartDayFromWorkspaceMember.ts | 3 +- .../ObjectFilterDropdownDateInput.tsx | 3 +- .../ObjectFilterDropdownDateTimeInput.tsx | 3 +- .../hooks/useRecordCalendarMonthDaysRange.tsx | 3 +- .../fragments/pageLayoutWidgetFragment.ts | 13 + .../hooks/useCreatePageLayoutGraphWidget.ts | 7 + .../utils/createDefaultGraphWidget.ts | 14 + .../graph/utils/formatDimensionValue.ts | 1 - ...QueryVariablesFromBarChartConfiguration.ts | 10 + .../widgets/graph/utils/getGroupByOrderBy.ts | 5 +- ...formOneDimensionalGroupByToBarChartData.ts | 2 + ...formTwoDimensionalGroupByToBarChartData.ts | 3 + .../components/FormatPreferencesSettings.tsx | 2 +- .../internal/date/components/DatePicker.tsx | 3 +- .../date/components/DateTimePicker.tsx | 3 +- ...DateTimeSettingsCalendarStartDaySelect.tsx | 3 +- .../dtos/aggregate-chart-configuration.dto.ts | 17 +- .../dtos/bar-chart-configuration.dto.ts | 34 ++- .../dtos/gauge-chart-configuration.dto.ts | 17 +- .../dtos/line-chart-configuration.dto.ts | 34 ++- .../dtos/pie-chart-configuration.dto.ts | 26 +- .../enums/date-granularity.enum.ts | 11 + .../get-page-layout-widget-data-seeds.util.ts | 38 +++ .../src}/constants/CalendarStartDay.ts | 0 packages/twenty-shared/src/constants/index.ts | 1 + packages/twenty-shared/src/index.ts | 2 + 50 files changed, 927 insertions(+), 36 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityXSelectionDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityYSelectionDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularitySetting.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityXSetting.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityYSetting.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts create mode 100644 packages/twenty-server/src/engine/core-modules/page-layout/enums/date-granularity.enum.ts rename packages/{twenty-front/src/modules/localization => twenty-shared/src}/constants/CalendarStartDay.ts (100%) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index f8f2ce81eb..a3edb8e872 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -132,9 +132,11 @@ export type AggregateChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; format?: Maybe; graphType: GraphType; label?: Maybe; + timezone?: Maybe; }; export enum AggregateOperations { @@ -325,17 +327,21 @@ export type BarChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; groupMode?: Maybe; omitNullValues?: Maybe; + primaryAxisDateGranularity?: Maybe; primaryAxisGroupByFieldMetadataId: Scalars['UUID']; primaryAxisGroupBySubFieldName?: Maybe; primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByDateGranularity?: Maybe; secondaryAxisGroupByFieldMetadataId?: Maybe; secondaryAxisGroupBySubFieldName?: Maybe; secondaryAxisOrderBy?: Maybe; + timezone?: Maybe; }; /** Display mode for bar charts with secondary grouping */ @@ -1424,7 +1430,9 @@ export type GaugeChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; + timezone?: Maybe; }; export type GetApiKeyInput = { @@ -1672,16 +1680,20 @@ export type LineChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; omitNullValues?: Maybe; + primaryAxisDateGranularity?: Maybe; primaryAxisGroupByFieldMetadataId: Scalars['UUID']; primaryAxisGroupBySubFieldName?: Maybe; primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByDateGranularity?: Maybe; secondaryAxisGroupByFieldMetadataId?: Maybe; secondaryAxisGroupBySubFieldName?: Maybe; secondaryAxisOrderBy?: Maybe; + timezone?: Maybe; }; export type LinkMetadata = { @@ -2926,6 +2938,18 @@ export type ObjectRecordFilterInput = { updatedAt?: InputMaybe; }; +/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */ +export enum ObjectRecordGroupByDateGranularity { + DAY = 'DAY', + DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK', + MONTH = 'MONTH', + MONTH_OF_THE_YEAR = 'MONTH_OF_THE_YEAR', + NONE = 'NONE', + QUARTER = 'QUARTER', + QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR', + YEAR = 'YEAR' +} + export type ObjectStandardOverrides = { __typename?: 'ObjectStandardOverrides'; description?: Maybe; @@ -3051,13 +3075,16 @@ export type PieChartConfiguration = { aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: AggregateOperations; color?: Maybe; + dateGranularity?: Maybe; description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; groupByFieldMetadataId: Scalars['UUID']; groupBySubFieldName?: Maybe; orderBy?: Maybe; + timezone?: Maybe; }; export type PlaceDetailsResult = { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 020e91017e..7ab4100ff3 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -132,9 +132,11 @@ export type AggregateChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; format?: Maybe; graphType: GraphType; label?: Maybe; + timezone?: Maybe; }; export enum AggregateOperations { @@ -325,17 +327,21 @@ export type BarChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; groupMode?: Maybe; omitNullValues?: Maybe; + primaryAxisDateGranularity?: Maybe; primaryAxisGroupByFieldMetadataId: Scalars['UUID']; primaryAxisGroupBySubFieldName?: Maybe; primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByDateGranularity?: Maybe; secondaryAxisGroupByFieldMetadataId?: Maybe; secondaryAxisGroupBySubFieldName?: Maybe; secondaryAxisOrderBy?: Maybe; + timezone?: Maybe; }; /** Display mode for bar charts with secondary grouping */ @@ -1381,7 +1387,9 @@ export type GaugeChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; + timezone?: Maybe; }; export type GetApiKeyInput = { @@ -1629,16 +1637,20 @@ export type LineChartConfiguration = { description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; omitNullValues?: Maybe; + primaryAxisDateGranularity?: Maybe; primaryAxisGroupByFieldMetadataId: Scalars['UUID']; primaryAxisGroupBySubFieldName?: Maybe; primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByDateGranularity?: Maybe; secondaryAxisGroupByFieldMetadataId?: Maybe; secondaryAxisGroupBySubFieldName?: Maybe; secondaryAxisOrderBy?: Maybe; + timezone?: Maybe; }; export type LinkMetadata = { @@ -2837,6 +2849,18 @@ export type ObjectRecordFilterInput = { updatedAt?: InputMaybe; }; +/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */ +export enum ObjectRecordGroupByDateGranularity { + DAY = 'DAY', + DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK', + MONTH = 'MONTH', + MONTH_OF_THE_YEAR = 'MONTH_OF_THE_YEAR', + NONE = 'NONE', + QUARTER = 'QUARTER', + QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR', + YEAR = 'YEAR' +} + export type ObjectStandardOverrides = { __typename?: 'ObjectStandardOverrides'; description?: Maybe; @@ -2962,13 +2986,16 @@ export type PieChartConfiguration = { aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: AggregateOperations; color?: Maybe; + dateGranularity?: Maybe; description?: Maybe; displayDataLabel?: Maybe; filter?: Maybe; + firstDayOfTheWeek?: Maybe; graphType: GraphType; groupByFieldMetadataId: Scalars['UUID']; groupBySubFieldName?: Maybe; orderBy?: Maybe; + timezone?: Maybe; }; export type PlaceDetailsResult = { @@ -4683,7 +4710,7 @@ export type SearchQueryVariables = Exact<{ export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } }; -export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, description?: string | null, filter?: any | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null }; +export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }; export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ id: Scalars['String']; @@ -4691,7 +4718,7 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ }>; -export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, description?: string | null, filter?: any | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null }> | null }> | null } }; +export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }> | null }> | null } }; export type OnDbEventSubscriptionVariables = Exact<{ input: OnDbEventInput; @@ -5007,6 +5034,8 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` description filter groupMode + timezone + firstDayOfTheWeek } ... on LineChartConfiguration { graphType @@ -5026,6 +5055,8 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` color description filter + timezone + firstDayOfTheWeek } ... on PieChartConfiguration { graphType @@ -5033,19 +5064,26 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` aggregateFieldMetadataId aggregateOperation groupBySubFieldName + dateGranularity orderBy displayDataLabel color description filter + timezone + firstDayOfTheWeek } ... on AggregateChartConfiguration { graphType aggregateFieldMetadataId aggregateOperation + label displayDataLabel + format description filter + timezone + firstDayOfTheWeek } ... on GaugeChartConfiguration { graphType @@ -5055,6 +5093,8 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` color description filter + timezone + firstDayOfTheWeek } ... on IframeConfiguration { url diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx index 408ea22e2b..a0491bfccd 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx @@ -123,6 +123,10 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { const chartSettings = GRAPH_TYPE_INFORMATION[currentGraphType].settings; + const objectMetadataItem = objectMetadataItems.find( + (item) => item.id === widget.objectMetadataId, + ); + const visibleItemIds = chartSettings.flatMap((group) => group.items .filter( @@ -131,6 +135,8 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { item, widget.objectMetadataId, isGroupByEnabled as boolean, + configuration, + objectMetadataItem, ), ) .map((item) => item.id), @@ -154,6 +160,8 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { item, widget.objectMetadataId, isGroupByEnabled as boolean, + configuration, + objectMetadataItem, ), ); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx new file mode 100644 index 0000000000..e3f1150976 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx @@ -0,0 +1,143 @@ +import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; +import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; +import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode'; +import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; +import { getDateGranularityLabel } from '@/command-menu/pages/page-layout/utils/getDateGranularityLabel'; +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 { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { MenuItemSelect } from 'twenty-ui/navigation'; + +type ChartDateGranularitySelectionDropdownContentProps = { + axis?: 'primary' | 'secondary'; +}; + +const getCurrentDateGranularity = ({ + configuration, + axis, +}: { + configuration: ChartConfiguration; + axis: 'primary' | 'secondary'; +}) => { + const defaultGranularity = ObjectRecordGroupByDateGranularity.DAY; + + const isBarOrLineChart = + configuration?.__typename === 'BarChartConfiguration' || + configuration?.__typename === 'LineChartConfiguration'; + + if (!isBarOrLineChart) { + return defaultGranularity; + } + + if (axis === 'primary') { + return configuration.primaryAxisDateGranularity || defaultGranularity; + } + + return ( + configuration.secondaryAxisGroupByDateGranularity || defaultGranularity + ); +}; + +export const ChartDateGranularitySelectionDropdownContent = ({ + axis, +}: ChartDateGranularitySelectionDropdownContentProps) => { + const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); + const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId); + + if ( + !isDefined(axis) && + widgetInEditMode?.configuration?.__typename !== 'PieChartConfiguration' + ) { + throw new Error('Invalid configuration type'); + } + + if ( + isDefined(axis) && + widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' && + widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration' + ) { + throw new Error('Invalid configuration type'); + } + + const currentDateGranularity = getCurrentDateGranularity({ + configuration: widgetInEditMode?.configuration as ChartConfiguration, + axis: axis as 'primary' | 'secondary', + }); + + const dropdownId = useAvailableComponentInstanceIdOrThrow( + DropdownComponentInstanceContext, + ); + + const selectedItemId = useRecoilComponentValue( + selectedItemIdComponentState, + dropdownId, + ); + + const dateGranularityOptions: ObjectRecordGroupByDateGranularity[] = [ + ObjectRecordGroupByDateGranularity.DAY, + ObjectRecordGroupByDateGranularity.MONTH, + ObjectRecordGroupByDateGranularity.QUARTER, + ObjectRecordGroupByDateGranularity.YEAR, + ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK, + ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR, + ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR, + ]; + + const { updateCurrentWidgetConfig } = + useUpdateCurrentWidgetConfig(pageLayoutId); + + const { closeDropdown } = useCloseDropdown(); + + const handleSelectDateGranularityOption = ( + dateGranularityOption: ObjectRecordGroupByDateGranularity, + ) => { + const configToUpdate = axis + ? axis === 'primary' + ? { primaryAxisDateGranularity: dateGranularityOption } + : { secondaryAxisGroupByDateGranularity: dateGranularityOption } + : { dateGranularity: dateGranularityOption }; + + updateCurrentWidgetConfig({ + configToUpdate, + }); + closeDropdown(); + }; + + return ( + <> + + + {dateGranularityOptions.map((option) => ( + { + handleSelectDateGranularityOption(option); + }} + > + { + handleSelectDateGranularityOption(option); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityXSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityXSelectionDropdownContent.tsx new file mode 100644 index 0000000000..813768a490 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityXSelectionDropdownContent.tsx @@ -0,0 +1,5 @@ +import { ChartDateGranularitySelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent'; + +export const ChartDateGranularityXSelectionDropdownContent = () => { + return ; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityYSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityYSelectionDropdownContent.tsx new file mode 100644 index 0000000000..d631aa3f6a --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityYSelectionDropdownContent.tsx @@ -0,0 +1,5 @@ +import { ChartDateGranularitySelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent'; + +export const ChartDateGranularityYSelectionDropdownContent = () => { + return ; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts index 4acf5746df..1946ddddff 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts @@ -4,6 +4,8 @@ import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/setti import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting'; import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting'; import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting'; +import { DATE_GRANULARITY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DateGranularityXSetting'; +import { DATE_GRANULARITY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DateGranularityYSetting'; import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting'; import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting'; import { OMIT_NULL_VALUES_SETTING } from '@/command-menu/pages/page-layout/constants/settings/OmitNullValuesSetting'; @@ -22,6 +24,7 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [ heading: 'X axis', items: [ DATA_DISPLAY_X_SETTING, + DATE_GRANULARITY_X_SETTING, SORT_BY_X_SETTING, OMIT_NULL_VALUES_SETTING, ], @@ -31,6 +34,7 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [ items: [ DATA_DISPLAY_Y_SETTING, GROUP_BY_SETTING, + DATE_GRANULARITY_Y_SETTING, SORT_BY_GROUP_BY_FIELD_SETTING, RANGE_MIN_SETTING, RANGE_MAX_SETTING, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/PieChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/PieChartSettings.ts index 5d9af574d4..7b3c4409aa 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/PieChartSettings.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/PieChartSettings.ts @@ -2,6 +2,7 @@ import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/cons import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting'; import { DATA_DISPLAY_PIE_CHART_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayPieChartSetting'; import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting'; +import { DATE_GRANULARITY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DateGranularitySetting'; import { EACH_SLICE_REPRESENTS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/EachSliceRepresentsSetting'; import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting'; import { SORT_BY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByXSetting'; @@ -14,6 +15,7 @@ export const PIE_CHART_SETTINGS: ChartSettingsGroup[] = [ CHART_DATA_SOURCE_SETTING, FILTER_SETTING, DATA_DISPLAY_PIE_CHART_SETTING, + DATE_GRANULARITY_SETTING, EACH_SLICE_REPRESENTS_SETTING, SORT_BY_X_SETTING, ], diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts index 9b1b426b45..332cefe33c 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts @@ -18,4 +18,7 @@ export const CHART_CONFIGURATION_SETTING_LABELS = { OMIT_NULL_VALUES: msg`Omit zero values`, MIN_RANGE: msg`Min range`, MAX_RANGE: msg`Max range`, + DATE_GRANULARITY_X: msg`Date granularity`, + DATE_GRANULARITY_Y: msg`Date granularity`, + DATE_GRANULARITY: msg`Date granularity`, }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularitySetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularitySetting.ts new file mode 100644 index 0000000000..024e9b5901 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularitySetting.ts @@ -0,0 +1,13 @@ +import { ChartDateGranularitySelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent'; +import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels'; +import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; +import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; +import { IconCalendar } from 'twenty-ui/display'; + +export const DATE_GRANULARITY_SETTING: ChartSettingsItem = { + isBoolean: false, + Icon: IconCalendar, + label: CHART_CONFIGURATION_SETTING_LABELS.DATE_GRANULARITY, + id: CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY, + DropdownContent: ChartDateGranularitySelectionDropdownContent, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityXSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityXSetting.ts new file mode 100644 index 0000000000..b2b41b0344 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityXSetting.ts @@ -0,0 +1,14 @@ +import { ChartDateGranularityXSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityXSelectionDropdownContent'; +import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels'; +import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; +import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; +import { IconCalendar } from 'twenty-ui/display'; + +export const DATE_GRANULARITY_X_SETTING: ChartSettingsItem = { + isBoolean: false, + Icon: IconCalendar, + label: CHART_CONFIGURATION_SETTING_LABELS.DATE_GRANULARITY_X, + id: CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_X, + dependsOn: [CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X], + DropdownContent: ChartDateGranularityXSelectionDropdownContent, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityYSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityYSetting.ts new file mode 100644 index 0000000000..c45aaf57a8 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DateGranularityYSetting.ts @@ -0,0 +1,14 @@ +import { ChartDateGranularityYSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularityYSelectionDropdownContent'; +import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels'; +import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; +import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; +import { IconCalendar } from 'twenty-ui/display'; + +export const DATE_GRANULARITY_Y_SETTING: ChartSettingsItem = { + isBoolean: false, + Icon: IconCalendar, + label: CHART_CONFIGURATION_SETTING_LABELS.DATE_GRANULARITY_Y, + id: CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_Y, + dependsOn: [CHART_CONFIGURATION_SETTING_IDS.GROUP_BY], + DropdownContent: ChartDateGranularityYSelectionDropdownContent, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts index e7923769b9..f2a5d18aca 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts @@ -3,6 +3,7 @@ import { useGraphXSortOptionLabels } from '@/command-menu/pages/page-layout/hook import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; import { getChartAxisNameDisplayOptions } from '@/command-menu/pages/page-layout/utils/getChartAxisNameDisplayOptions'; +import { getDateGranularityLabel } from '@/command-menu/pages/page-layout/utils/getDateGranularityLabel'; import { getFieldLabelWithSubField } from '@/command-menu/pages/page-layout/utils/getFieldLabelWithSubField'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; @@ -173,6 +174,23 @@ export const useChartSettingsValues = ({ return 'rangeMax' in configuration ? (configuration.rangeMax?.toString() ?? '') : ''; + case CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_X: + return 'primaryAxisDateGranularity' in configuration && + isDefined(configuration.primaryAxisDateGranularity) + ? getDateGranularityLabel(configuration.primaryAxisDateGranularity) + : undefined; + case CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_Y: + return 'secondaryAxisGroupByDateGranularity' in configuration && + isDefined(configuration.secondaryAxisGroupByDateGranularity) + ? getDateGranularityLabel( + configuration.secondaryAxisGroupByDateGranularity, + ) + : undefined; + case CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY: + return 'dateGranularity' in configuration && + isDefined(configuration.dateGranularity) + ? getDateGranularityLabel(configuration.dateGranularity) + : undefined; default: return ''; } diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts index ebc3cd9522..a7a475d419 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts @@ -17,6 +17,9 @@ export enum CHART_CONFIGURATION_SETTING_IDS { OMIT_NULL_VALUES = 'OMIT_NULL_VALUES', MIN_RANGE = 'MIN_RANGE', MAX_RANGE = 'MAX_RANGE', + DATE_GRANULARITY_X = 'DATE_GRANULARITY_X', + DATE_GRANULARITY_Y = 'DATE_GRANULARITY_Y', + DATE_GRANULARITY = 'DATE_GRANULARITY', } export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = { @@ -25,4 +28,9 @@ export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = { [CHART_CONFIGURATION_SETTING_IDS.OMIT_NULL_VALUES]: 'omitNullValues', [CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE]: 'rangeMin', [CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE]: 'rangeMax', + [CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_X]: + 'primaryAxisDateGranularity', + [CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_Y]: + 'secondaryAxisGroupByDateGranularity', + [CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY]: 'dateGranularity', } as const; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts index e733516125..f31d74d806 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts @@ -4,6 +4,8 @@ import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/setti import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting'; import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting'; import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting'; +import { DATE_GRANULARITY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DateGranularityXSetting'; +import { DATE_GRANULARITY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DateGranularityYSetting'; import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting'; import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting'; import { OMIT_NULL_VALUES_SETTING } from '@/command-menu/pages/page-layout/constants/settings/OmitNullValuesSetting'; @@ -24,12 +26,13 @@ describe('getBarChartSettings', () => { const xAxisGroup = result.find((group) => group.heading === 'X axis'); expect(xAxisGroup).toBeDefined(); - expect(xAxisGroup?.items).toHaveLength(3); + expect(xAxisGroup?.items).toHaveLength(4); expect(xAxisGroup?.items[0].id).toBe(DATA_DISPLAY_X_SETTING.id); expect(xAxisGroup?.items[0].label).toBe(DATA_DISPLAY_X_SETTING.label); expect(xAxisGroup?.items[0].Icon).toBe(IconAxisX); - expect(xAxisGroup?.items[1]).toEqual(SORT_BY_X_SETTING); - expect(xAxisGroup?.items[2]).toEqual(OMIT_NULL_VALUES_SETTING); + expect(xAxisGroup?.items[1]).toEqual(DATE_GRANULARITY_X_SETTING); + expect(xAxisGroup?.items[2]).toEqual(SORT_BY_X_SETTING); + expect(xAxisGroup?.items[3]).toEqual(OMIT_NULL_VALUES_SETTING); }); it('should place secondary axis items under "Y axis" heading', () => { @@ -38,14 +41,15 @@ describe('getBarChartSettings', () => { const yAxisGroup = result.find((group) => group.heading === 'Y axis'); expect(yAxisGroup).toBeDefined(); - expect(yAxisGroup?.items).toHaveLength(5); + expect(yAxisGroup?.items).toHaveLength(6); expect(yAxisGroup?.items[0].id).toBe(DATA_DISPLAY_Y_SETTING.id); expect(yAxisGroup?.items[0].label).toBe(DATA_DISPLAY_Y_SETTING.label); expect(yAxisGroup?.items[0].Icon).toBe(IconAxisY); expect(yAxisGroup?.items[1]).toEqual(GROUP_BY_SETTING); - expect(yAxisGroup?.items[2]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING); - expect(yAxisGroup?.items[3]).toEqual(RANGE_MIN_SETTING); - expect(yAxisGroup?.items[4]).toEqual(RANGE_MAX_SETTING); + expect(yAxisGroup?.items[2]).toEqual(DATE_GRANULARITY_Y_SETTING); + expect(yAxisGroup?.items[3]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING); + expect(yAxisGroup?.items[4]).toEqual(RANGE_MIN_SETTING); + expect(yAxisGroup?.items[5]).toEqual(RANGE_MAX_SETTING); }); it('should have all expected groups in correct order', () => { @@ -66,14 +70,15 @@ describe('getBarChartSettings', () => { const xAxisGroup = result.find((group) => group.heading === 'X axis'); expect(xAxisGroup).toBeDefined(); - expect(xAxisGroup?.items).toHaveLength(5); + expect(xAxisGroup?.items).toHaveLength(6); expect(xAxisGroup?.items[0].id).toBe(DATA_DISPLAY_Y_SETTING.id); expect(xAxisGroup?.items[0].label).toBe(DATA_DISPLAY_Y_SETTING.label); expect(xAxisGroup?.items[0].Icon).toBe(IconAxisX); expect(xAxisGroup?.items[1]).toEqual(GROUP_BY_SETTING); - expect(xAxisGroup?.items[2]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING); - expect(xAxisGroup?.items[3]).toEqual(RANGE_MIN_SETTING); - expect(xAxisGroup?.items[4]).toEqual(RANGE_MAX_SETTING); + expect(xAxisGroup?.items[2]).toEqual(DATE_GRANULARITY_Y_SETTING); + expect(xAxisGroup?.items[3]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING); + expect(xAxisGroup?.items[4]).toEqual(RANGE_MIN_SETTING); + expect(xAxisGroup?.items[5]).toEqual(RANGE_MAX_SETTING); }); it('should place PRIMARY axis items under "Y axis" heading', () => { @@ -82,12 +87,13 @@ describe('getBarChartSettings', () => { const yAxisGroup = result.find((group) => group.heading === 'Y axis'); expect(yAxisGroup).toBeDefined(); - expect(yAxisGroup?.items).toHaveLength(3); + expect(yAxisGroup?.items).toHaveLength(4); expect(yAxisGroup?.items[0].id).toBe(DATA_DISPLAY_X_SETTING.id); expect(yAxisGroup?.items[0].label).toBe(DATA_DISPLAY_X_SETTING.label); expect(yAxisGroup?.items[0].Icon).toBe(IconAxisY); - expect(yAxisGroup?.items[1]).toEqual(SORT_BY_X_SETTING); - expect(yAxisGroup?.items[2]).toEqual(OMIT_NULL_VALUES_SETTING); + expect(yAxisGroup?.items[1]).toEqual(DATE_GRANULARITY_X_SETTING); + expect(yAxisGroup?.items[2]).toEqual(SORT_BY_X_SETTING); + expect(yAxisGroup?.items[3]).toEqual(OMIT_NULL_VALUES_SETTING); }); it('should have all expected groups in correct order', () => { diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts index 18129cf239..35a6d70226 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts @@ -1,6 +1,9 @@ +import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { msg } from '@lingui/core/macro'; +import { FieldMetadataType } from 'twenty-shared/types'; import { IconChartBar } from 'twenty-ui/display'; import { shouldHideChartSetting } from '../shouldHideChartSetting'; @@ -176,4 +179,262 @@ describe('shouldHideChartSetting', () => { expect(result).toBe(false); }); }); + + describe('date granularity visibility based on field type', () => { + const mockDateGranularityXItem: ChartSettingsItem = { + id: CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_X, + label: msg`Date Granularity X`, + Icon: IconChartBar, + isBoolean: false, + isInput: false, + }; + + const mockDateGranularityYItem: ChartSettingsItem = { + id: CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_Y, + label: msg`Date Granularity Y`, + Icon: IconChartBar, + isBoolean: false, + isInput: false, + }; + + const mockDateGranularityItem: ChartSettingsItem = { + id: CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY, + label: msg`Date Granularity`, + Icon: IconChartBar, + isBoolean: false, + isInput: false, + }; + + const mockObjectMetadataItem: ObjectMetadataItem = { + id: 'object-id', + nameSingular: 'opportunity', + namePlural: 'opportunities', + labelSingular: 'Opportunity', + labelPlural: 'Opportunities', + description: 'An opportunity', + icon: 'IconTargetArrow', + isActive: true, + isSystem: false, + isRemote: false, + isAuditLogged: false, + isLabelSyncedWithName: true, + fields: [ + { + id: 'date-field-id', + name: 'closedAt', + label: 'Closed At', + type: FieldMetadataType.DATE, + isNullable: true, + isSystem: false, + isActive: true, + isCustom: false, + defaultValue: null, + options: null, + relationDefinition: null, + } as any, + { + id: 'text-field-id', + name: 'name', + label: 'Name', + type: FieldMetadataType.TEXT, + isNullable: true, + isSystem: false, + isActive: true, + isCustom: false, + defaultValue: null, + options: null, + relationDefinition: null, + } as any, + { + id: 'datetime-field-id', + name: 'createdAt', + label: 'Created At', + type: FieldMetadataType.DATE_TIME, + isNullable: false, + isSystem: true, + isActive: true, + isCustom: false, + defaultValue: null, + options: null, + relationDefinition: null, + } as any, + ], + } as any; + + describe('DATE_GRANULARITY_X', () => { + it('should show when primary axis field is a date field', () => { + const barChartConfig: ChartConfiguration = { + __typename: 'BarChartConfiguration', + primaryAxisGroupByFieldMetadataId: 'date-field-id', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityXItem, + 'object-id', + true, + barChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(false); + }); + + it('should show when primary axis field is a datetime field', () => { + const barChartConfig: ChartConfiguration = { + __typename: 'BarChartConfiguration', + primaryAxisGroupByFieldMetadataId: 'datetime-field-id', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityXItem, + 'object-id', + true, + barChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(false); + }); + + it('should hide when primary axis field is not a date field', () => { + const barChartConfig: ChartConfiguration = { + __typename: 'BarChartConfiguration', + primaryAxisGroupByFieldMetadataId: 'text-field-id', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityXItem, + 'object-id', + true, + barChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(true); + }); + + it('should hide when no primary axis field is selected', () => { + const barChartConfig: ChartConfiguration = { + __typename: 'BarChartConfiguration', + primaryAxisGroupByFieldMetadataId: undefined, + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityXItem, + 'object-id', + true, + barChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(true); + }); + }); + + describe('DATE_GRANULARITY_Y', () => { + it('should show when secondary axis field is a date field', () => { + const barChartConfig: ChartConfiguration = { + __typename: 'BarChartConfiguration', + secondaryAxisGroupByFieldMetadataId: 'date-field-id', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityYItem, + 'object-id', + true, + barChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(false); + }); + + it('should hide when secondary axis field is not a date field', () => { + const barChartConfig: ChartConfiguration = { + __typename: 'BarChartConfiguration', + secondaryAxisGroupByFieldMetadataId: 'text-field-id', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityYItem, + 'object-id', + true, + barChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(true); + }); + + it('should hide when no secondary axis field is selected', () => { + const barChartConfig: ChartConfiguration = { + __typename: 'BarChartConfiguration', + secondaryAxisGroupByFieldMetadataId: undefined, + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityYItem, + 'object-id', + true, + barChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(true); + }); + }); + + describe('DATE_GRANULARITY (Pie Chart)', () => { + it('should show when group by field is a date field', () => { + const pieChartConfig: ChartConfiguration = { + __typename: 'PieChartConfiguration', + groupByFieldMetadataId: 'date-field-id', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityItem, + 'object-id', + true, + pieChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(false); + }); + + it('should hide when group by field is not a date field', () => { + const pieChartConfig: ChartConfiguration = { + __typename: 'PieChartConfiguration', + groupByFieldMetadataId: 'text-field-id', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityItem, + 'object-id', + true, + pieChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(true); + }); + + it('should hide when no group by field is selected', () => { + const pieChartConfig: ChartConfiguration = { + __typename: 'PieChartConfiguration', + groupByFieldMetadataId: undefined, + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityItem, + 'object-id', + true, + pieChartConfig, + mockObjectMetadataItem, + ); + + expect(result).toBe(true); + }); + }); + }); }); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts index 1b67bfac39..2ff87c3f69 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts @@ -4,6 +4,8 @@ import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/setti import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting'; import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting'; import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting'; +import { DATE_GRANULARITY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DateGranularityXSetting'; +import { DATE_GRANULARITY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DateGranularityYSetting'; import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting'; import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting'; import { OMIT_NULL_VALUES_SETTING } from '@/command-menu/pages/page-layout/constants/settings/OmitNullValuesSetting'; @@ -26,6 +28,7 @@ export const getBarChartSettings = ( const primaryAxisItems = [ { ...DATA_DISPLAY_X_SETTING, Icon: dataDisplayXIcon }, + DATE_GRANULARITY_X_SETTING, SORT_BY_X_SETTING, OMIT_NULL_VALUES_SETTING, ]; @@ -33,6 +36,7 @@ export const getBarChartSettings = ( const secondaryAxisItems = [ { ...DATA_DISPLAY_Y_SETTING, Icon: dataDisplayYIcon }, GROUP_BY_SETTING, + DATE_GRANULARITY_Y_SETTING, SORT_BY_GROUP_BY_FIELD_SETTING, RANGE_MIN_SETTING, RANGE_MAX_SETTING, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts new file mode 100644 index 0000000000..dc2220eeb2 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts @@ -0,0 +1,27 @@ +import { t } from '@lingui/core/macro'; +import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; + +export const getDateGranularityLabel = ( + granularity: ObjectRecordGroupByDateGranularity, +): string => { + switch (granularity) { + case ObjectRecordGroupByDateGranularity.DAY: + return t`Day`; + case ObjectRecordGroupByDateGranularity.MONTH: + return t`Month`; + case ObjectRecordGroupByDateGranularity.QUARTER: + return t`Quarter`; + case ObjectRecordGroupByDateGranularity.YEAR: + return t`Year`; + case ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK: + return t`Day of the week`; + case ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR: + return t`Month of the year`; + case ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR: + return t`Quarter of the year`; + case ObjectRecordGroupByDateGranularity.NONE: + return t`None`; + default: + return granularity; + } +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts index c13e4d3f2f..f216bdbff2 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts @@ -1,11 +1,35 @@ +import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isNonEmptyString } from '@sniptt/guards'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; + +const shouldHideDateGranularityBasedOnFieldType = ( + fieldMetadataId: string | undefined | null, + objectMetadataItem: ObjectMetadataItem, +): boolean => { + if (!isDefined(fieldMetadataId)) { + return true; + } + + const field = objectMetadataItem.fields.find( + (field) => field.id === fieldMetadataId, + ); + + if (!isDefined(field)) { + return true; + } + + return !isFieldMetadataDateKind(field.type); +}; export const shouldHideChartSetting = ( item: ChartSettingsItem, objectMetadataId: string, isGroupByEnabled: boolean, + configuration?: ChartConfiguration, + objectMetadataItem?: ObjectMetadataItem, ): boolean => { const hasNoObjectMetadata = !isNonEmptyString(objectMetadataId); const dependsOnSource = item?.dependsOn?.includes( @@ -15,6 +39,43 @@ export const shouldHideChartSetting = ( CHART_CONFIGURATION_SETTING_IDS.GROUP_BY, ); + if (isDefined(configuration) && isDefined(objectMetadataItem)) { + if (item.id === CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_X) { + const isBarOrLineChart = + configuration.__typename === 'BarChartConfiguration' || + configuration.__typename === 'LineChartConfiguration'; + + if (isBarOrLineChart) { + return shouldHideDateGranularityBasedOnFieldType( + configuration.primaryAxisGroupByFieldMetadataId, + objectMetadataItem, + ); + } + } + + if (item.id === CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_Y) { + const isBarOrLineChart = + configuration.__typename === 'BarChartConfiguration' || + configuration.__typename === 'LineChartConfiguration'; + + if (isBarOrLineChart) { + return shouldHideDateGranularityBasedOnFieldType( + configuration.secondaryAxisGroupByFieldMetadataId, + objectMetadataItem, + ); + } + } + + if (item.id === CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY) { + if (configuration.__typename === 'PieChartConfiguration') { + return shouldHideDateGranularityBasedOnFieldType( + configuration.groupByFieldMetadataId, + objectMetadataItem, + ); + } + } + } + return ( (hasNoObjectMetadata && (dependsOnSource ?? false)) || (!isGroupByEnabled && (dependsOnGroupBy ?? false)) diff --git a/packages/twenty-front/src/modules/localization/hooks/__tests__/useDateTimeFormat.test.tsx b/packages/twenty-front/src/modules/localization/hooks/__tests__/useDateTimeFormat.test.tsx index 7e8f0a0149..6ca7a07c50 100644 --- a/packages/twenty-front/src/modules/localization/hooks/__tests__/useDateTimeFormat.test.tsx +++ b/packages/twenty-front/src/modules/localization/hooks/__tests__/useDateTimeFormat.test.tsx @@ -2,7 +2,7 @@ import { renderHook } from '@testing-library/react'; import { type ReactNode } from 'react'; import { RecoilRoot } from 'recoil'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; import { DateFormat } from '@/localization/constants/DateFormat'; import { TimeFormat } from '@/localization/constants/TimeFormat'; import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat'; diff --git a/packages/twenty-front/src/modules/localization/hooks/__tests__/useFormatPreferences.test.tsx b/packages/twenty-front/src/modules/localization/hooks/__tests__/useFormatPreferences.test.tsx index dba42a9709..ba2e86a160 100644 --- a/packages/twenty-front/src/modules/localization/hooks/__tests__/useFormatPreferences.test.tsx +++ b/packages/twenty-front/src/modules/localization/hooks/__tests__/useFormatPreferences.test.tsx @@ -3,7 +3,7 @@ import { type ReactNode } from 'react'; import { RecoilRoot, type MutableSnapshot } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; import { DateFormat } from '@/localization/constants/DateFormat'; import { NumberFormat } from '@/localization/constants/NumberFormat'; import { TimeFormat } from '@/localization/constants/TimeFormat'; diff --git a/packages/twenty-front/src/modules/localization/hooks/useFormatPreferences.ts b/packages/twenty-front/src/modules/localization/hooks/useFormatPreferences.ts index 9834276067..8b4cbc34ca 100644 --- a/packages/twenty-front/src/modules/localization/hooks/useFormatPreferences.ts +++ b/packages/twenty-front/src/modules/localization/hooks/useFormatPreferences.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; import { DateFormat } from '@/localization/constants/DateFormat'; import { NumberFormat } from '@/localization/constants/NumberFormat'; import { TimeFormat } from '@/localization/constants/TimeFormat'; diff --git a/packages/twenty-front/src/modules/localization/states/workspaceMemberFormatPreferencesState.ts b/packages/twenty-front/src/modules/localization/states/workspaceMemberFormatPreferencesState.ts index 4c817a3f62..886eec0b10 100644 --- a/packages/twenty-front/src/modules/localization/states/workspaceMemberFormatPreferencesState.ts +++ b/packages/twenty-front/src/modules/localization/states/workspaceMemberFormatPreferencesState.ts @@ -1,4 +1,4 @@ -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; import { DateFormat } from '@/localization/constants/DateFormat'; import { NumberFormat } from '@/localization/constants/NumberFormat'; import { TimeFormat } from '@/localization/constants/TimeFormat'; diff --git a/packages/twenty-front/src/modules/localization/utils/detection/detectCalendarStartDay.ts b/packages/twenty-front/src/modules/localization/utils/detection/detectCalendarStartDay.ts index 88deb53e85..7f38f1dac3 100644 --- a/packages/twenty-front/src/modules/localization/utils/detection/detectCalendarStartDay.ts +++ b/packages/twenty-front/src/modules/localization/utils/detection/detectCalendarStartDay.ts @@ -1,4 +1,4 @@ -import { type CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { type CalendarStartDay } from 'twenty-shared'; import { type ExcludeLiteral } from '~/types/ExcludeLiteral'; const MONDAY_KEY: keyof typeof CalendarStartDay = 'MONDAY'; diff --git a/packages/twenty-front/src/modules/localization/utils/format-preferences/getCalendarStartDayFromWorkspaceMember.ts b/packages/twenty-front/src/modules/localization/utils/format-preferences/getCalendarStartDayFromWorkspaceMember.ts index 2ad2cb5ed3..3b7373ec62 100644 --- a/packages/twenty-front/src/modules/localization/utils/format-preferences/getCalendarStartDayFromWorkspaceMember.ts +++ b/packages/twenty-front/src/modules/localization/utils/format-preferences/getCalendarStartDayFromWorkspaceMember.ts @@ -1,5 +1,6 @@ import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; + import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay'; import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index 269e02009f..b6a5cb3ab3 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -1,5 +1,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; + import { detectCalendarStartDay, type NonSystemCalendarStartDay, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateTimeInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateTimeInput.tsx index bb2b6342a5..6b57dba421 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateTimeInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateTimeInput.tsx @@ -1,5 +1,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; + import { detectCalendarStartDay, type NonSystemCalendarStartDay, diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange.tsx index e463d23194..c6178d86ee 100644 --- a/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange.tsx +++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange.tsx @@ -1,5 +1,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; + import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay'; import { addDays, diff --git a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts index 76d435239c..a6781001b1 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts @@ -35,6 +35,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` description filter groupMode + timezone + firstDayOfTheWeek } ... on LineChartConfiguration { graphType @@ -54,6 +56,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` color description filter + timezone + firstDayOfTheWeek } ... on PieChartConfiguration { graphType @@ -61,19 +65,26 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` aggregateFieldMetadataId aggregateOperation groupBySubFieldName + dateGranularity orderBy displayDataLabel color description filter + timezone + firstDayOfTheWeek } ... on AggregateChartConfiguration { graphType aggregateFieldMetadataId aggregateOperation + label displayDataLabel + format description filter + timezone + firstDayOfTheWeek } ... on GaugeChartConfiguration { graphType @@ -83,6 +94,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` color description filter + timezone + firstDayOfTheWeek } ... on IframeConfiguration { url diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts index b70487ce49..b41740e0a7 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts @@ -1,3 +1,4 @@ +import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; @@ -27,6 +28,8 @@ export const useCreatePageLayoutGraphWidget = ( pageLayoutIdFromProps, ); + const { timeZone, calendarStartDay } = useDateTimeFormat(); + const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId); const activeTabIdState = useRecoilComponentCallbackState( @@ -109,6 +112,8 @@ export const useCreatePageLayoutGraphWidget = ( columnSpan: position.w, }, fieldSelection, + timezone: timeZone, + firstDayOfTheWeek: calendarStartDay, }); const newLayout = { @@ -142,6 +147,8 @@ export const useCreatePageLayoutGraphWidget = ( pageLayoutCurrentLayoutsState, pageLayoutDraftState, pageLayoutDraggedAreaState, + timeZone, + calendarStartDay, ], ); diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts index 5609723f57..cab585481c 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts @@ -16,6 +16,8 @@ import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetF const createDefaultGraphConfiguration = ( graphType: GraphType, fieldSelection?: GraphWidgetFieldSelection, + timezone?: string, + firstDayOfTheWeek?: number, ): WidgetConfiguration | null => { switch (graphType) { case GraphType.AGGREGATE: @@ -28,6 +30,8 @@ const createDefaultGraphConfiguration = ( aggregateFieldMetadataId: fieldSelection.aggregateFieldMetadataId, aggregateOperation: AggregateOperations.COUNT, displayDataLabel: true, + timezone, + firstDayOfTheWeek, }; case GraphType.PIE: @@ -51,6 +55,8 @@ const createDefaultGraphConfiguration = ( aggregateOperation: AggregateOperations.SUM, primaryAxisOrderBy: GraphOrderBy.FIELD_ASC, axisNameDisplay: AxisNameDisplay.NONE, + timezone, + firstDayOfTheWeek, }; case GraphType.HORIZONTAL_BAR: @@ -71,6 +77,8 @@ const createDefaultGraphConfiguration = ( aggregateOperation: AggregateOperations.SUM, primaryAxisOrderBy: GraphOrderBy.FIELD_ASC, axisNameDisplay: AxisNameDisplay.NONE, + timezone, + firstDayOfTheWeek, }; case GraphType.LINE: @@ -92,6 +100,8 @@ type CreateDefaultGraphWidgetParams = { gridPosition: GridPosition; objectMetadataId?: string | null; fieldSelection?: GraphWidgetFieldSelection; + timezone?: string; + firstDayOfTheWeek?: number; }; export const createDefaultGraphWidget = ({ @@ -102,6 +112,8 @@ export const createDefaultGraphWidget = ({ gridPosition, objectMetadataId, fieldSelection, + timezone, + firstDayOfTheWeek, }: CreateDefaultGraphWidgetParams): PageLayoutWidget => { const resolvedObjectMetadataId = fieldSelection?.objectMetadataId ?? objectMetadataId ?? null; @@ -109,6 +121,8 @@ export const createDefaultGraphWidget = ({ const configuration = createDefaultGraphConfiguration( graphType, fieldSelection, + timezone, + firstDayOfTheWeek, ); return { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts index 8549ccc16f..f43a662cfa 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts @@ -75,7 +75,6 @@ export const formatDimensionValue = ({ case FieldMetadataType.DATE: case FieldMetadataType.DATE_TIME: { - // TODO: granularity will be passed from the graph configuration when implemented if ( dateGranularity === ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK || diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts index d8d22c5095..c0153f55cb 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts @@ -53,6 +53,8 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({ buildGroupByFieldObject({ field: groupByFieldX, subFieldName: groupBySubFieldNameX, + dateGranularity: + barChartConfiguration.primaryAxisDateGranularity ?? undefined, }), ); @@ -61,6 +63,9 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({ buildGroupByFieldObject({ field: groupByFieldY, subFieldName: groupBySubFieldNameY, + dateGranularity: + barChartConfiguration.secondaryAxisGroupByDateGranularity ?? + undefined, }), ); } @@ -80,6 +85,8 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({ groupBySubFieldName: barChartConfiguration.primaryAxisGroupBySubFieldName, aggregateOperation, + dateGranularity: + barChartConfiguration.primaryAxisDateGranularity ?? undefined, }), ); } @@ -94,6 +101,9 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({ groupBySubFieldName: barChartConfiguration.secondaryAxisGroupBySubFieldName, aggregateOperation, + dateGranularity: + barChartConfiguration.secondaryAxisGroupByDateGranularity ?? + undefined, }), ); } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts index bd634a8049..50e43c1c8c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts @@ -4,6 +4,7 @@ import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/cons import { OrderByDirection, type AggregateOrderByWithGroupByField, + type ObjectRecordGroupByDateGranularity, type ObjectRecordOrderByForCompositeField, type ObjectRecordOrderByForScalarField, type ObjectRecordOrderByWithGroupByDateField, @@ -35,11 +36,13 @@ export const getGroupByOrderBy = ({ groupByField, groupBySubFieldName, aggregateOperation, + dateGranularity, }: { graphOrderBy: GraphOrderBy; groupByField: FieldMetadataItem; groupBySubFieldName?: string | null; aggregateOperation?: string; + dateGranularity?: ObjectRecordGroupByDateGranularity; }): | AggregateOrderByWithGroupByField | ObjectRecordOrderByForScalarField @@ -63,7 +66,7 @@ export const getGroupByOrderBy = ({ return { [groupByField.name]: { orderBy: mapOrderByToDirection(graphOrderBy), - granularity: GRAPH_DEFAULT_DATE_GRANULARITY, + granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY, }, }; } else { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts index 616b3bdc8e..c28aeaace1 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts @@ -60,6 +60,8 @@ export const transformOneDimensionalGroupByToBarChartData = ({ ? formatDimensionValue({ value: dimensionValues[0], fieldMetadata: groupByFieldX, + dateGranularity: + configuration.primaryAxisDateGranularity ?? undefined, subFieldName: configuration.primaryAxisGroupBySubFieldName ?? undefined, }) diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts index 7a371cc5c9..3baee1b271 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts @@ -64,11 +64,14 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ const xValue = formatDimensionValue({ value: dimensionValues[0], fieldMetadata: groupByFieldX, + dateGranularity: configuration.primaryAxisDateGranularity ?? undefined, subFieldName: configuration.primaryAxisGroupBySubFieldName ?? undefined, }); const yValue = formatDimensionValue({ value: dimensionValues[1], fieldMetadata: groupByFieldY, + dateGranularity: + configuration.secondaryAxisGroupByDateGranularity ?? undefined, subFieldName: configuration.secondaryAxisGroupBySubFieldName ?? undefined, }); diff --git a/packages/twenty-front/src/modules/settings/experience/components/FormatPreferencesSettings.tsx b/packages/twenty-front/src/modules/settings/experience/components/FormatPreferencesSettings.tsx index d747d5bb47..4785ad4f75 100644 --- a/packages/twenty-front/src/modules/settings/experience/components/FormatPreferencesSettings.tsx +++ b/packages/twenty-front/src/modules/settings/experience/components/FormatPreferencesSettings.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; import { DateFormat } from '@/localization/constants/DateFormat'; import { NumberFormat } from '@/localization/constants/NumberFormat'; import { TimeFormat } from '@/localization/constants/TimeFormat'; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePicker.tsx index 7530f91e5e..59957fe857 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePicker.tsx @@ -5,7 +5,8 @@ import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; + import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay'; import { DatePickerHeader } from '@/ui/input/components/internal/date/components/DatePickerHeader'; import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader'; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx index b172fe4d89..f04d256f91 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx @@ -1,5 +1,6 @@ import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; + import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay'; import { DateTimePickerHeader } from '@/ui/input/components/internal/date/components/DateTimePickerHeader'; import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader'; diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsCalendarStartDaySelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsCalendarStartDaySelect.tsx index a56d700adc..153b8010c0 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsCalendarStartDaySelect.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsCalendarStartDaySelect.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; -import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; +import { CalendarStartDay } from 'twenty-shared'; + import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay'; import { Select } from '@/ui/input/components/Select'; import { type DayNameWithIndex } from '@/ui/input/components/internal/date/types/DayNameWithIndex'; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/aggregate-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/aggregate-chart-configuration.dto.ts index 08f21e9a5c..39ee0ef84c 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/aggregate-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/aggregate-chart-configuration.dto.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; import { IsBoolean, @@ -7,9 +7,13 @@ import { IsObject, IsOptional, IsString, + IsTimeZone, IsUUID, + Max, + Min, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { CalendarStartDay } from 'twenty-shared/constants'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; @@ -58,4 +62,15 @@ export class AggregateChartConfigurationDTO { @IsObject() @IsOptional() filter?: ObjectRecordFilter; + + @Field(() => String, { nullable: true, defaultValue: 'UTC' }) + @IsTimeZone() + @IsOptional() + timezone?: string; + + @Field(() => Int, { nullable: true, defaultValue: CalendarStartDay.MONDAY }) + @IsOptional() + @Min(0) + @Max(7) + firstDayOfTheWeek?: number; } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts index f6ab99056a..9cea899a07 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; import { IsBoolean, @@ -9,9 +9,13 @@ import { IsObject, IsOptional, IsString, + IsTimeZone, IsUUID, + Max, + Min, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { CalendarStartDay } from 'twenty-shared/constants'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; @@ -19,6 +23,7 @@ import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { AxisNameDisplay } from 'src/engine/core-modules/page-layout/enums/axis-name-display.enum'; import { BarChartGroupMode } from 'src/engine/core-modules/page-layout/enums/bar-chart-group-mode.enum'; +import { ObjectRecordGroupByDateGranularity } from 'src/engine/core-modules/page-layout/enums/date-granularity.enum'; import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum'; import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; @@ -49,6 +54,14 @@ export class BarChartConfigurationDTO { @IsOptional() primaryAxisGroupBySubFieldName?: string; + @Field(() => ObjectRecordGroupByDateGranularity, { + nullable: true, + defaultValue: ObjectRecordGroupByDateGranularity.DAY, + }) + @IsEnum(ObjectRecordGroupByDateGranularity) + @IsOptional() + primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity; + @Field(() => GraphOrderBy, { nullable: true }) @IsEnum(GraphOrderBy) @IsOptional() @@ -64,6 +77,14 @@ export class BarChartConfigurationDTO { @IsOptional() secondaryAxisGroupBySubFieldName?: string; + @Field(() => ObjectRecordGroupByDateGranularity, { + nullable: true, + defaultValue: ObjectRecordGroupByDateGranularity.DAY, + }) + @IsEnum(ObjectRecordGroupByDateGranularity) + @IsOptional() + secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity; + @Field(() => GraphOrderBy, { nullable: true }) @IsEnum(GraphOrderBy) @IsOptional() @@ -119,4 +140,15 @@ export class BarChartConfigurationDTO { @IsEnum(BarChartGroupMode) @IsOptional() groupMode?: BarChartGroupMode; + + @Field(() => String, { nullable: true, defaultValue: 'UTC' }) + @IsTimeZone() + @IsOptional() + timezone?: string; + + @Field(() => Int, { nullable: true, defaultValue: CalendarStartDay.MONDAY }) + @IsOptional() + @Min(0) + @Max(7) + firstDayOfTheWeek?: number; } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts index 6998eab488..67b5bf0307 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; import { IsBoolean, @@ -7,9 +7,13 @@ import { IsObject, IsOptional, IsString, + IsTimeZone, IsUUID, + Max, + Min, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { CalendarStartDay } from 'twenty-shared/constants'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; @@ -53,4 +57,15 @@ export class GaugeChartConfigurationDTO { @IsObject() @IsOptional() filter?: ObjectRecordFilter; + + @Field(() => String, { nullable: true, defaultValue: 'UTC' }) + @IsTimeZone() + @IsOptional() + timezone?: string; + + @Field(() => Int, { nullable: true, defaultValue: CalendarStartDay.MONDAY }) + @IsOptional() + @Min(0) + @Max(7) + firstDayOfTheWeek?: number; } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts index 1932161c9c..8f34d0dc7d 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; import { IsBoolean, @@ -8,15 +8,20 @@ import { IsObject, IsOptional, IsString, + IsTimeZone, IsUUID, + Max, + Min, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { CalendarStartDay } from 'twenty-shared/constants'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { AxisNameDisplay } from 'src/engine/core-modules/page-layout/enums/axis-name-display.enum'; +import { ObjectRecordGroupByDateGranularity } from 'src/engine/core-modules/page-layout/enums/date-granularity.enum'; import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum'; import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; @@ -47,6 +52,14 @@ export class LineChartConfigurationDTO { @IsOptional() primaryAxisGroupBySubFieldName?: string; + @Field(() => ObjectRecordGroupByDateGranularity, { + nullable: true, + defaultValue: ObjectRecordGroupByDateGranularity.DAY, + }) + @IsEnum(ObjectRecordGroupByDateGranularity) + @IsOptional() + primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity; + @Field(() => GraphOrderBy, { nullable: true, defaultValue: GraphOrderBy.FIELD_ASC, @@ -65,6 +78,14 @@ export class LineChartConfigurationDTO { @IsOptional() secondaryAxisGroupBySubFieldName?: string; + @Field(() => ObjectRecordGroupByDateGranularity, { + nullable: true, + defaultValue: ObjectRecordGroupByDateGranularity.DAY, + }) + @IsEnum(ObjectRecordGroupByDateGranularity) + @IsOptional() + secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity; + @Field(() => GraphOrderBy, { nullable: true }) @IsEnum(GraphOrderBy) @IsOptional() @@ -112,4 +133,15 @@ export class LineChartConfigurationDTO { @IsObject() @IsOptional() filter?: ObjectRecordFilter; + + @Field(() => String, { nullable: true, defaultValue: 'UTC' }) + @IsTimeZone() + @IsOptional() + timezone?: string; + + @Field(() => Int, { nullable: true, defaultValue: CalendarStartDay.MONDAY }) + @IsOptional() + @Min(0) + @Max(7) + firstDayOfTheWeek?: number; } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts index b7fac15339..5e3f75120e 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; import { IsBoolean, @@ -7,14 +7,19 @@ import { IsObject, IsOptional, IsString, + IsTimeZone, IsUUID, + Max, + Min, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { CalendarStartDay } from 'twenty-shared/constants'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ObjectRecordGroupByDateGranularity } from 'src/engine/core-modules/page-layout/enums/date-granularity.enum'; import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum'; import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; @@ -45,6 +50,14 @@ export class PieChartConfigurationDTO { @IsOptional() groupBySubFieldName?: string; + @Field(() => ObjectRecordGroupByDateGranularity, { + nullable: true, + defaultValue: ObjectRecordGroupByDateGranularity.DAY, + }) + @IsEnum(ObjectRecordGroupByDateGranularity) + @IsOptional() + dateGranularity?: ObjectRecordGroupByDateGranularity; + @Field(() => GraphOrderBy, { nullable: true, defaultValue: GraphOrderBy.VALUE_DESC, @@ -72,4 +85,15 @@ export class PieChartConfigurationDTO { @IsObject() @IsOptional() filter?: ObjectRecordFilter; + + @Field(() => String, { nullable: true, defaultValue: 'UTC' }) + @IsTimeZone() + @IsOptional() + timezone?: string; + + @Field(() => Int, { nullable: true, defaultValue: CalendarStartDay.MONDAY }) + @IsOptional() + @Min(0) + @Max(7) + firstDayOfTheWeek?: number; } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/date-granularity.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/date-granularity.enum.ts new file mode 100644 index 0000000000..75254febfd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/date-granularity.enum.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from '@nestjs/graphql'; + +import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; + +registerEnumType(ObjectRecordGroupByDateGranularity, { + name: 'ObjectRecordGroupByDateGranularity', + description: + 'Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR)', +}); + +export { ObjectRecordGroupByDateGranularity }; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts index 889927501e..be79ad60e9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts @@ -1,3 +1,4 @@ +import { CalendarStartDay } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; @@ -98,6 +99,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: opportunityAmountFieldId, aggregateOperation: AggregateOperations.SUM, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -120,6 +123,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: rocketIdFieldId, aggregateOperation: AggregateOperations.COUNT, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: rocketObject?.id ?? null, @@ -147,6 +152,8 @@ export const getPageLayoutWidgetDataSeeds = ( primaryAxisOrderBy: 'FIELD_ASC', axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -177,6 +184,8 @@ export const getPageLayoutWidgetDataSeeds = ( axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, color: 'auto', + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -206,6 +215,8 @@ export const getPageLayoutWidgetDataSeeds = ( axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, color: 'auto', + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: rocketObject?.id ?? null, @@ -228,6 +239,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: opportunityIdFieldId, aggregateOperation: AggregateOperations.COUNT, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -252,6 +265,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: companyIdFieldId, aggregateOperation: AggregateOperations.COUNT, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -278,6 +293,8 @@ export const getPageLayoutWidgetDataSeeds = ( primaryAxisOrderBy: 'FIELD_ASC', axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -305,10 +322,13 @@ export const getPageLayoutWidgetDataSeeds = ( primaryAxisGroupByFieldMetadataId: companyEmployeesFieldId, secondaryAxisGroupByFieldMetadataId: companyAddressFieldId, secondaryAxisGroupBySubFieldName: 'addressCity', + secondaryAxisGroupByDateGranularity: 'DAY', primaryAxisOrderBy: 'FIELD_ASC', axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, color: 'auto', + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -333,6 +353,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: companyArrFieldId, aggregateOperation: AggregateOperations.SUM, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -358,6 +380,8 @@ export const getPageLayoutWidgetDataSeeds = ( groupByFieldMetadataId: companyNameFieldId, orderBy: 'VALUE_DESC', displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -380,6 +404,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: companyArrFieldId, aggregateOperation: AggregateOperations.AVG, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -402,6 +428,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: companyLinkedinLinkFieldId, aggregateOperation: AggregateOperations.COUNT, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -427,6 +455,8 @@ export const getPageLayoutWidgetDataSeeds = ( groupByFieldMetadataId: companyLinkedinLinkFieldId, orderBy: 'VALUE_DESC', displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: companyObject?.id ?? null, @@ -448,6 +478,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: personIdFieldId, aggregateOperation: AggregateOperations.COUNT, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: personObject?.id ?? null, @@ -475,6 +507,8 @@ export const getPageLayoutWidgetDataSeeds = ( axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, color: 'auto', + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: personObject?.id ?? null, @@ -502,6 +536,8 @@ export const getPageLayoutWidgetDataSeeds = ( groupByFieldMetadataId: personJobTitleFieldId, orderBy: 'VALUE_DESC', displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: personObject?.id ?? null, @@ -521,6 +557,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateFieldMetadataId: taskIdFieldId, aggregateOperation: AggregateOperations.COUNT, displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, } : null, objectMetadataId: taskObject?.id ?? null, diff --git a/packages/twenty-front/src/modules/localization/constants/CalendarStartDay.ts b/packages/twenty-shared/src/constants/CalendarStartDay.ts similarity index 100% rename from packages/twenty-front/src/modules/localization/constants/CalendarStartDay.ts rename to packages/twenty-shared/src/constants/CalendarStartDay.ts diff --git a/packages/twenty-shared/src/constants/index.ts b/packages/twenty-shared/src/constants/index.ts index adcf5b4fae..e287f5c6b8 100644 --- a/packages/twenty-shared/src/constants/index.ts +++ b/packages/twenty-shared/src/constants/index.ts @@ -7,6 +7,7 @@ * |___/ */ +export { CalendarStartDay } from './CalendarStartDay'; export { COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES } from './CompositeFieldTypeSubFieldsNames'; export { CurrencyCode } from './CurrencyCode'; export { CURRENCY_CODE_LABELS } from './CurrencyCodeLabels'; diff --git a/packages/twenty-shared/src/index.ts b/packages/twenty-shared/src/index.ts index 4b2fef6e10..fa453dd42d 100644 --- a/packages/twenty-shared/src/index.ts +++ b/packages/twenty-shared/src/index.ts @@ -7,4 +7,6 @@ * |___/ */ +export { CalendarStartDay } from './constants'; + export default {};