diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts index fd33201e92..c511f669d7 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts @@ -1,5 +1,6 @@ import { plainToInstance } from 'class-transformer'; import { validateSync, type ValidationError } from 'class-validator'; +import { isDefined } from 'twenty-shared/utils'; import { AggregateChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/aggregate-chart-configuration.dto'; import { BarChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto'; @@ -8,6 +9,7 @@ import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto'; import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto'; import { type WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; +import { BarChartGroupMode } from 'src/engine/core-modules/page-layout/enums/bar-chart-group-mode.enum'; import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; @@ -58,6 +60,13 @@ const validateGraphConfiguration = ({ throw errors; } + if ( + isDefined(instance.secondaryAxisGroupByFieldMetadataId) && + !isDefined(instance.groupMode) + ) { + instance.groupMode = BarChartGroupMode.STACKED; + } + return instance; } case GraphType.LINE: { @@ -75,6 +84,13 @@ const validateGraphConfiguration = ({ throw errors; } + if ( + isDefined(instance.secondaryAxisGroupByFieldMetadataId) && + !isDefined(instance.isStacked) + ) { + instance.isStacked = true; + } + return instance; } case GraphType.PIE: { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds-v2.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds-v2.util.ts new file mode 100644 index 0000000000..cab613fee9 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds-v2.util.ts @@ -0,0 +1,239 @@ +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'; +import { AxisNameDisplay } from 'src/engine/core-modules/page-layout/enums/axis-name-display.enum'; +import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; +import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant'; +import { PAGE_LAYOUT_WIDGET_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant'; +import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; + +type PageLayoutWidgetDataSeed = { + id: string; + pageLayoutTabId: string; + title: string; + type: WidgetType; + gridPosition: { + row: number; + column: number; + rowSpan: number; + columnSpan: number; + }; + configuration: Record | null; + objectMetadataId: string | null; +}; + +const getFieldId = ( + object: ObjectMetadataEntity | undefined, + fieldName: string, +): string | undefined => { + return object?.fields?.find((field) => field.name === fieldName)?.id; +}; + +export const getPageLayoutWidgetDataSeedsV2 = ( + workspaceId: string, + objectMetadataItems: ObjectMetadataEntity[], +): PageLayoutWidgetDataSeed[] => { + const opportunityObject = objectMetadataItems.find( + (obj) => obj.standardId === STANDARD_OBJECT_IDS.opportunity, + ); + const companyObject = objectMetadataItems.find( + (obj) => obj.standardId === STANDARD_OBJECT_IDS.company, + ); + const personObject = objectMetadataItems.find( + (obj) => obj.standardId === STANDARD_OBJECT_IDS.person, + ); + + const opportunityAmountFieldId = getFieldId(opportunityObject, 'amount'); + const opportunityCloseDateFieldId = getFieldId( + opportunityObject, + 'closeDate', + ); + + const companyIdFieldId = getFieldId(companyObject, 'id'); + const companyCreatedAtFieldId = getFieldId(companyObject, 'createdAt'); + const companyArrFieldId = getFieldId(companyObject, 'annualRecurringRevenue'); + const companyNameFieldId = getFieldId(companyObject, 'name'); + const companyLinkedinLinkFieldId = getFieldId(companyObject, 'linkedinLink'); + + const personIdFieldId = getFieldId(personObject, 'id'); + const personJobTitleFieldId = getFieldId(personObject, 'jobTitle'); + + return [ + // LINE chart: Revenue Forecast (Sales Overview) + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.SALES_REVENUE_FORECAST, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, + ), + title: 'Revenue Forecast', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 7, rowSpan: 8, columnSpan: 5 }, + configuration: + isDefined(opportunityAmountFieldId) && + isDefined(opportunityCloseDateFieldId) + ? { + graphType: 'LINE', + aggregateFieldMetadataId: opportunityAmountFieldId, + aggregateOperation: AggregateOperations.SUM, + primaryAxisGroupByFieldMetadataId: opportunityCloseDateFieldId, + primaryAxisOrderBy: 'FIELD_ASC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, + } + : null, + objectMetadataId: opportunityObject?.id ?? null, + }, + + // LINE chart: New Customers Over Time (Customer Overview) + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_NEW_OVER_TIME, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, + ), + title: 'New Customers Over Time', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 3, rowSpan: 6, columnSpan: 5 }, + configuration: + isDefined(companyIdFieldId) && isDefined(companyCreatedAtFieldId) + ? { + graphType: 'LINE', + aggregateFieldMetadataId: companyIdFieldId, + aggregateOperation: AggregateOperations.COUNT, + primaryAxisGroupByFieldMetadataId: companyCreatedAtFieldId, + primaryAxisOrderBy: 'FIELD_ASC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, + } + : null, + objectMetadataId: companyObject?.id ?? null, + }, + + // PIE chart: Revenue Distribution (Customer Analytics) + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_REVENUE_DISTRIBUTION, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, + ), + title: 'Revenue Distribution', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 3 }, + configuration: + isDefined(companyArrFieldId) && isDefined(companyNameFieldId) + ? { + graphType: 'PIE', + aggregateFieldMetadataId: companyArrFieldId, + aggregateOperation: AggregateOperations.SUM, + groupByFieldMetadataId: companyNameFieldId, + orderBy: 'VALUE_DESC', + displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, + } + : null, + objectMetadataId: companyObject?.id ?? null, + }, + + // GAUGE chart: Average ARR (Customer Analytics) + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_AVERAGE_ARR, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, + ), + title: 'Average ARR', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 7, rowSpan: 6, columnSpan: 5 }, + configuration: isDefined(companyArrFieldId) + ? { + graphType: 'GAUGE', + aggregateFieldMetadataId: companyArrFieldId, + aggregateOperation: AggregateOperations.AVG, + displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, + } + : null, + objectMetadataId: companyObject?.id ?? null, + }, + + // PIE chart: Companies by LinkedIn (Customer Overview) + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_LINKEDIN_DISTRIBUTION, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, + ), + title: 'Companies by LinkedIn (Field Permission Test)', + type: WidgetType.GRAPH, + gridPosition: { row: 6, column: 0, rowSpan: 4, columnSpan: 6 }, + configuration: + isDefined(companyIdFieldId) && isDefined(companyLinkedinLinkFieldId) + ? { + graphType: 'PIE', + aggregateFieldMetadataId: companyIdFieldId, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataId: companyLinkedinLinkFieldId, + orderBy: 'VALUE_DESC', + displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, + } + : null, + objectMetadataId: companyObject?.id ?? null, + }, + + // PIE chart: Contact Roles (Team Metrics) + { + id: generateSeedId( + workspaceId, + PAGE_LAYOUT_WIDGET_SEEDS.TEAM_CONTACT_ROLES, + ), + pageLayoutTabId: generateSeedId( + workspaceId, + PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS, + ), + title: 'Contact Roles', + type: WidgetType.GRAPH, + gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 6 }, + configuration: + isDefined(personIdFieldId) && isDefined(personJobTitleFieldId) + ? { + graphType: 'PIE', + aggregateFieldMetadataId: personIdFieldId, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataId: personJobTitleFieldId, + orderBy: 'VALUE_DESC', + displayDataLabel: true, + timezone: 'UTC', + firstDayOfTheWeek: CalendarStartDay.MONDAY, + } + : null, + objectMetadataId: personObject?.id ?? null, + }, + ]; +}; 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 be79ad60e9..2f2ebf3414 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 @@ -9,6 +9,7 @@ import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-me import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant'; import { PAGE_LAYOUT_WIDGET_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant'; import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util'; +import { getPageLayoutWidgetDataSeedsV2 } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds-v2.util'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; type PageLayoutWidgetDataSeed = { @@ -36,6 +37,7 @@ const getFieldId = ( export const getPageLayoutWidgetDataSeeds = ( workspaceId: string, objectMetadataItems: ObjectMetadataEntity[], + isDashboardV2Enabled: boolean, ): PageLayoutWidgetDataSeed[] => { const opportunityObject = objectMetadataItems.find( (obj) => obj.standardId === STANDARD_OBJECT_IDS.opportunity, @@ -61,16 +63,13 @@ export const getPageLayoutWidgetDataSeeds = ( const opportunityStageFieldId = getFieldId(opportunityObject, 'stage'); const companyIdFieldId = getFieldId(companyObject, 'id'); - const companyCreatedAtFieldId = getFieldId(companyObject, 'createdAt'); const companyEmployeesFieldId = getFieldId(companyObject, 'employees'); const companyArrFieldId = getFieldId(companyObject, 'annualRecurringRevenue'); - const companyNameFieldId = getFieldId(companyObject, 'name'); const companyLinkedinLinkFieldId = getFieldId(companyObject, 'linkedinLink'); const companyAddressFieldId = getFieldId(companyObject, 'address'); const personIdFieldId = getFieldId(personObject, 'id'); const personCityFieldId = getFieldId(personObject, 'city'); - const personJobTitleFieldId = getFieldId(personObject, 'jobTitle'); const opportunityIdFieldId = getFieldId(opportunityObject, 'id'); @@ -79,7 +78,7 @@ export const getPageLayoutWidgetDataSeeds = ( const rocketIdFieldId = getFieldId(rocketObject, 'id'); const rocketCreatedAtFieldId = getFieldId(rocketObject, 'createdAt'); - return [ + const v1Widgets = [ // Sales Overview Tab Widgets { id: generateSeedId( @@ -129,35 +128,6 @@ export const getPageLayoutWidgetDataSeeds = ( : null, objectMetadataId: rocketObject?.id ?? null, }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.SALES_REVENUE_FORECAST, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW, - ), - title: 'Revenue Forecast', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 7, rowSpan: 8, columnSpan: 5 }, - configuration: - isDefined(opportunityAmountFieldId) && - isDefined(opportunityCloseDateFieldId) - ? { - graphType: 'LINE', - aggregateFieldMetadataId: opportunityAmountFieldId, - aggregateOperation: AggregateOperations.SUM, - primaryAxisGroupByFieldMetadataId: opportunityCloseDateFieldId, - primaryAxisOrderBy: 'FIELD_ASC', - axisNameDisplay: AxisNameDisplay.NONE, - displayDataLabel: false, - timezone: 'UTC', - firstDayOfTheWeek: CalendarStartDay.MONDAY, - } - : null, - objectMetadataId: opportunityObject?.id ?? null, - }, { id: generateSeedId( workspaceId, @@ -271,34 +241,6 @@ export const getPageLayoutWidgetDataSeeds = ( : null, objectMetadataId: companyObject?.id ?? null, }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_NEW_OVER_TIME, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, - ), - title: 'New Customers Over Time', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 3, rowSpan: 6, columnSpan: 5 }, - configuration: - isDefined(companyIdFieldId) && isDefined(companyCreatedAtFieldId) - ? { - graphType: 'LINE', - aggregateFieldMetadataId: companyIdFieldId, - aggregateOperation: AggregateOperations.COUNT, - primaryAxisGroupByFieldMetadataId: companyCreatedAtFieldId, - primaryAxisOrderBy: 'FIELD_ASC', - axisNameDisplay: AxisNameDisplay.NONE, - displayDataLabel: false, - timezone: 'UTC', - firstDayOfTheWeek: CalendarStartDay.MONDAY, - } - : null, - objectMetadataId: companyObject?.id ?? null, - }, { id: generateSeedId( workspaceId, @@ -359,57 +301,6 @@ export const getPageLayoutWidgetDataSeeds = ( : null, objectMetadataId: companyObject?.id ?? null, }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_REVENUE_DISTRIBUTION, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, - ), - title: 'Revenue Distribution', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 3 }, - configuration: - isDefined(companyArrFieldId) && isDefined(companyNameFieldId) - ? { - graphType: 'PIE', - aggregateFieldMetadataId: companyArrFieldId, - aggregateOperation: AggregateOperations.SUM, - groupByFieldMetadataId: companyNameFieldId, - orderBy: 'VALUE_DESC', - displayDataLabel: true, - timezone: 'UTC', - firstDayOfTheWeek: CalendarStartDay.MONDAY, - } - : null, - objectMetadataId: companyObject?.id ?? null, - }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_AVERAGE_ARR, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS, - ), - title: 'Average ARR', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 7, rowSpan: 6, columnSpan: 5 }, - configuration: isDefined(companyArrFieldId) - ? { - graphType: 'GAUGE', - aggregateFieldMetadataId: companyArrFieldId, - aggregateOperation: AggregateOperations.AVG, - displayDataLabel: true, - timezone: 'UTC', - firstDayOfTheWeek: CalendarStartDay.MONDAY, - } - : null, - objectMetadataId: companyObject?.id ?? null, - }, { id: generateSeedId( workspaceId, @@ -434,33 +325,6 @@ export const getPageLayoutWidgetDataSeeds = ( : null, objectMetadataId: companyObject?.id ?? null, }, - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_LINKEDIN_DISTRIBUTION, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW, - ), - title: 'Companies by LinkedIn (Field Permission Test)', - type: WidgetType.GRAPH, - gridPosition: { row: 6, column: 0, rowSpan: 4, columnSpan: 6 }, - configuration: - isDefined(companyIdFieldId) && isDefined(companyLinkedinLinkFieldId) - ? { - graphType: 'PIE', - aggregateFieldMetadataId: companyIdFieldId, - aggregateOperation: AggregateOperations.COUNT, - groupByFieldMetadataId: companyLinkedinLinkFieldId, - orderBy: 'VALUE_DESC', - displayDataLabel: true, - timezone: 'UTC', - firstDayOfTheWeek: CalendarStartDay.MONDAY, - } - : null, - objectMetadataId: companyObject?.id ?? null, - }, // Team Overview Tab Widgets { @@ -515,33 +379,6 @@ export const getPageLayoutWidgetDataSeeds = ( }, // Team Metrics Tab Widgets - { - id: generateSeedId( - workspaceId, - PAGE_LAYOUT_WIDGET_SEEDS.TEAM_CONTACT_ROLES, - ), - pageLayoutTabId: generateSeedId( - workspaceId, - PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS, - ), - title: 'Contact Roles', - type: WidgetType.GRAPH, - gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 6 }, - configuration: - isDefined(personIdFieldId) && isDefined(personJobTitleFieldId) - ? { - graphType: 'PIE', - aggregateFieldMetadataId: personIdFieldId, - aggregateOperation: AggregateOperations.COUNT, - groupByFieldMetadataId: personJobTitleFieldId, - orderBy: 'VALUE_DESC', - displayDataLabel: true, - timezone: 'UTC', - firstDayOfTheWeek: CalendarStartDay.MONDAY, - } - : null, - objectMetadataId: personObject?.id ?? null, - }, { id: generateSeedId(workspaceId, PAGE_LAYOUT_WIDGET_SEEDS.TEAM_OPEN_TASKS), pageLayoutTabId: generateSeedId( @@ -564,4 +401,11 @@ export const getPageLayoutWidgetDataSeeds = ( objectMetadataId: taskObject?.id ?? null, }, ]; + + return [ + ...v1Widgets, + ...(isDashboardV2Enabled + ? getPageLayoutWidgetDataSeedsV2(workspaceId, objectMetadataItems) + : []), + ]; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts index 481cb58861..cb2bf774fe 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util.ts @@ -20,6 +20,7 @@ export const seedPageLayoutWidgets = async ({ const pageLayoutWidgets = getPageLayoutWidgetDataSeeds( workspaceId, objectMetadataItems, + isDashboardV2Enabled, ).map((widget) => { const validatedConfiguration = widget.configuration ? validateAndTransformWidgetConfiguration({