Add configuration types for widgets (#14717)
closes https://github.com/twentyhq/core-team-issues/issues/1532
This commit is contained in:
+98
@@ -0,0 +1,98 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
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 { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('BarChartConfiguration')
|
||||
export class BarChartConfigurationDTO {
|
||||
@Field(() => GraphType)
|
||||
@IsEnum(GraphType)
|
||||
@IsNotEmpty()
|
||||
graphType: GraphType.BAR;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
groupByFieldMetadataIdX: string;
|
||||
|
||||
@Field(() => GraphOrderBy)
|
||||
@IsEnum(GraphOrderBy)
|
||||
@IsNotEmpty()
|
||||
orderByX: GraphOrderBy;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
groupByFieldMetadataIdY?: string;
|
||||
|
||||
@Field(() => GraphOrderBy, { nullable: true })
|
||||
@IsEnum(GraphOrderBy)
|
||||
@IsOptional()
|
||||
orderByY?: GraphOrderBy;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
omitNullValues?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
xAxisName?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
yAxisName?: string;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
rangeMin?: number;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
rangeMax?: number;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
filter?: ObjectRecordFilter;
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
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 { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('GaugeChartConfiguration')
|
||||
export class GaugeChartConfigurationDTO {
|
||||
@Field(() => GraphType)
|
||||
@IsEnum(GraphType)
|
||||
@IsNotEmpty()
|
||||
graphType: GraphType.GAUGE;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperationTotal: AggregateOperations;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataIdTotal: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
filter?: ObjectRecordFilter;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString, IsUrl } from 'class-validator';
|
||||
|
||||
@ObjectType('IframeConfiguration')
|
||||
export class IframeConfigurationDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsUrl()
|
||||
url: string;
|
||||
}
|
||||
+1
-1
@@ -49,7 +49,7 @@ export class UpdatePageLayoutWidgetWithIdInput {
|
||||
@IsNotEmpty()
|
||||
gridPosition: GridPositionInput;
|
||||
|
||||
@Field(() => GraphQLJSON)
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
configuration: Record<string, unknown> | null;
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
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 { 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';
|
||||
|
||||
@ObjectType('LineChartConfiguration')
|
||||
export class LineChartConfigurationDTO {
|
||||
@Field(() => GraphType)
|
||||
@IsEnum(GraphType)
|
||||
@IsNotEmpty()
|
||||
graphType: GraphType.LINE;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
groupByFieldMetadataIdX: string;
|
||||
|
||||
@Field(() => GraphOrderBy)
|
||||
@IsEnum(GraphOrderBy)
|
||||
@IsNotEmpty()
|
||||
orderByX: GraphOrderBy;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
groupByFieldMetadataIdY?: string;
|
||||
|
||||
@Field(() => GraphOrderBy, { nullable: true })
|
||||
@IsEnum(GraphOrderBy)
|
||||
@IsOptional()
|
||||
orderByY?: GraphOrderBy;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
omitNullValues?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
xAxisName?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
yAxisName?: string;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
rangeMin?: number;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
rangeMax?: number;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
filter?: ObjectRecordFilter;
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
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 { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('NumberChartConfiguration')
|
||||
export class NumberChartConfigurationDTO {
|
||||
@Field(() => GraphType)
|
||||
@IsEnum(GraphType)
|
||||
@IsNotEmpty()
|
||||
graphType: GraphType.NUMBER;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
label?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
format?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
filter?: ObjectRecordFilter;
|
||||
}
|
||||
+6
-3
@@ -1,9 +1,12 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import {
|
||||
WidgetConfiguration,
|
||||
WidgetConfigurationInterface,
|
||||
} from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface';
|
||||
import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum';
|
||||
|
||||
registerEnumType(WidgetType, { name: 'WidgetType' });
|
||||
@@ -43,8 +46,8 @@ export class PageLayoutWidgetDTO {
|
||||
@Field(() => GridPositionDTO, { nullable: false })
|
||||
gridPosition: GridPositionDTO;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
configuration: Record<string, unknown> | null;
|
||||
@Field(() => WidgetConfiguration, { nullable: true })
|
||||
configuration: WidgetConfigurationInterface | null;
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
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 { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { GraphOrderBy } from 'src/engine/core-modules/page-layout/enums/graph-order-by.enum';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('PieChartConfiguration')
|
||||
export class PieChartConfigurationDTO {
|
||||
@Field(() => GraphType)
|
||||
@IsEnum(GraphType)
|
||||
@IsNotEmpty()
|
||||
graphType: GraphType.PIE;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
@IsNotEmpty()
|
||||
aggregateOperation: AggregateOperations;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
groupByFieldMetadataId: string;
|
||||
|
||||
@Field(() => GraphOrderBy)
|
||||
@IsEnum(GraphOrderBy)
|
||||
@IsNotEmpty()
|
||||
orderBy: GraphOrderBy;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
filter?: ObjectRecordFilter;
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { createUnionType } from '@nestjs/graphql';
|
||||
|
||||
import { BarChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto';
|
||||
import { GaugeChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto';
|
||||
import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/iframe-configuration.dto';
|
||||
import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto';
|
||||
import { NumberChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto';
|
||||
import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto';
|
||||
import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum';
|
||||
|
||||
export const WidgetConfiguration = createUnionType({
|
||||
name: 'WidgetConfiguration',
|
||||
types: () => [
|
||||
BarChartConfigurationDTO,
|
||||
LineChartConfigurationDTO,
|
||||
PieChartConfigurationDTO,
|
||||
NumberChartConfigurationDTO,
|
||||
GaugeChartConfigurationDTO,
|
||||
IframeConfigurationDTO,
|
||||
],
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
switch (configuration.configurationType) {
|
||||
case WidgetConfigurationType.IFRAME_CONFIG:
|
||||
return IframeConfigurationDTO;
|
||||
case WidgetConfigurationType.BAR_CHART_CONFIG:
|
||||
return BarChartConfigurationDTO;
|
||||
case WidgetConfigurationType.LINE_CHART_CONFIG:
|
||||
return LineChartConfigurationDTO;
|
||||
case WidgetConfigurationType.PIE_CHART_CONFIG:
|
||||
return PieChartConfigurationDTO;
|
||||
case WidgetConfigurationType.NUMBER_CHART_CONFIG:
|
||||
return NumberChartConfigurationDTO;
|
||||
case WidgetConfigurationType.GAUGE_CHART_CONFIG:
|
||||
return GaugeChartConfigurationDTO;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown widget configuration type: ${configuration.configurationType}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export type WidgetConfigurationInterface =
|
||||
| BarChartConfigurationDTO
|
||||
| LineChartConfigurationDTO
|
||||
| PieChartConfigurationDTO
|
||||
| NumberChartConfigurationDTO
|
||||
| GaugeChartConfigurationDTO
|
||||
| IframeConfigurationDTO;
|
||||
+2
-1
@@ -11,6 +11,7 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface';
|
||||
import { PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum';
|
||||
import { GridPosition } from 'src/engine/core-modules/page-layout/types/grid-position.type';
|
||||
@@ -72,7 +73,7 @@ export class PageLayoutWidgetEntity
|
||||
gridPosition: GridPosition;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
configuration: Record<string, unknown> | null;
|
||||
configuration: WidgetConfigurationInterface | null;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum GraphOrderBy {
|
||||
FIELD_ASC = 'FIELD_ASC',
|
||||
FIELD_DESC = 'FIELD_DESC',
|
||||
VALUE_ASC = 'VALUE_ASC',
|
||||
VALUE_DESC = 'VALUE_DESC',
|
||||
}
|
||||
|
||||
registerEnumType(GraphOrderBy, {
|
||||
name: 'GraphOrderBy',
|
||||
description: 'Order by options for graph widgets',
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum GraphType {
|
||||
NUMBER = 'NUMBER',
|
||||
GAUGE = 'GAUGE',
|
||||
PIE = 'PIE',
|
||||
BAR = 'BAR',
|
||||
LINE = 'LINE',
|
||||
}
|
||||
|
||||
registerEnumType(GraphType, {
|
||||
name: 'GraphType',
|
||||
description: 'Type of graph widget',
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export enum WidgetConfigurationType {
|
||||
BAR_CHART_CONFIG = 'BAR_CHART_CONFIG',
|
||||
LINE_CHART_CONFIG = 'LINE_CHART_CONFIG',
|
||||
PIE_CHART_CONFIG = 'PIE_CHART_CONFIG',
|
||||
NUMBER_CHART_CONFIG = 'NUMBER_CHART_CONFIG',
|
||||
GAUGE_CHART_CONFIG = 'GAUGE_CHART_CONFIG',
|
||||
IFRAME_CONFIG = 'IFRAME_CONFIG',
|
||||
}
|
||||
+17
-2
@@ -14,17 +14,20 @@ export enum PageLayoutWidgetExceptionMessageKey {
|
||||
PAGE_LAYOUT_TAB_NOT_FOUND = 'PAGE_LAYOUT_TAB_NOT_FOUND',
|
||||
PAGE_LAYOUT_WIDGET_NOT_DELETED = 'PAGE_LAYOUT_WIDGET_NOT_DELETED',
|
||||
GRID_POSITION_REQUIRED = 'GRID_POSITION_REQUIRED',
|
||||
INVALID_WIDGET_CONFIGURATION = 'INVALID_WIDGET_CONFIGURATION',
|
||||
}
|
||||
|
||||
export class PageLayoutWidgetException extends CustomException<PageLayoutWidgetExceptionCode> {}
|
||||
|
||||
export const generatePageLayoutWidgetExceptionMessage = (
|
||||
key: PageLayoutWidgetExceptionMessageKey,
|
||||
value?: string,
|
||||
widgetTitle?: string,
|
||||
widgetType?: string,
|
||||
detailedError?: string,
|
||||
): string => {
|
||||
switch (key) {
|
||||
case PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND:
|
||||
return `Page layout widget with ID "${value}" not found`;
|
||||
return `Page layout widget with ID "${widgetTitle}" not found`;
|
||||
case PageLayoutWidgetExceptionMessageKey.TITLE_REQUIRED:
|
||||
return 'Page layout widget title is required';
|
||||
case PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_ID_REQUIRED:
|
||||
@@ -35,6 +38,18 @@ export const generatePageLayoutWidgetExceptionMessage = (
|
||||
return 'Page layout widget is not deleted and cannot be restored';
|
||||
case PageLayoutWidgetExceptionMessageKey.GRID_POSITION_REQUIRED:
|
||||
return 'Grid position is required';
|
||||
case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION:
|
||||
if (widgetTitle && widgetType && detailedError) {
|
||||
return `Invalid configuration for widget "${widgetTitle}" of type ${widgetType}: ${detailedError}`;
|
||||
}
|
||||
if (widgetTitle && widgetType) {
|
||||
return `Invalid configuration for widget "${widgetTitle}" of type ${widgetType}`;
|
||||
}
|
||||
if (widgetType) {
|
||||
return `Invalid configuration for widget type ${widgetType}`;
|
||||
}
|
||||
|
||||
return 'Invalid widget configuration';
|
||||
default:
|
||||
assertUnreachable(key);
|
||||
}
|
||||
|
||||
+15
-13
@@ -147,7 +147,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
pageLayoutTabId: 'tab-1',
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -210,8 +210,10 @@ describe('PageLayoutUpdateService', () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const input = {
|
||||
...mockPageLayout,
|
||||
name: 'Updated Page Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
tabs: [],
|
||||
};
|
||||
|
||||
jest
|
||||
@@ -435,7 +437,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
id: 'new-widget-id',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
title: 'New Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
@@ -451,7 +453,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
title: 'New Widget',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
});
|
||||
|
||||
@@ -467,7 +469,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
id: 'new-widget-id',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
title: 'New Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
@@ -484,7 +486,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
{
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
id: 'widget-1',
|
||||
title: 'Updated Widget',
|
||||
type: WidgetType.VIEW,
|
||||
@@ -517,7 +519,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
type: WidgetType.VIEW,
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
gridPosition: { row: 1, column: 1, rowSpan: 2, columnSpan: 2 },
|
||||
},
|
||||
mockTransactionManager,
|
||||
@@ -559,7 +561,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
title: 'Updated Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
@@ -568,7 +570,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
id: 'new-widget-id',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
title: 'New Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
@@ -624,7 +626,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
type: WidgetType.VIEW,
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
},
|
||||
mockTransactionManager,
|
||||
@@ -636,7 +638,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
type: WidgetType.FIELDS,
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
},
|
||||
workspaceId,
|
||||
@@ -666,7 +668,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
pageLayoutTabId: 'tab-1',
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
},
|
||||
{
|
||||
id: 'widget-2',
|
||||
@@ -675,7 +677,7 @@ describe('PageLayoutUpdateService', () => {
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
pageLayoutTabId: 'tab-1',
|
||||
objectMetadataId: null,
|
||||
configuration: {},
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+4
-2
@@ -3,8 +3,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
|
||||
import { CreatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout-widget.input';
|
||||
import { UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-tab-with-widgets.input';
|
||||
import { UpdatePageLayoutWidgetWithIdInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input';
|
||||
import { UpdatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget.input';
|
||||
import { UpdatePageLayoutWithTabsInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
|
||||
import { PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity';
|
||||
@@ -229,14 +231,14 @@ export class PageLayoutUpdateService {
|
||||
await this.pageLayoutWidgetService.update(
|
||||
widgetUpdate.id,
|
||||
workspaceId,
|
||||
widgetUpdate,
|
||||
widgetUpdate as UpdatePageLayoutWidgetInput,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
for (const widgetToCreate of entitiesToCreate) {
|
||||
await this.pageLayoutWidgetService.create(
|
||||
widgetToCreate,
|
||||
widgetToCreate as CreatePageLayoutWidgetInput,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
+76
-4
@@ -7,6 +7,7 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity
|
||||
|
||||
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';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity';
|
||||
import {
|
||||
PageLayoutTabException,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/core-modules/page-layout/exceptions/page-layout-widget.exception';
|
||||
import { PageLayoutTabService } from 'src/engine/core-modules/page-layout/services/page-layout-tab.service';
|
||||
import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util';
|
||||
|
||||
@Injectable()
|
||||
export class PageLayoutWidgetService {
|
||||
@@ -120,11 +122,44 @@ export class PageLayoutWidgetService {
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
let validatedConfig: WidgetConfigurationInterface | null = null;
|
||||
|
||||
if (pageLayoutWidgetData.configuration && pageLayoutWidgetData.type) {
|
||||
try {
|
||||
validatedConfig = validateAndTransformWidgetConfiguration(
|
||||
pageLayoutWidgetData.type,
|
||||
pageLayoutWidgetData.configuration,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
pageLayoutWidgetData.title,
|
||||
pageLayoutWidgetData.type,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
if (!validatedConfig) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
pageLayoutWidgetData.title,
|
||||
pageLayoutWidgetData.type,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
|
||||
const insertResult = await repository.insert({
|
||||
...pageLayoutWidgetData,
|
||||
workspaceId,
|
||||
...(validatedConfig && { configuration: validatedConfig }),
|
||||
} as QueryDeepPartialEntity<PageLayoutWidgetEntity>);
|
||||
|
||||
return this.findByIdOrThrow(
|
||||
@@ -174,10 +209,47 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
}
|
||||
|
||||
await repository.update(
|
||||
{ id },
|
||||
updateData as QueryDeepPartialEntity<PageLayoutWidgetEntity>,
|
||||
);
|
||||
let validatedConfig: WidgetConfigurationInterface | null = null;
|
||||
|
||||
if (updateData.configuration) {
|
||||
const typeForValidation = updateData.type ?? existingWidget.type;
|
||||
const titleForError = updateData.title ?? existingWidget.title;
|
||||
|
||||
if (typeForValidation) {
|
||||
try {
|
||||
validatedConfig = validateAndTransformWidgetConfiguration(
|
||||
typeForValidation,
|
||||
updateData.configuration,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
titleForError,
|
||||
typeForValidation,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
if (!validatedConfig) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
titleForError,
|
||||
typeForValidation,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await repository.update({ id }, {
|
||||
...updateData,
|
||||
...(validatedConfig && { configuration: validatedConfig }),
|
||||
} as QueryDeepPartialEntity<PageLayoutWidgetEntity>);
|
||||
|
||||
return this.findByIdOrThrow(id, workspaceId, transactionManager);
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
import {
|
||||
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
INVALID_IFRAME_CONFIG_BAD_URL,
|
||||
INVALID_IFRAME_CONFIG_EMPTY_URL,
|
||||
INVALID_IFRAME_CONFIG_MISSING_URL,
|
||||
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
|
||||
TEST_BAR_CHART_CONFIG,
|
||||
TEST_BAR_CHART_CONFIG_MINIMAL,
|
||||
TEST_GAUGE_CHART_CONFIG,
|
||||
TEST_GAUGE_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,
|
||||
} from 'test/integration/constants/widget-configuration-test-data.constants';
|
||||
|
||||
import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum';
|
||||
import { 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';
|
||||
|
||||
describe('validateAndTransformWidgetConfiguration', () => {
|
||||
describe('IFRAME widget', () => {
|
||||
it('should validate and transform valid iframe configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration(
|
||||
WidgetType.IFRAME,
|
||||
TEST_IFRAME_CONFIG,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject(TEST_IFRAME_CONFIG);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.IFRAME_CONFIG,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for invalid URL', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(
|
||||
WidgetType.IFRAME,
|
||||
INVALID_IFRAME_CONFIG_BAD_URL,
|
||||
),
|
||||
).toThrow(/url must be a URL address/);
|
||||
});
|
||||
|
||||
it('should throw error for missing URL', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(
|
||||
WidgetType.IFRAME,
|
||||
INVALID_IFRAME_CONFIG_MISSING_URL,
|
||||
),
|
||||
).toThrow(/url must be a URL address/);
|
||||
});
|
||||
|
||||
it('should throw error for empty URL', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(
|
||||
WidgetType.IFRAME,
|
||||
INVALID_IFRAME_CONFIG_EMPTY_URL,
|
||||
),
|
||||
).toThrow(/url must be a URL address/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GRAPH widget', () => {
|
||||
describe('NUMBER graph', () => {
|
||||
it('should validate full number graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
TEST_NUMBER_CHART_CONFIG,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.NUMBER_CHART_CONFIG,
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate minimal number graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
TEST_NUMBER_CHART_CONFIG_MINIMAL,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.NUMBER_CHART_CONFIG,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing required fields', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
|
||||
),
|
||||
).toThrow(/aggregateFieldMetadataId should not be empty/);
|
||||
});
|
||||
|
||||
it('should throw error for invalid UUID', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
),
|
||||
).toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BAR graph', () => {
|
||||
it('should validate full bar graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
TEST_BAR_CHART_CONFIG,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject(TEST_BAR_CHART_CONFIG);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.BAR_CHART_CONFIG,
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate minimal bar graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
TEST_BAR_CHART_CONFIG_MINIMAL,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject(TEST_BAR_CHART_CONFIG_MINIMAL);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.BAR_CHART_CONFIG,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing group by field', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
),
|
||||
).toThrow(/groupByFieldMetadataIdX should not be empty/);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.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);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.LINE_CHART_CONFIG,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.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);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.PIE_CHART_CONFIG,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.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);
|
||||
expect((result as any)?.configurationType).toBe(
|
||||
WidgetConfigurationType.GAUGE_CHART_CONFIG,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for unsupported graph type', () => {
|
||||
const configuration = {
|
||||
graphType: 'UNSUPPORTED',
|
||||
viewId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
};
|
||||
|
||||
const result = validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
configuration,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for missing graph type', () => {
|
||||
const configuration = {
|
||||
viewId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
};
|
||||
|
||||
const result = validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
configuration,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should throw error for null configuration', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(WidgetType.IFRAME, null),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for undefined configuration', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(WidgetType.IFRAME, undefined),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for non-object configuration', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(WidgetType.IFRAME, 'string'),
|
||||
).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,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error messages', () => {
|
||||
it('should include validation details in error message', () => {
|
||||
expect(() =>
|
||||
validateAndTransformWidgetConfiguration(
|
||||
WidgetType.GRAPH,
|
||||
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
),
|
||||
).toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync, type ValidationError } from 'class-validator';
|
||||
|
||||
import { BarChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto';
|
||||
import { GaugeChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto';
|
||||
import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/iframe-configuration.dto';
|
||||
import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto';
|
||||
import { NumberChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/number-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 { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum';
|
||||
import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum';
|
||||
|
||||
const formatValidationErrors = (errors: ValidationError[]): string => {
|
||||
return errors
|
||||
.map((err) => {
|
||||
const constraints = err.constraints
|
||||
? Object.values(err.constraints).join(', ')
|
||||
: 'Unknown error';
|
||||
|
||||
return `${err.property}: ${constraints}`;
|
||||
})
|
||||
.join('; ');
|
||||
};
|
||||
|
||||
const validateGraphConfiguration = (
|
||||
configuration: Record<string, unknown>,
|
||||
): WidgetConfigurationInterface | null => {
|
||||
const graphType = configuration.graphType as GraphType;
|
||||
|
||||
if (!graphType || !Object.values(GraphType).includes(graphType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (graphType) {
|
||||
case GraphType.BAR: {
|
||||
const instance = plainToInstance(BarChartConfigurationDTO, configuration);
|
||||
|
||||
const errors = validateSync(instance);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return {
|
||||
...instance,
|
||||
configurationType: WidgetConfigurationType.BAR_CHART_CONFIG,
|
||||
} as WidgetConfigurationInterface;
|
||||
}
|
||||
case GraphType.LINE: {
|
||||
const instance = plainToInstance(
|
||||
LineChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const errors = validateSync(instance);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return {
|
||||
...instance,
|
||||
configurationType: WidgetConfigurationType.LINE_CHART_CONFIG,
|
||||
} as WidgetConfigurationInterface;
|
||||
}
|
||||
case GraphType.PIE: {
|
||||
const instance = plainToInstance(PieChartConfigurationDTO, configuration);
|
||||
|
||||
const errors = validateSync(instance);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return {
|
||||
...instance,
|
||||
configurationType: WidgetConfigurationType.PIE_CHART_CONFIG,
|
||||
} as WidgetConfigurationInterface;
|
||||
}
|
||||
case GraphType.NUMBER: {
|
||||
const instance = plainToInstance(
|
||||
NumberChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const errors = validateSync(instance);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return {
|
||||
...instance,
|
||||
configurationType: WidgetConfigurationType.NUMBER_CHART_CONFIG,
|
||||
} as WidgetConfigurationInterface;
|
||||
}
|
||||
case GraphType.GAUGE: {
|
||||
const instance = plainToInstance(
|
||||
GaugeChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const errors = validateSync(instance);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return {
|
||||
...instance,
|
||||
configurationType: WidgetConfigurationType.GAUGE_CHART_CONFIG,
|
||||
} as WidgetConfigurationInterface;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const validateIframeConfiguration = (
|
||||
configuration: unknown,
|
||||
): WidgetConfigurationInterface | null => {
|
||||
const instance = plainToInstance(IframeConfigurationDTO, configuration);
|
||||
|
||||
const errors = validateSync(instance);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return {
|
||||
...instance,
|
||||
configurationType: WidgetConfigurationType.IFRAME_CONFIG,
|
||||
} as WidgetConfigurationInterface;
|
||||
};
|
||||
|
||||
export const validateAndTransformWidgetConfiguration = (
|
||||
type: WidgetType,
|
||||
configuration: unknown,
|
||||
): WidgetConfigurationInterface | null => {
|
||||
if (!configuration || typeof configuration !== 'object') {
|
||||
throw new Error('Invalid configuration: not an object');
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case WidgetType.GRAPH:
|
||||
return validateGraphConfiguration(
|
||||
configuration as Record<string, unknown>,
|
||||
);
|
||||
case WidgetType.IFRAME:
|
||||
return validateIframeConfiguration(configuration);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
if (Array.isArray(error)) {
|
||||
const errorMessage = formatValidationErrors(error);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
-7
@@ -2,11 +2,4 @@ export const PAGE_LAYOUT_SEEDS = {
|
||||
SALES_DASHBOARD: 'SALES_DASHBOARD',
|
||||
CUSTOMER_DASHBOARD: 'CUSTOMER_DASHBOARD',
|
||||
TEAM_DASHBOARD: 'TEAM_DASHBOARD',
|
||||
REVENUE_ANALYTICS: 'REVENUE_ANALYTICS',
|
||||
MARKETING_METRICS: 'MARKETING_METRICS',
|
||||
SUPPORT_DASHBOARD: 'SUPPORT_DASHBOARD',
|
||||
PRODUCT_USAGE: 'PRODUCT_USAGE',
|
||||
OPERATIONS_KPI: 'OPERATIONS_KPI',
|
||||
FINANCE_OVERVIEW: 'FINANCE_OVERVIEW',
|
||||
EXECUTIVE_SUMMARY: 'EXECUTIVE_SUMMARY',
|
||||
};
|
||||
|
||||
-7
@@ -5,11 +5,4 @@ export const PAGE_LAYOUT_TAB_SEEDS = {
|
||||
CUSTOMER_ANALYTICS: 'CUSTOMER_ANALYTICS_TAB',
|
||||
TEAM_OVERVIEW: 'TEAM_OVERVIEW_TAB',
|
||||
TEAM_METRICS: 'TEAM_METRICS_TAB',
|
||||
REVENUE_MAIN: 'REVENUE_MAIN_TAB',
|
||||
MARKETING_MAIN: 'MARKETING_MAIN_TAB',
|
||||
SUPPORT_MAIN: 'SUPPORT_MAIN_TAB',
|
||||
PRODUCT_MAIN: 'PRODUCT_MAIN_TAB',
|
||||
OPERATIONS_MAIN: 'OPERATIONS_MAIN_TAB',
|
||||
FINANCE_MAIN: 'FINANCE_MAIN_TAB',
|
||||
EXECUTIVE_MAIN: 'EXECUTIVE_MAIN_TAB',
|
||||
};
|
||||
|
||||
+14
-49
@@ -1,55 +1,20 @@
|
||||
export const PAGE_LAYOUT_WIDGET_SEEDS = {
|
||||
SALES_PIPELINE_NUMBER: 'SALES_PIPELINE_NUMBER_WIDGET',
|
||||
SALES_CONVERSION_GAUGE: 'SALES_CONVERSION_GAUGE_WIDGET',
|
||||
SALES_MONTHLY_REVENUE: 'SALES_MONTHLY_REVENUE_WIDGET',
|
||||
SALES_PIPELINE_VALUE: 'SALES_PIPELINE_VALUE_WIDGET',
|
||||
SALES_AVERAGE_DEAL_SIZE: 'SALES_AVERAGE_DEAL_SIZE_WIDGET',
|
||||
SALES_REVENUE_FORECAST: 'SALES_REVENUE_FORECAST_WIDGET',
|
||||
SALES_DEALS_BY_STAGE: 'SALES_DEALS_BY_STAGE_WIDGET',
|
||||
SALES_TOP_PERFORMERS: 'SALES_TOP_PERFORMERS_WIDGET',
|
||||
SALES_FORECAST_LINE: 'SALES_FORECAST_LINE_WIDGET',
|
||||
SALES_DEAL_DISTRIBUTION: 'SALES_DEAL_DISTRIBUTION_WIDGET',
|
||||
SALES_OPPORTUNITY_COUNT: 'SALES_OPPORTUNITY_COUNT_WIDGET',
|
||||
|
||||
CUSTOMER_TOTAL_COUNT: 'CUSTOMER_TOTAL_COUNT_WIDGET',
|
||||
CUSTOMER_ACQUISITION_TREND: 'CUSTOMER_ACQUISITION_TREND_WIDGET',
|
||||
CUSTOMER_SEGMENTS_PIE: 'CUSTOMER_SEGMENTS_PIE_WIDGET',
|
||||
CUSTOMER_SATISFACTION_GAUGE: 'CUSTOMER_SATISFACTION_GAUGE_WIDGET',
|
||||
CUSTOMER_RETENTION_RATE: 'CUSTOMER_RETENTION_RATE_WIDGET',
|
||||
CUSTOMER_LIFETIME_VALUE: 'CUSTOMER_LIFETIME_VALUE_WIDGET',
|
||||
CUSTOMER_NEW_OVER_TIME: 'CUSTOMER_NEW_OVER_TIME_WIDGET',
|
||||
CUSTOMER_COMPANIES_BY_SIZE: 'CUSTOMER_COMPANIES_BY_SIZE_WIDGET',
|
||||
CUSTOMER_ANNUAL_RECURRING_REVENUE: 'CUSTOMER_ANNUAL_RECURRING_REVENUE_WIDGET',
|
||||
CUSTOMER_REVENUE_DISTRIBUTION: 'CUSTOMER_REVENUE_DISTRIBUTION_WIDGET',
|
||||
CUSTOMER_AVERAGE_ARR: 'CUSTOMER_AVERAGE_ARR_WIDGET',
|
||||
|
||||
TEAM_ACTIVITY_OVERVIEW: 'TEAM_ACTIVITY_OVERVIEW_WIDGET',
|
||||
TEAM_PRODUCTIVITY_METRICS: 'TEAM_PRODUCTIVITY_METRICS_WIDGET',
|
||||
TEAM_GOAL_PROGRESS: 'TEAM_GOAL_PROGRESS_WIDGET',
|
||||
TEAM_MEMBER_LEADERBOARD: 'TEAM_MEMBER_LEADERBOARD_WIDGET',
|
||||
|
||||
REVENUE_TOTAL_NUMBER: 'REVENUE_TOTAL_NUMBER_WIDGET',
|
||||
REVENUE_GROWTH_TREND: 'REVENUE_GROWTH_TREND_WIDGET',
|
||||
REVENUE_BY_PRODUCT: 'REVENUE_BY_PRODUCT_WIDGET',
|
||||
REVENUE_BY_REGION: 'REVENUE_BY_REGION_WIDGET',
|
||||
|
||||
MARKETING_LEADS_GENERATED: 'MARKETING_LEADS_GENERATED_WIDGET',
|
||||
MARKETING_CAMPAIGN_ROI: 'MARKETING_CAMPAIGN_ROI_WIDGET',
|
||||
MARKETING_CHANNEL_PERFORMANCE: 'MARKETING_CHANNEL_PERFORMANCE_WIDGET',
|
||||
MARKETING_CONVERSION_FUNNEL: 'MARKETING_CONVERSION_FUNNEL_WIDGET',
|
||||
|
||||
SUPPORT_TICKET_COUNT: 'SUPPORT_TICKET_COUNT_WIDGET',
|
||||
SUPPORT_RESOLUTION_TIME: 'SUPPORT_RESOLUTION_TIME_WIDGET',
|
||||
SUPPORT_SATISFACTION_SCORE: 'SUPPORT_SATISFACTION_SCORE_WIDGET',
|
||||
SUPPORT_TICKET_BY_PRIORITY: 'SUPPORT_TICKET_BY_PRIORITY_WIDGET',
|
||||
|
||||
PRODUCT_ACTIVE_USERS: 'PRODUCT_ACTIVE_USERS_WIDGET',
|
||||
PRODUCT_FEATURE_ADOPTION: 'PRODUCT_FEATURE_ADOPTION_WIDGET',
|
||||
PRODUCT_USAGE_HEATMAP: 'PRODUCT_USAGE_HEATMAP_WIDGET',
|
||||
PRODUCT_USER_ENGAGEMENT: 'PRODUCT_USER_ENGAGEMENT_WIDGET',
|
||||
|
||||
OPERATIONS_EFFICIENCY_GAUGE: 'OPERATIONS_EFFICIENCY_GAUGE_WIDGET',
|
||||
OPERATIONS_COST_BREAKDOWN: 'OPERATIONS_COST_BREAKDOWN_WIDGET',
|
||||
OPERATIONS_PROCESS_TIME: 'OPERATIONS_PROCESS_TIME_WIDGET',
|
||||
OPERATIONS_ERROR_RATE: 'OPERATIONS_ERROR_RATE_WIDGET',
|
||||
|
||||
FINANCE_CASH_FLOW: 'FINANCE_CASH_FLOW_WIDGET',
|
||||
FINANCE_EXPENSES_PIE: 'FINANCE_EXPENSES_PIE_WIDGET',
|
||||
FINANCE_PROFIT_MARGIN: 'FINANCE_PROFIT_MARGIN_WIDGET',
|
||||
FINANCE_BUDGET_VARIANCE: 'FINANCE_BUDGET_VARIANCE_WIDGET',
|
||||
|
||||
EXECUTIVE_KEY_METRICS: 'EXECUTIVE_KEY_METRICS_WIDGET',
|
||||
EXECUTIVE_COMPANY_HEALTH: 'EXECUTIVE_COMPANY_HEALTH_WIDGET',
|
||||
EXECUTIVE_QUARTERLY_REVIEW: 'EXECUTIVE_QUARTERLY_REVIEW_WIDGET',
|
||||
EXECUTIVE_STRATEGIC_GOALS: 'EXECUTIVE_STRATEGIC_GOALS_WIDGET',
|
||||
TEAM_SIZE: 'TEAM_SIZE_WIDGET',
|
||||
TEAM_GEOGRAPHIC_DISTRIBUTION: 'TEAM_GEOGRAPHIC_DISTRIBUTION_WIDGET',
|
||||
TEAM_CONTACT_ROLES: 'TEAM_CONTACT_ROLES_WIDGET',
|
||||
TEAM_OPEN_TASKS: 'TEAM_OPEN_TASKS_WIDGET',
|
||||
};
|
||||
|
||||
-49
@@ -34,53 +34,4 @@ export const getPageLayoutDataSeeds = (
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.REVENUE_ANALYTICS),
|
||||
name: 'Revenue Analytics Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.MARKETING_METRICS),
|
||||
name: 'Marketing Metrics Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.SUPPORT_DASHBOARD),
|
||||
name: 'Support Dashboard Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.PRODUCT_USAGE),
|
||||
name: 'Product Usage Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.OPERATIONS_KPI),
|
||||
name: 'Operations KPI Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.FINANCE_OVERVIEW),
|
||||
name: 'Finance Overview Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.EXECUTIVE_SUMMARY),
|
||||
name: 'Executive Summary Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
|
||||
+2
-66
@@ -55,80 +55,16 @@ export const getPageLayoutTabDataSeeds = (
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW),
|
||||
title: 'Overview',
|
||||
title: 'Team & People',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS),
|
||||
title: 'Metrics',
|
||||
title: 'Tasks & Activity',
|
||||
position: 1,
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.REVENUE_MAIN),
|
||||
title: 'Revenue',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.REVENUE_ANALYTICS,
|
||||
),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.MARKETING_MAIN),
|
||||
title: 'Marketing',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.MARKETING_METRICS,
|
||||
),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.SUPPORT_MAIN),
|
||||
title: 'Support',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.SUPPORT_DASHBOARD,
|
||||
),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.PRODUCT_MAIN),
|
||||
title: 'Product',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.PRODUCT_USAGE),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.OPERATIONS_MAIN),
|
||||
title: 'Operations',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.OPERATIONS_KPI),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.FINANCE_MAIN),
|
||||
title: 'Finance',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.FINANCE_OVERVIEW,
|
||||
),
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.EXECUTIVE_MAIN),
|
||||
title: 'Executive',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.EXECUTIVE_SUMMARY,
|
||||
),
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
|
||||
+357
-675
File diff suppressed because it is too large
Load Diff
-11
@@ -4,9 +4,6 @@ import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seede
|
||||
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
|
||||
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
|
||||
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
|
||||
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
|
||||
import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util';
|
||||
import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util';
|
||||
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import { seedUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
import { seedWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
@@ -17,7 +14,6 @@ type SeedCoreSchemaArgs = {
|
||||
appVersion: string | undefined;
|
||||
seedBilling?: boolean;
|
||||
seedFeatureFlags?: boolean;
|
||||
seedPageLayouts?: boolean;
|
||||
};
|
||||
|
||||
export const seedCoreSchema = async ({
|
||||
@@ -26,7 +22,6 @@ export const seedCoreSchema = async ({
|
||||
workspaceId,
|
||||
seedBilling = true,
|
||||
seedFeatureFlags: shouldSeedFeatureFlags = true,
|
||||
seedPageLayouts: shouldSeedPageLayouts = true,
|
||||
}: SeedCoreSchemaArgs) => {
|
||||
const schemaName = 'core';
|
||||
|
||||
@@ -50,10 +45,4 @@ export const seedCoreSchema = async ({
|
||||
if (seedBilling) {
|
||||
await seedBillingSubscriptions(dataSource, schemaName, workspaceId);
|
||||
}
|
||||
|
||||
if (shouldSeedPageLayouts) {
|
||||
await seedPageLayouts(dataSource, schemaName, workspaceId);
|
||||
await seedPageLayoutTabs(dataSource, schemaName, workspaceId);
|
||||
await seedPageLayoutWidgets(dataSource, schemaName, workspaceId);
|
||||
}
|
||||
};
|
||||
|
||||
+18
-5
@@ -1,20 +1,33 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util';
|
||||
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[],
|
||||
) => {
|
||||
const pageLayoutWidgets = getPageLayoutWidgetDataSeeds(workspaceId).map(
|
||||
(widget) => ({
|
||||
const pageLayoutWidgets = getPageLayoutWidgetDataSeeds(
|
||||
workspaceId,
|
||||
objectMetadataItems,
|
||||
).map((widget) => {
|
||||
const validatedConfiguration = widget.configuration
|
||||
? validateAndTransformWidgetConfiguration(
|
||||
widget.type,
|
||||
widget.configuration,
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...widget,
|
||||
workspaceId,
|
||||
gridPosition: widget.gridPosition,
|
||||
configuration: widget.configuration,
|
||||
}),
|
||||
);
|
||||
configuration: validatedConfiguration,
|
||||
};
|
||||
});
|
||||
|
||||
if (pageLayoutWidgets.length > 0) {
|
||||
await dataSource
|
||||
|
||||
+1
-86
@@ -26,13 +26,6 @@ export const DASHBOARD_DATA_SEED_IDS = {
|
||||
SALES_OVERVIEW: '20202020-9e82-4342-91ef-c9e70f16a675',
|
||||
CUSTOMER_INSIGHTS: '20202020-d64e-4588-98cc-c56ba821247b',
|
||||
TEAM_PERFORMANCE: '20202020-b888-4c58-8975-76b4c2035d3a',
|
||||
REVENUE_ANALYTICS: '20202020-842e-4cd0-a205-a5bfb1efdb1c',
|
||||
MARKETING_METRICS: '20202020-7b87-4522-9fa8-7f97332c4fa7',
|
||||
SUPPORT_DASHBOARD: '20202020-5404-49ba-a10a-7941205e577f',
|
||||
PRODUCT_USAGE: '20202020-fce2-4c4c-b1dd-6db836141c11',
|
||||
OPERATIONS_KPI: '20202020-c1e4-444c-ab36-4fb42d493eb3',
|
||||
FINANCE_OVERVIEW: '20202020-b993-4601-945b-996ec668f9e9',
|
||||
EXECUTIVE_SUMMARY: '20202020-d2e3-4304-9c80-b3db67903974',
|
||||
};
|
||||
|
||||
export const getDashboardDataSeeds = (
|
||||
@@ -64,89 +57,11 @@ export const getDashboardDataSeeds = (
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.TEAM_PERFORMANCE,
|
||||
title: 'Team Performance',
|
||||
title: 'Team & Activity',
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL,
|
||||
createdByName: 'Phil Schiller',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.REVENUE_ANALYTICS,
|
||||
title: 'Revenue Analytics',
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.REVENUE_ANALYTICS,
|
||||
),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
createdByName: 'Tim Apple',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.MARKETING_METRICS,
|
||||
title: 'Marketing Metrics',
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.MARKETING_METRICS,
|
||||
),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
|
||||
createdByName: 'Jony Ive',
|
||||
position: 4,
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.SUPPORT_DASHBOARD,
|
||||
title: 'Support Dashboard',
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.SUPPORT_DASHBOARD,
|
||||
),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL,
|
||||
createdByName: 'Phil Schiller',
|
||||
position: 5,
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.PRODUCT_USAGE,
|
||||
title: 'Product Usage',
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.PRODUCT_USAGE),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
createdByName: 'Tim Apple',
|
||||
position: 6,
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.OPERATIONS_KPI,
|
||||
title: 'Operations KPI',
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.OPERATIONS_KPI),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
|
||||
createdByName: 'Jony Ive',
|
||||
position: 7,
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.FINANCE_OVERVIEW,
|
||||
title: 'Finance Overview',
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.FINANCE_OVERVIEW,
|
||||
),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL,
|
||||
createdByName: 'Phil Schiller',
|
||||
position: 8,
|
||||
},
|
||||
{
|
||||
id: DASHBOARD_DATA_SEED_IDS.EXECUTIVE_SUMMARY,
|
||||
title: 'Executive Summary',
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.EXECUTIVE_SUMMARY,
|
||||
),
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
createdByName: 'Tim Apple',
|
||||
position: 9,
|
||||
},
|
||||
];
|
||||
|
||||
+20
@@ -6,10 +6,14 @@ import { DataSource } from 'typeorm';
|
||||
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';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
|
||||
import { seedCoreSchema } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util';
|
||||
import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util';
|
||||
import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util';
|
||||
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
|
||||
import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service';
|
||||
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
|
||||
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
|
||||
@@ -73,6 +77,22 @@ export class DevSeederService {
|
||||
|
||||
await this.devSeederPermissionsService.initPermissions(workspaceId);
|
||||
|
||||
await seedPageLayouts(this.coreDataSource, 'core', workspaceId);
|
||||
await seedPageLayoutTabs(this.coreDataSource, 'core', workspaceId);
|
||||
|
||||
const objectMetadataRepository =
|
||||
this.coreDataSource.getRepository(ObjectMetadataEntity);
|
||||
const objectMetadataItems = await objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
await seedPageLayoutWidgets(
|
||||
this.coreDataSource,
|
||||
'core',
|
||||
workspaceId,
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
await this.devSeederDataService.seed({
|
||||
schemaName: dataSourceMetadata.schema,
|
||||
workspaceId,
|
||||
|
||||
Reference in New Issue
Block a user