[BREAKING_CHANGE_DASHBOARDS][DASHBOARD_CACHE_FLUSH_REQUIRED] Refactor page layout widget configuration type (#16671)

# Introduction
In this pull-request we're refactoring the page layout widget
configuration entity to be containing its discriminated key simplifying
underlying code and maintainability
- Made the configuration and title non nullable
- Introduced a generic predicate for the `widgetConfigurationType`
- Upgraded command to remove `graphType` and insert new
`configurationType` to existing entries
- Migrated frontend to new type system

---------

Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
This commit is contained in:
Paul Rastoin
2025-12-29 15:46:59 +01:00
committed by GitHub
parent 2b9728230a
commit 4135a6473a
221 changed files with 3241 additions and 1947 deletions
@@ -38,7 +38,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
type: createPageLayoutWidgetInput.type ?? WidgetType.VIEW,
objectMetadataId: createPageLayoutWidgetInput.objectMetadataId ?? null,
gridPosition: createPageLayoutWidgetInput.gridPosition,
configuration: createPageLayoutWidgetInput.configuration ?? null,
configuration: createPageLayoutWidgetInput.configuration,
applicationId: workspaceCustomApplicationId,
};
};
@@ -0,0 +1,24 @@
import { AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
import { GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-configuration.dto';
import { LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
import { StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { type PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
export const ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYPE =
{
[WidgetConfigurationType.AGGREGATE_CHART]: AggregateChartConfigurationDTO,
[WidgetConfigurationType.BAR_CHART]: BarChartConfigurationDTO,
[WidgetConfigurationType.GAUGE_CHART]: GaugeChartConfigurationDTO,
[WidgetConfigurationType.IFRAME]: IframeConfigurationDTO,
[WidgetConfigurationType.LINE_CHART]: LineChartConfigurationDTO,
[WidgetConfigurationType.PIE_CHART]: PieChartConfigurationDTO,
[WidgetConfigurationType.STANDALONE_RICH_TEXT]:
StandaloneRichTextConfigurationDTO,
// TODO: Remove the partial when we have all the widget configuration types in the backend
} as const satisfies Partial<{
[P in WidgetConfigurationType]: new () => PageLayoutWidgetConfigurationBase;
}>;
@@ -4,6 +4,7 @@ import { Type } from 'class-transformer';
import {
IsBoolean,
IsEnum,
IsIn,
IsNotEmpty,
IsObject,
IsOptional,
@@ -22,14 +23,17 @@ import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-build
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 { RatioAggregateConfigDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/ratio-aggregate-config.dto';
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('AggregateChartConfiguration')
export class AggregateChartConfigurationDTO {
@Field(() => GraphType)
@IsEnum(GraphType)
export class AggregateChartConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.AGGREGATE_CHART])
@IsNotEmpty()
graphType: GraphType.AGGREGATE;
configurationType: WidgetConfigurationType.AGGREGATE_CHART;
@Field(() => UUIDScalarType)
@IsUUID()
@@ -23,16 +23,20 @@ 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/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
import { BarChartGroupMode } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-group-mode.enum';
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
import { ObjectRecordGroupByDateGranularity } from 'src/engine/metadata-modules/page-layout-widget/enums/date-granularity.enum';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('BarChartConfiguration')
export class BarChartConfigurationDTO {
@Field(() => GraphType)
@IsIn([GraphType.VERTICAL_BAR, GraphType.HORIZONTAL_BAR])
export class BarChartConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.BAR_CHART])
@IsNotEmpty()
graphType: GraphType.VERTICAL_BAR | GraphType.HORIZONTAL_BAR;
configurationType: WidgetConfigurationType.BAR_CHART;
@Field(() => UUIDScalarType)
@IsUUID()
@@ -145,6 +149,11 @@ export class BarChartConfigurationDTO {
@IsOptional()
groupMode?: BarChartGroupMode;
@Field(() => BarChartLayout)
@IsEnum(BarChartLayout)
@IsNotEmpty()
layout: BarChartLayout;
@Field(() => Boolean, {
nullable: true,
})
@@ -3,6 +3,7 @@ import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsIn,
IsNotEmpty,
IsObject,
IsOptional,
@@ -19,14 +20,17 @@ import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-build
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 { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('GaugeChartConfiguration')
export class GaugeChartConfigurationDTO {
@Field(() => GraphType)
@IsEnum(GraphType)
export class GaugeChartConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.GAUGE_CHART])
@IsNotEmpty()
graphType: GraphType.GAUGE;
configurationType: WidgetConfigurationType.GAUGE_CHART;
@Field(() => UUIDScalarType)
@IsUUID()
@@ -1,9 +1,19 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsOptional, IsString, IsUrl } from 'class-validator';
import { IsIn, IsNotEmpty, IsOptional, IsString, IsUrl } from 'class-validator';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('IframeConfiguration')
export class IframeConfigurationDTO {
export class IframeConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.IFRAME])
@IsNotEmpty()
configurationType: WidgetConfigurationType.IFRAME;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
@@ -15,6 +15,7 @@ import { GraphQLJSON } from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
@InputType()
export class CreatePageLayoutWidgetInput {
@@ -28,10 +29,10 @@ export class CreatePageLayoutWidgetInput {
@IsNotEmpty()
title: string;
@Field(() => WidgetType, { nullable: true, defaultValue: WidgetType.VIEW })
@Field(() => WidgetType, { nullable: false })
@IsEnum(WidgetType)
@IsOptional()
type?: WidgetType;
type: WidgetType;
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@@ -43,8 +44,8 @@ export class CreatePageLayoutWidgetInput {
@Type(() => GridPositionInput)
gridPosition: GridPositionInput;
@Field(() => GraphQLJSON, { nullable: true })
@Field(() => GraphQLJSON, { nullable: false })
@IsObject()
@IsOptional()
configuration?: Record<string, unknown> | null;
configuration: AllPageLayoutWidgetConfiguration;
}
@@ -15,6 +15,7 @@ import { GraphQLJSON } from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
@InputType()
export class UpdatePageLayoutWidgetWithIdInput {
@@ -52,5 +53,5 @@ export class UpdatePageLayoutWidgetWithIdInput {
@Field(() => GraphQLJSON, { nullable: true })
@IsObject()
@IsOptional()
configuration: Record<string, unknown> | null;
configuration: AllPageLayoutWidgetConfiguration;
}
@@ -14,6 +14,7 @@ import { GraphQLJSON } from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
@InputType()
export class UpdatePageLayoutWidgetInput {
@@ -41,5 +42,5 @@ export class UpdatePageLayoutWidgetInput {
@Field(() => GraphQLJSON, { nullable: true })
@IsObject()
@IsOptional()
configuration?: Record<string, unknown> | null;
configuration?: AllPageLayoutWidgetConfiguration;
}
@@ -3,6 +3,7 @@ import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsIn,
IsNotEmpty,
IsNumber,
IsObject,
@@ -23,14 +24,17 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
import { ObjectRecordGroupByDateGranularity } from 'src/engine/metadata-modules/page-layout-widget/enums/date-granularity.enum';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('LineChartConfiguration')
export class LineChartConfigurationDTO {
@Field(() => GraphType)
@IsEnum(GraphType)
export class LineChartConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.LINE_CHART])
@IsNotEmpty()
graphType: GraphType.LINE;
configurationType: WidgetConfigurationType.LINE_CHART;
@Field(() => UUIDScalarType)
@IsUUID()
@@ -3,11 +3,9 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
WidgetConfiguration,
WidgetConfigurationInterface,
} from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
registerEnumType(WidgetType, { name: 'WidgetType' });
@@ -41,13 +39,13 @@ export class PageLayoutWidgetDTO {
type: WidgetType;
@Field(() => UUIDScalarType, { nullable: true })
objectMetadataId: string | null;
objectMetadataId?: string;
@Field(() => GridPositionDTO, { nullable: false })
gridPosition: GridPositionDTO;
@Field(() => WidgetConfiguration, { nullable: true })
configuration: WidgetConfigurationInterface | null;
@Field(() => WidgetConfiguration, { nullable: false })
configuration: AllPageLayoutWidgetConfiguration;
@Field()
createdAt: Date;
@@ -56,5 +54,5 @@ export class PageLayoutWidgetDTO {
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
deletedAt?: Date;
}
@@ -3,6 +3,7 @@ import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsIn,
IsNotEmpty,
IsObject,
IsOptional,
@@ -21,14 +22,17 @@ import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ObjectRecordGroupByDateGranularity } from 'src/engine/metadata-modules/page-layout-widget/enums/date-granularity.enum';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('PieChartConfiguration')
export class PieChartConfigurationDTO {
@Field(() => GraphType)
@IsEnum(GraphType)
export class PieChartConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.PIE_CHART])
@IsNotEmpty()
graphType: GraphType.PIE;
configurationType: WidgetConfigurationType.PIE_CHART;
@Field(() => UUIDScalarType)
@IsUUID()
@@ -1,11 +1,10 @@
import { Field, InputType, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('RatioAggregateConfig')
@InputType('RatioAggregateConfigInput')
export class RatioAggregateConfigDTO {
@Field(() => UUIDScalarType)
@IsUUID()
@@ -1,12 +1,21 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import { IsNotEmpty, ValidateNested } from 'class-validator';
import { IsIn, IsNotEmpty, ValidateNested } from 'class-validator';
import { RichTextV2BodyDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/rich-text-v2-body.dto';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('StandaloneRichTextConfiguration')
export class StandaloneRichTextConfigurationDTO {
export class StandaloneRichTextConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.STANDALONE_RICH_TEXT])
@IsNotEmpty()
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT;
@Field(() => RichTextV2BodyDTO)
@ValidateNested()
@Type(() => RichTextV2BodyDTO)
@@ -1,77 +1,17 @@
import { createUnionType } from '@nestjs/graphql';
import { AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
import { GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-configuration.dto';
import { LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
import { StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.enum';
import { ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYPE } from 'src/engine/metadata-modules/page-layout-widget/constants/all-widget-configuration-type-validator-by-widget-configuration-type.constant';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
export const WidgetConfiguration = createUnionType({
name: 'WidgetConfiguration',
types: () => [
BarChartConfigurationDTO,
LineChartConfigurationDTO,
PieChartConfigurationDTO,
AggregateChartConfigurationDTO,
GaugeChartConfigurationDTO,
IframeConfigurationDTO,
StandaloneRichTextConfigurationDTO,
],
resolveType(configuration: Record<string, unknown>) {
if (!('configurationType' in configuration)) {
throw new Error(
'Widget configuration missing configurationType discriminator. This indicates a validation bug or data corruption.',
);
}
if (
configuration.configurationType === WidgetConfigurationType.CHART_CONFIG
) {
switch (configuration.graphType) {
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR:
return BarChartConfigurationDTO;
case GraphType.LINE:
return LineChartConfigurationDTO;
case GraphType.PIE:
return PieChartConfigurationDTO;
case GraphType.AGGREGATE:
return AggregateChartConfigurationDTO;
case GraphType.GAUGE:
return GaugeChartConfigurationDTO;
default:
throw new Error(`Unknown graph type: ${configuration.graphType}`);
}
}
if (
configuration.configurationType === WidgetConfigurationType.IFRAME_CONFIG
) {
return IframeConfigurationDTO;
}
if (
configuration.configurationType ===
WidgetConfigurationType.STANDALONE_RICH_TEXT_CONFIG
) {
return StandaloneRichTextConfigurationDTO;
}
throw new Error(
`Unknown widget configuration type: ${configuration.configurationType}`,
);
types: () =>
Object.values(
ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYPE,
),
resolveType({ configurationType }: AllPageLayoutWidgetConfiguration) {
return ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYPE[
configurationType
];
},
});
export type WidgetConfigurationInterface =
| BarChartConfigurationDTO
| LineChartConfigurationDTO
| PieChartConfigurationDTO
| AggregateChartConfigurationDTO
| GaugeChartConfigurationDTO
| IframeConfigurationDTO
| StandaloneRichTextConfigurationDTO;
@@ -18,8 +18,8 @@ import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/inte
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { WidgetConfigurationInterface } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
import { GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
@Entity({ name: 'pageLayoutWidget', schema: 'core' })
@@ -78,8 +78,8 @@ export class PageLayoutWidgetEntity
@Column({ type: 'jsonb', nullable: false })
gridPosition: GridPosition;
@Column({ type: 'jsonb', nullable: true })
configuration: WidgetConfigurationInterface | null;
@Column({ type: 'jsonb', nullable: false })
configuration: AllPageLayoutWidgetConfiguration;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -0,0 +1,11 @@
import { registerEnumType } from '@nestjs/graphql';
export enum BarChartLayout {
VERTICAL = 'VERTICAL',
HORIZONTAL = 'HORIZONTAL',
}
registerEnumType(BarChartLayout, {
name: 'BarChartLayout',
description: 'Layout orientation for bar charts',
});
@@ -1,12 +1,11 @@
import { registerEnumType } from '@nestjs/graphql';
export enum GraphType {
AGGREGATE = 'AGGREGATE',
GAUGE = 'GAUGE',
PIE = 'PIE',
VERTICAL_BAR = 'VERTICAL_BAR',
HORIZONTAL_BAR = 'HORIZONTAL_BAR',
LINE = 'LINE',
AGGREGATE_CHART = 'AGGREGATE_CHART',
GAUGE_CHART = 'GAUGE_CHART',
PIE_CHART = 'PIE_CHART',
BAR_CHART = 'BAR_CHART',
LINE_CHART = 'LINE_CHART',
}
registerEnumType(GraphType, {
@@ -1,5 +0,0 @@
export enum WidgetConfigurationType {
CHART_CONFIG = 'CHART_CONFIG',
IFRAME_CONFIG = 'IFRAME_CONFIG',
STANDALONE_RICH_TEXT_CONFIG = 'STANDALONE_RICH_TEXT_CONFIG',
}
@@ -0,0 +1,37 @@
import { registerEnumType } from '@nestjs/graphql';
import { type Expect } from 'twenty-shared/testing';
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
export enum WidgetConfigurationType {
AGGREGATE_CHART = GraphType.AGGREGATE_CHART,
GAUGE_CHART = GraphType.GAUGE_CHART,
PIE_CHART = GraphType.PIE_CHART,
BAR_CHART = GraphType.BAR_CHART,
LINE_CHART = GraphType.LINE_CHART,
IFRAME = 'IFRAME',
STANDALONE_RICH_TEXT = 'STANDALONE_RICH_TEXT',
VIEW = 'VIEW',
FIELD = 'FIELD',
FIELDS = 'FIELDS',
TIMELINE = 'TIMELINE',
TASKS = 'TASKS',
NOTES = 'NOTES',
FILES = 'FILES',
EMAILS = 'EMAILS',
CALENDAR = 'CALENDAR',
FIELD_RICH_TEXT = 'FIELD_RICH_TEXT',
WORKFLOW = 'WORKFLOW',
WORKFLOW_VERSION = 'WORKFLOW_VERSION',
WORKFLOW_RUN = 'WORKFLOW_RUN',
}
registerEnumType(WidgetConfigurationType, {
name: 'WidgetConfigurationType',
});
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertion = Expect<
`${GraphType}` extends `${WidgetConfigurationType}` ? true : false
>;
@@ -26,7 +26,6 @@ import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-la
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
import { injectWidgetConfigurationDiscriminator } from 'src/engine/metadata-modules/page-layout-widget/utils/inject-widget-configuration-discriminator.util';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
@@ -109,9 +108,6 @@ export class PageLayoutWidgetResolver {
@ResolveField(() => WidgetConfiguration, { nullable: true })
configuration(@Parent() widget: PageLayoutWidgetDTO) {
return injectWidgetConfigurationDiscriminator(
widget.type,
widget.configuration,
);
return widget.configuration;
}
}
@@ -20,7 +20,6 @@ import {
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { WidgetConfigurationInterface } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import {
PageLayoutWidgetException,
@@ -28,6 +27,7 @@ import {
PageLayoutWidgetExceptionMessageKey,
generatePageLayoutWidgetExceptionMessage,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
import { validateAndTransformWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util';
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
@@ -70,16 +70,16 @@ export class PageLayoutWidgetService {
titleForError,
}: {
type: WidgetType;
configuration: Record<string, unknown>;
configuration: AllPageLayoutWidgetConfiguration;
workspaceId: string;
titleForError: string;
}): Promise<WidgetConfigurationInterface> {
}): Promise<AllPageLayoutWidgetConfiguration> {
const isDashboardV2Enabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_DASHBOARD_V2_ENABLED,
workspaceId,
);
let validatedConfig: WidgetConfigurationInterface | null = null;
let validatedConfig: AllPageLayoutWidgetConfiguration | null = null;
try {
validatedConfig = await validateAndTransformWidgetConfiguration({
@@ -210,9 +210,7 @@ export class PageLayoutWidgetService {
fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate({
createPageLayoutWidgetInput: {
...createPageLayoutWidgetInput,
...(validatedConfig && {
configuration: validatedConfig as Record<string, unknown>,
}),
configuration: validatedConfig,
},
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
@@ -271,12 +269,8 @@ export class PageLayoutWidgetService {
private async getValidatedConfigurationForCreate(
input: CreatePageLayoutWidgetInput,
workspaceId: string,
): Promise<WidgetConfigurationInterface | null> {
if (!input.configuration || !input.type) {
return null;
}
return this.validateWidgetConfigurationOrThrow({
): Promise<AllPageLayoutWidgetConfiguration> {
return await this.validateWidgetConfigurationOrThrow({
type: input.type,
configuration: input.configuration,
workspaceId,
@@ -313,9 +307,11 @@ export class PageLayoutWidgetService {
id,
update: {
...updateData,
...(validatedConfig && {
configuration: validatedConfig as Record<string, unknown>,
}),
...(isDefined(validatedConfig)
? {
configuration: validatedConfig,
}
: {}),
},
};
@@ -369,20 +365,15 @@ export class PageLayoutWidgetService {
updateData: UpdatePageLayoutWidgetInput,
existingWidget: FlatPageLayoutWidget,
workspaceId: string,
): Promise<WidgetConfigurationInterface | null> {
if (!updateData.configuration) {
return null;
): Promise<AllPageLayoutWidgetConfiguration | undefined> {
if (!isDefined(updateData.configuration)) {
return undefined;
}
const typeForValidation = updateData.type ?? existingWidget.type;
if (!typeForValidation) {
return null;
}
const titleForError = updateData.title ?? existingWidget.title;
return this.validateWidgetConfigurationOrThrow({
return await this.validateWidgetConfigurationOrThrow({
type: typeForValidation,
configuration: updateData.configuration,
workspaceId,
@@ -0,0 +1,5 @@
import { type ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYPE } from 'src/engine/metadata-modules/page-layout-widget/constants/all-widget-configuration-type-validator-by-widget-configuration-type.constant';
export type AllPageLayoutWidgetConfiguration = InstanceType<
(typeof ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYPE)[keyof typeof ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYPE]
>;
@@ -0,0 +1,5 @@
import { type WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
export type PageLayoutWidgetConfigurationBase = {
configurationType: WidgetConfigurationType;
};
@@ -4,12 +4,14 @@ import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layou
export const fromFlatPageLayoutWidgetToPageLayoutWidgetDto = (
flatPageLayoutWidget: FlatPageLayoutWidget,
): PageLayoutWidgetDTO => {
const { createdAt, updatedAt, deletedAt, ...rest } = flatPageLayoutWidget;
const { createdAt, updatedAt, deletedAt, objectMetadataId, ...rest } =
flatPageLayoutWidget;
return {
...rest,
objectMetadataId: objectMetadataId ?? undefined,
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
deletedAt: deletedAt ? new Date(deletedAt) : null,
deletedAt: deletedAt ? new Date(deletedAt) : undefined,
};
};
@@ -1,39 +0,0 @@
import { type WidgetConfigurationInterface } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.enum';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
type ConfigurationWithDiscriminator = WidgetConfigurationInterface & {
configurationType: WidgetConfigurationType;
};
export const injectWidgetConfigurationDiscriminator = (
widgetType: WidgetType,
configuration: WidgetConfigurationInterface | null,
): ConfigurationWithDiscriminator | null => {
if (!configuration) {
return null;
}
if (widgetType === WidgetType.IFRAME) {
return {
...configuration,
configurationType: WidgetConfigurationType.IFRAME_CONFIG,
} satisfies ConfigurationWithDiscriminator;
}
if (widgetType === WidgetType.GRAPH && 'graphType' in configuration) {
return {
...configuration,
configurationType: WidgetConfigurationType.CHART_CONFIG,
} satisfies ConfigurationWithDiscriminator;
}
if (widgetType === WidgetType.STANDALONE_RICH_TEXT) {
return {
...configuration,
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT_CONFIG,
} satisfies ConfigurationWithDiscriminator;
}
return configuration as ConfigurationWithDiscriminator;
};
@@ -10,10 +10,10 @@ import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-
import { LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
import { StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
import { type WidgetConfigurationInterface } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { BarChartGroupMode } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-group-mode.enum';
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
const formatValidationErrors = (errors: ValidationError[]): string => {
return errors
@@ -33,22 +33,24 @@ const validateGraphConfiguration = ({
}: {
configuration: Record<string, unknown>;
isDashboardV2Enabled: boolean;
}): WidgetConfigurationInterface | null => {
const graphType = configuration.graphType as GraphType;
}): AllPageLayoutWidgetConfiguration | null => {
const configurationType = configuration.configurationType as GraphType;
if (!graphType || !Object.values(GraphType).includes(graphType)) {
if (
!configurationType ||
!Object.values(GraphType).includes(configurationType)
) {
return null;
}
if (graphType === GraphType.GAUGE && !isDashboardV2Enabled) {
if (configurationType === GraphType.GAUGE_CHART && !isDashboardV2Enabled) {
throw new Error(
`Chart type ${graphType} requires IS_DASHBOARD_V2_ENABLED feature flag`,
`Chart type ${configurationType} requires IS_DASHBOARD_V2_ENABLED feature flag`,
);
}
switch (graphType) {
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR: {
switch (configurationType) {
case GraphType.BAR_CHART: {
const instance = plainToInstance(BarChartConfigurationDTO, configuration);
const errors = validateSync(instance, {
@@ -69,7 +71,7 @@ const validateGraphConfiguration = ({
return instance;
}
case GraphType.LINE: {
case GraphType.LINE_CHART: {
const instance = plainToInstance(
LineChartConfigurationDTO,
configuration,
@@ -93,7 +95,7 @@ const validateGraphConfiguration = ({
return instance;
}
case GraphType.PIE: {
case GraphType.PIE_CHART: {
const instance = plainToInstance(PieChartConfigurationDTO, configuration);
const errors = validateSync(instance, {
@@ -107,7 +109,7 @@ const validateGraphConfiguration = ({
return instance;
}
case GraphType.AGGREGATE: {
case GraphType.AGGREGATE_CHART: {
const instance = plainToInstance(
AggregateChartConfigurationDTO,
configuration,
@@ -124,7 +126,7 @@ const validateGraphConfiguration = ({
return instance;
}
case GraphType.GAUGE: {
case GraphType.GAUGE_CHART: {
const instance = plainToInstance(
GaugeChartConfigurationDTO,
configuration,
@@ -148,7 +150,7 @@ const validateGraphConfiguration = ({
const validateIframeConfiguration = (
configuration: unknown,
): WidgetConfigurationInterface | null => {
): AllPageLayoutWidgetConfiguration | null => {
const instance = plainToInstance(IframeConfigurationDTO, configuration);
const errors = validateSync(instance, {
@@ -165,7 +167,7 @@ const validateIframeConfiguration = (
const validateStandaloneRichTextConfiguration = async (
configuration: unknown,
): Promise<WidgetConfigurationInterface | null> => {
): Promise<AllPageLayoutWidgetConfiguration | null> => {
const instance = plainToInstance(
StandaloneRichTextConfigurationDTO,
configuration,
@@ -195,7 +197,7 @@ export const validateAndTransformWidgetConfiguration = async ({
type: WidgetType;
configuration: unknown;
isDashboardV2Enabled: boolean;
}): Promise<WidgetConfigurationInterface | null> => {
}): Promise<AllPageLayoutWidgetConfiguration | null> => {
if (!configuration || typeof configuration !== 'object') {
throw new Error('Invalid configuration: not an object');
}
@@ -272,10 +272,7 @@ export class PageLayoutDuplicationService {
gridPosition: originalWidget.gridPosition,
type: originalWidget.type,
objectMetadataId: originalWidget.objectMetadataId,
configuration: originalWidget.configuration as Record<
string,
unknown
> | null,
configuration: originalWidget.configuration,
pageLayoutTabId: newTabId,
},
workspaceId,
@@ -6,8 +6,10 @@ import { v4 } from 'uuid';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant';
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant';
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
@@ -63,6 +65,7 @@ export class PageLayoutUpdateService {
const existingPageLayout = flatPageLayoutMaps.byId[id];
// TODO move in validator
if (
!isDefined(existingPageLayout) ||
isDefined(existingPageLayout.deletedAt)
@@ -75,6 +78,7 @@ export class PageLayoutUpdateService {
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
);
}
///
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
@@ -201,7 +205,7 @@ export class PageLayoutUpdateService {
>({
existingObjects: existingTabs,
receivedObjects: tabs,
propertiesToCompare: ['title', 'position'],
propertiesToCompare: FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES,
});
const now = new Date();
@@ -228,10 +232,13 @@ export class PageLayoutUpdateService {
const tabsToUpdate: FlatPageLayoutTab[] = entitiesToUpdate.map(
(tabInput) => {
const existingTab = flatPageLayoutTabMaps.byId[tabInput.id];
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: tabInput.id,
flatEntityMaps: flatPageLayoutTabMaps,
});
return {
...existingTab!,
...existingTab,
title: tabInput.title,
position: tabInput.position,
updatedAt: now.toISOString(),
@@ -241,10 +248,13 @@ export class PageLayoutUpdateService {
const tabsToRestoreAndUpdate: FlatPageLayoutTab[] =
entitiesToRestoreAndUpdate.map((tabInput) => {
const existingTab = flatPageLayoutTabMaps.byId[tabInput.id];
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: tabInput.id,
flatEntityMaps: flatPageLayoutTabMaps,
});
return {
...existingTab!,
...existingTab,
title: tabInput.title,
position: tabInput.position,
deletedAt: null,
@@ -349,14 +359,7 @@ export class PageLayoutUpdateService {
>({
existingObjects: existingWidgets,
receivedObjects: widgets,
propertiesToCompare: [
'pageLayoutTabId',
'objectMetadataId',
'title',
'type',
'gridPosition',
'configuration',
],
propertiesToCompare: FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
});
const now = new Date();
@@ -385,10 +388,13 @@ export class PageLayoutUpdateService {
const widgetsToUpdate: FlatPageLayoutWidget[] = entitiesToUpdate.map(
(widgetInput) => {
const existingWidget = flatPageLayoutWidgetMaps.byId[widgetInput.id];
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
return {
...existingWidget!,
...existingWidget,
pageLayoutTabId: widgetInput.pageLayoutTabId,
title: widgetInput.title,
type: widgetInput.type,
@@ -402,10 +408,13 @@ export class PageLayoutUpdateService {
const widgetsToRestoreAndUpdate: FlatPageLayoutWidget[] =
entitiesToRestoreAndUpdate.map((widgetInput) => {
const existingWidget = flatPageLayoutWidgetMaps.byId[widgetInput.id];
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
return {
...existingWidget!,
...existingWidget,
pageLayoutTabId: widgetInput.pageLayoutTabId,
title: widgetInput.title,
type: widgetInput.type,