From 05a28a8ec2de4dc4afdb15a953e291f2adc264e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:29:08 +0100 Subject: [PATCH] Create a feature flag for dashboards v2 (#15601) Prevent users from creating v2 chart types via the api. Only created unit tests and not integration tests (since it's not that important, and the v2 will be released soon), but tested via the api playground. --- .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 1 + .../components/ChartTypeSelectionSection.tsx | 21 +- .../enums/feature-flag-key.enum.ts | 1 + .../page-layout/page-layout.module.ts | 2 + .../page-layout-widget.service.spec.ts | 7 + .../services/page-layout-widget.service.ts | 33 +- ...is-widget-configuration-valid.util.spec.ts | 43 --- ...ransform-widget-configuration.util.spec.ts | 292 ++++++++++-------- .../is-widget-configuration-valid.util.ts | 18 -- ...and-transform-widget-configuration.util.ts | 38 ++- .../workspace-entity-manager.spec.ts | 3 + .../core/utils/seed-feature-flags.util.ts | 5 + .../utils/seed-page-layout-widgets.util.ts | 28 +- .../dev-seeder/services/dev-seeder.service.ts | 15 +- 15 files changed, 273 insertions(+), 235 deletions(-) delete mode 100644 packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/is-widget-configuration-valid.util.spec.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 535c59768f..8b160b8c85 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1255,6 +1255,7 @@ export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', + IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 9cbd9a2caf..55e0968424 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1219,6 +1219,7 @@ export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', + IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx index 9571465963..0beed69933 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx @@ -1,6 +1,7 @@ import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; -import { GraphType } from '~/generated-metadata/graphql'; +import { FeatureFlagKey, GraphType } from '~/generated-metadata/graphql'; import { t } from '@lingui/core/macro'; import { MenuPicker } from 'twenty-ui/navigation'; @@ -14,12 +15,6 @@ const graphTypeOptions = [ GraphType.GAUGE, ]; -const disabledGraphTypeOptions = [ - GraphType.PIE, - GraphType.LINE, - GraphType.GAUGE, -]; - const StyledChartTypeSelectionContainer = styled.div` display: flex; flex-direction: row; @@ -35,10 +30,20 @@ export const ChartTypeSelectionSection = ({ currentGraphType, setCurrentGraphType, }: ChartTypeSelectionSectionProps) => { + const isDashboardV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IS_DASHBOARD_V2_ENABLED, + ); + return ( {graphTypeOptions.map((graphType) => { - const isDisabled = disabledGraphTypeOptions.includes(graphType); + const isChartV2Type = [ + GraphType.PIE, + GraphType.LINE, + GraphType.GAUGE, + ].includes(graphType); + + const isDisabled = isChartV2Type && !isDashboardV2Enabled; return ( { findByIdOrThrow: jest.fn(), }, }, + { + provide: FeatureFlagService, + useValue: { + isFeatureEnabled: jest.fn(), + }, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts index 9c176be778..b6908a15ed 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/services/page-layout-widget.service.ts @@ -5,6 +5,8 @@ import { isDefined } from 'twenty-shared/utils'; import { EntityManager, IsNull, Repository } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { CreatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout-widget.input'; import { UpdatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget.input'; import { WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; @@ -29,6 +31,7 @@ export class PageLayoutWidgetService { @InjectRepository(PageLayoutWidgetEntity) private readonly pageLayoutWidgetRepository: Repository, private readonly pageLayoutTabService: PageLayoutTabService, + private readonly featureFlagService: FeatureFlagService, ) {} private getPageLayoutWidgetRepository( @@ -131,11 +134,18 @@ export class PageLayoutWidgetService { let validatedConfig: WidgetConfigurationInterface | null = null; if (pageLayoutWidgetData.configuration && pageLayoutWidgetData.type) { - try { - validatedConfig = validateAndTransformWidgetConfiguration( - pageLayoutWidgetData.type, - pageLayoutWidgetData.configuration, + const isDashboardV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DASHBOARD_V2_ENABLED, + workspaceId, ); + + try { + validatedConfig = validateAndTransformWidgetConfiguration({ + type: pageLayoutWidgetData.type, + configuration: pageLayoutWidgetData.configuration, + isDashboardV2Enabled, + }); } catch (error) { throw new PageLayoutWidgetException( generatePageLayoutWidgetExceptionMessage( @@ -228,11 +238,18 @@ export class PageLayoutWidgetService { const titleForError = updateData.title ?? existingWidget.title; if (typeForValidation) { - try { - validatedConfig = validateAndTransformWidgetConfiguration( - typeForValidation, - updateData.configuration, + const isDashboardV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DASHBOARD_V2_ENABLED, + workspaceId, ); + + try { + validatedConfig = validateAndTransformWidgetConfiguration({ + type: typeForValidation, + configuration: updateData.configuration, + isDashboardV2Enabled, + }); } catch (error) { throw new PageLayoutWidgetException( generatePageLayoutWidgetExceptionMessage( diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/is-widget-configuration-valid.util.spec.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/is-widget-configuration-valid.util.spec.ts deleted file mode 100644 index 6b4e4876b0..0000000000 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/is-widget-configuration-valid.util.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - INVALID_IFRAME_CONFIG_BAD_URL, - TEST_IFRAME_CONFIG, - TEST_NUMBER_CHART_CONFIG, -} from 'test/integration/constants/widget-configuration-test-data.constants'; - -import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; -import { isWidgetConfigurationValid } from 'src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util'; - -describe('isWidgetConfigurationValid', () => { - it('should return true for valid configuration', () => { - const result = isWidgetConfigurationValid( - WidgetType.IFRAME, - TEST_IFRAME_CONFIG, - ); - - expect(result).toBe(true); - }); - - it('should return false for invalid configuration', () => { - const result = isWidgetConfigurationValid( - WidgetType.IFRAME, - INVALID_IFRAME_CONFIG_BAD_URL, - ); - - expect(result).toBe(false); - }); - - it('should return false for null configuration', () => { - const result = isWidgetConfigurationValid(WidgetType.IFRAME, null); - - expect(result).toBe(false); - }); - - it('should return false for unsupported widget type', () => { - const result = isWidgetConfigurationValid( - 'UNSUPPORTED' as WidgetType, - TEST_NUMBER_CHART_CONFIG, - ); - - expect(result).toBe(false); - }); -}); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts index 2e486691c1..f532f54f66 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts @@ -6,16 +6,13 @@ import { INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, TEST_GAUGE_CHART_CONFIG, - TEST_GAUGE_CHART_CONFIG_MINIMAL, TEST_HORIZONTAL_BAR_CHART_CONFIG, TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL, TEST_IFRAME_CONFIG, TEST_LINE_CHART_CONFIG, - TEST_LINE_CHART_CONFIG_MINIMAL, TEST_NUMBER_CHART_CONFIG, TEST_NUMBER_CHART_CONFIG_MINIMAL, TEST_PIE_CHART_CONFIG, - TEST_PIE_CHART_CONFIG_MINIMAL, TEST_VERTICAL_BAR_CHART_CONFIG, TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL, } from 'test/integration/constants/widget-configuration-test-data.constants'; @@ -26,29 +23,32 @@ import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules describe('validateAndTransformWidgetConfiguration', () => { describe('IFRAME widget', () => { it('should validate and transform valid iframe configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.IFRAME, - TEST_IFRAME_CONFIG, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.IFRAME, + configuration: TEST_IFRAME_CONFIG, + isDashboardV2Enabled: false, + }); expect(result).toMatchObject(TEST_IFRAME_CONFIG); }); it('should throw error for invalid URL', () => { expect(() => - validateAndTransformWidgetConfiguration( - WidgetType.IFRAME, - INVALID_IFRAME_CONFIG_BAD_URL, - ), + validateAndTransformWidgetConfiguration({ + type: WidgetType.IFRAME, + configuration: INVALID_IFRAME_CONFIG_BAD_URL, + isDashboardV2Enabled: false, + }), ).toThrow(/url must be a URL address/); }); it('should throw error for empty URL', () => { expect(() => - validateAndTransformWidgetConfiguration( - WidgetType.IFRAME, - INVALID_IFRAME_CONFIG_EMPTY_URL, - ), + validateAndTransformWidgetConfiguration({ + type: WidgetType.IFRAME, + configuration: INVALID_IFRAME_CONFIG_EMPTY_URL, + isDashboardV2Enabled: false, + }), ).toThrow(/url must be a URL address/); }); }); @@ -56,170 +56,121 @@ describe('validateAndTransformWidgetConfiguration', () => { describe('GRAPH widget', () => { describe('NUMBER graph', () => { it('should validate full number graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_NUMBER_CHART_CONFIG, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_NUMBER_CHART_CONFIG, + isDashboardV2Enabled: false, + }); expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG); }); it('should validate minimal number graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_NUMBER_CHART_CONFIG_MINIMAL, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL, + isDashboardV2Enabled: false, + }); expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL); }); it('should throw error for partial number graph configuration with missing required fields', () => { expect(() => - validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, - ), + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, + isDashboardV2Enabled: false, + }), ).toThrow(/aggregateFieldMetadataId.*aggregateOperation/); }); it('should throw error for invalid UUID', () => { expect(() => - validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - INVALID_NUMBER_CHART_CONFIG_BAD_UUID, - ), + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID, + isDashboardV2Enabled: false, + }), ).toThrow(/aggregateFieldMetadataId must be a UUID/); }); }); describe('VERTICAL_BAR graph', () => { it('should validate full vertical bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_VERTICAL_BAR_CHART_CONFIG, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_VERTICAL_BAR_CHART_CONFIG, + isDashboardV2Enabled: false, + }); expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG); }); it('should validate minimal vertical bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL, + isDashboardV2Enabled: false, + }); expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL); }); it('should throw error for partial vertical bar graph configuration with missing required fields', () => { expect(() => - validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, - ), + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, + isDashboardV2Enabled: false, + }), ).toThrow(/primaryAxisGroupByFieldMetadataId/); }); }); describe('HORIZONTAL_BAR graph', () => { it('should validate full horizontal bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_HORIZONTAL_BAR_CHART_CONFIG, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG, + isDashboardV2Enabled: false, + }); expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG); }); it('should validate minimal horizontal bar graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL, + isDashboardV2Enabled: false, + }); expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL); }); it('should throw error for partial horizontal bar graph configuration with missing required fields', () => { expect(() => - validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, - ), + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY, + isDashboardV2Enabled: false, + }), ).toThrow(/primaryAxisGroupByFieldMetadataId/); }); }); - describe('LINE graph', () => { - it('should validate full line graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_LINE_CHART_CONFIG, - ); - - expect(result).toMatchObject(TEST_LINE_CHART_CONFIG); - }); - - it('should validate minimal line graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_LINE_CHART_CONFIG_MINIMAL, - ); - - expect(result).toMatchObject(TEST_LINE_CHART_CONFIG_MINIMAL); - }); - }); - - describe('PIE graph', () => { - it('should validate full pie graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_PIE_CHART_CONFIG, - ); - - expect(result).toMatchObject(TEST_PIE_CHART_CONFIG); - }); - - it('should validate minimal pie graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_PIE_CHART_CONFIG_MINIMAL, - ); - - expect(result).toMatchObject(TEST_PIE_CHART_CONFIG_MINIMAL); - }); - }); - - describe('GAUGE graph', () => { - it('should validate full gauge graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_GAUGE_CHART_CONFIG, - ); - - expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG); - }); - - it('should validate minimal gauge graph configuration', () => { - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - TEST_GAUGE_CHART_CONFIG_MINIMAL, - ); - - expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG_MINIMAL); - }); - }); - it('should return null for unsupported graph type', () => { const configuration = { graphType: 'UNSUPPORTED', viewId: '550e8400-e29b-41d4-a716-446655440000', }; - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - configuration, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: configuration, + isDashboardV2Enabled: false, + }); expect(result).toBeNull(); }); @@ -229,10 +180,11 @@ describe('validateAndTransformWidgetConfiguration', () => { viewId: '550e8400-e29b-41d4-a716-446655440000', }; - const result = validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - configuration, - ); + const result = validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: configuration, + isDashboardV2Enabled: false, + }); expect(result).toBeNull(); }); @@ -241,29 +193,42 @@ describe('validateAndTransformWidgetConfiguration', () => { describe('Edge cases', () => { it('should throw error for null configuration', () => { expect(() => - validateAndTransformWidgetConfiguration(WidgetType.IFRAME, null), + validateAndTransformWidgetConfiguration({ + type: WidgetType.IFRAME, + configuration: null, + isDashboardV2Enabled: false, + }), ).toThrow('Invalid configuration: not an object'); }); it('should throw error for undefined configuration', () => { expect(() => - validateAndTransformWidgetConfiguration(WidgetType.IFRAME, undefined), + validateAndTransformWidgetConfiguration({ + type: WidgetType.IFRAME, + configuration: undefined, + isDashboardV2Enabled: false, + }), ).toThrow('Invalid configuration: not an object'); }); it('should throw error for non-object configuration', () => { expect(() => - validateAndTransformWidgetConfiguration(WidgetType.IFRAME, 'string'), + validateAndTransformWidgetConfiguration({ + type: WidgetType.IFRAME, + configuration: 'string', + isDashboardV2Enabled: false, + }), ).toThrow('Invalid configuration: not an object'); }); it('should return null for unsupported widget type', () => { const configuration = { someField: 'value' }; - const result = validateAndTransformWidgetConfiguration( - 'UNSUPPORTED' as WidgetType, - configuration, - ); + const result = validateAndTransformWidgetConfiguration({ + type: 'UNSUPPORTED' as WidgetType, + configuration: configuration, + isDashboardV2Enabled: false, + }); expect(result).toBeNull(); }); @@ -272,11 +237,70 @@ describe('validateAndTransformWidgetConfiguration', () => { describe('Error messages', () => { it('should include validation details in error message', () => { expect(() => - validateAndTransformWidgetConfiguration( - WidgetType.GRAPH, - INVALID_NUMBER_CHART_CONFIG_BAD_UUID, - ), + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID, + isDashboardV2Enabled: false, + }), ).toThrow(/aggregateFieldMetadataId must be a UUID/); }); }); + + describe('Feature flags', () => { + it('should throw error for unsupported graph type', () => { + expect(() => + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_PIE_CHART_CONFIG, + isDashboardV2Enabled: false, + }), + ).toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/); + + expect(() => + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_LINE_CHART_CONFIG, + isDashboardV2Enabled: false, + }), + ).toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/); + + expect(() => + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_GAUGE_CHART_CONFIG, + isDashboardV2Enabled: false, + }), + ).toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/); + }); + + it('should not throw error when IS_DASHBOARD_V2_ENABLED feature flag is enabled', () => { + expect(() => + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_PIE_CHART_CONFIG, + isDashboardV2Enabled: true, + }), + ).not.toThrow(); + }); + + it('should not throw error when IS_DASHBOARD_V2_ENABLED feature flag is enabled', () => { + expect(() => + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_LINE_CHART_CONFIG, + isDashboardV2Enabled: true, + }), + ).not.toThrow(); + }); + + it('should not throw error when IS_DASHBOARD_V2_ENABLED feature flag is enabled', () => { + expect(() => + validateAndTransformWidgetConfiguration({ + type: WidgetType.GRAPH, + configuration: TEST_GAUGE_CHART_CONFIG, + isDashboardV2Enabled: true, + }), + ).not.toThrow(); + }); + }); }); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util.ts deleted file mode 100644 index cdcb7ba687..0000000000 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/is-widget-configuration-valid.util.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; -import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util'; - -export const isWidgetConfigurationValid = ( - type: WidgetType, - configuration: unknown, -): boolean => { - try { - const validatedConfig = validateAndTransformWidgetConfiguration( - type, - configuration, - ); - - return validatedConfig !== null; - } catch { - return false; - } -}; 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 b555af5537..fd33201e92 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 @@ -23,15 +23,27 @@ const formatValidationErrors = (errors: ValidationError[]): string => { .join('; '); }; -const validateGraphConfiguration = ( - configuration: Record, -): WidgetConfigurationInterface | null => { +const validateGraphConfiguration = ({ + configuration, + isDashboardV2Enabled, +}: { + configuration: Record; + isDashboardV2Enabled: boolean; +}): WidgetConfigurationInterface | null => { const graphType = configuration.graphType as GraphType; if (!graphType || !Object.values(GraphType).includes(graphType)) { return null; } + const v2ChartTypes = [GraphType.PIE, GraphType.LINE, GraphType.GAUGE]; + + if (v2ChartTypes.includes(graphType) && !isDashboardV2Enabled) { + throw new Error( + `Chart type ${graphType} requires IS_DASHBOARD_V2_ENABLED feature flag`, + ); + } + switch (graphType) { case GraphType.VERTICAL_BAR: case GraphType.HORIZONTAL_BAR: { @@ -135,10 +147,15 @@ const validateIframeConfiguration = ( return instance; }; -export const validateAndTransformWidgetConfiguration = ( - type: WidgetType, - configuration: unknown, -): WidgetConfigurationInterface | null => { +export const validateAndTransformWidgetConfiguration = ({ + type, + configuration, + isDashboardV2Enabled, +}: { + type: WidgetType; + configuration: unknown; + isDashboardV2Enabled: boolean; +}): WidgetConfigurationInterface | null => { if (!configuration || typeof configuration !== 'object') { throw new Error('Invalid configuration: not an object'); } @@ -146,9 +163,10 @@ export const validateAndTransformWidgetConfiguration = ( try { switch (type) { case WidgetType.GRAPH: - return validateGraphConfiguration( - configuration as Record, - ); + return validateGraphConfiguration({ + configuration: configuration as Record, + isDashboardV2Enabled, + }); case WidgetType.IFRAME: return validateIframeConfiguration(configuration); default: diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 1cce1e9861..c10c30b707 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -138,6 +138,7 @@ describe('WorkspaceEntityManager', () => { IS_PUBLIC_DOMAIN_ENABLED: false, IS_EMAILING_DOMAIN_ENABLED: false, IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false, + IS_DASHBOARD_V2_ENABLED: false, }, eventEmitterService: { emitMutationEvent: jest.fn(), @@ -163,6 +164,8 @@ describe('WorkspaceEntityManager', () => { IS_MESSAGE_FOLDER_CONTROL_ENABLED: false, IS_PUBLIC_DOMAIN_ENABLED: false, IS_EMAILING_DOMAIN_ENABLED: false, + IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false, + IS_DASHBOARD_V2_ENABLED: false, }, permissionsPerRoleId: {}, } as WorkspaceDataSource; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index 91c217a9f7..a2302e7262 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -81,6 +81,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IS_DASHBOARD_V2_ENABLED, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; 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 c0b0a28810..481cb58861 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 @@ -4,21 +4,29 @@ import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { getPageLayoutWidgetDataSeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util'; -export const seedPageLayoutWidgets = async ( - dataSource: DataSource, - schemaName: string, - workspaceId: string, - objectMetadataItems: ObjectMetadataEntity[], -) => { +export const seedPageLayoutWidgets = async ({ + dataSource, + schemaName, + workspaceId, + objectMetadataItems, + isDashboardV2Enabled, +}: { + dataSource: DataSource; + schemaName: string; + workspaceId: string; + objectMetadataItems: ObjectMetadataEntity[]; + isDashboardV2Enabled: boolean; +}) => { const pageLayoutWidgets = getPageLayoutWidgetDataSeeds( workspaceId, objectMetadataItems, ).map((widget) => { const validatedConfiguration = widget.configuration - ? validateAndTransformWidgetConfiguration( - widget.type, - widget.configuration, - ) + ? validateAndTransformWidgetConfiguration({ + type: widget.type, + configuration: widget.configuration, + isDashboardV2Enabled, + }) : null; return { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts index 91c291f455..88eeff8195 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts @@ -3,6 +3,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -89,12 +90,18 @@ export class DevSeederService { relations: { fields: true }, }); - await seedPageLayoutWidgets( - this.coreDataSource, - 'core', + const isDashboardV2Enabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DASHBOARD_V2_ENABLED, + workspaceId, + ); + + await seedPageLayoutWidgets({ + dataSource: this.coreDataSource, + schemaName: 'core', workspaceId, objectMetadataItems, - ); + isDashboardV2Enabled, + }); await this.devSeederDataService.seed({ schemaName: dataSourceMetadata.schema,