[Dashboards] Server: Rich Text Widget (#16296)
closes https://github.com/twentyhq/core-team-issues/issues/1893 sync in the transformation layer - with just the blocknote format - <img width="1532" height="1058" alt="CleanShot 2025-12-04 at 16 43 27" src="https://github.com/user-attachments/assets/081483fb-0bd0-4f14-81fc-8ea2261367a2" /> with just the mardown format - <img width="1534" height="1056" alt="CleanShot 2025-12-04 at 16 45 03" src="https://github.com/user-attachments/assets/4eae513b-423d-4597-b79c-97d1f33ab994" /> attaching response since in screen shot its getting cut - ``` { "id": "7189a10a-177c-4cc2-8946-950b8c125913", "pageLayoutTabId": "82ac56dd-4351-43ad-bf3d-49624c057e17", "workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419", "title": "awdawd awdawdawd", "type": "STANDALONE_RICH_TEXT", "objectMetadataId": null, "gridPosition": { "row": 1, "column": 1, "rowSpan": 1, "columnSpan": 1 }, "configuration": { "body": { "markdown": "# Hello World", "blocknote": "[{\"id\":\"2e744f21-7c71-414d-b87f-fb48400c593c\",\"type\":\"heading\",\"props\":{\"textColor\":\"default\",\"backgroundColor\":\"default\",\"textAlignment\":\"left\",\"level\":1},\"content\":[{\"type\":\"text\",\"text\":\"Hello World\",\"styles\":{}}],\"children\":[]}]" } }, "createdAt": "2025-12-04T11:14:52.212Z", "updatedAt": "2025-12-04T11:14:52.212Z", "deletedAt": null } ```
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { type RichTextV2Metadata } from 'twenty-shared/types';
|
||||
|
||||
@ObjectType('RichTextV2Body')
|
||||
export class RichTextV2BodyDTO implements RichTextV2Metadata {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
blocknote?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
markdown: string | null;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNotEmpty, ValidateNested } from 'class-validator';
|
||||
|
||||
import { RichTextV2BodyDTO } from 'src/engine/core-modules/page-layout/dtos/rich-text-v2-body.dto';
|
||||
|
||||
@ObjectType('StandaloneRichTextConfiguration')
|
||||
export class StandaloneRichTextConfigurationDTO {
|
||||
@Field(() => RichTextV2BodyDTO)
|
||||
@ValidateNested()
|
||||
@Type(() => RichTextV2BodyDTO)
|
||||
@IsNotEmpty()
|
||||
body: RichTextV2BodyDTO;
|
||||
}
|
||||
+11
-1
@@ -6,6 +6,7 @@ import { GaugeChartConfigurationDTO } from 'src/engine/core-modules/page-layout/
|
||||
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 { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto';
|
||||
import { StandaloneRichTextConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/standalone-rich-text-configuration.dto';
|
||||
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';
|
||||
|
||||
@@ -18,6 +19,7 @@ export const WidgetConfiguration = createUnionType({
|
||||
AggregateChartConfigurationDTO,
|
||||
GaugeChartConfigurationDTO,
|
||||
IframeConfigurationDTO,
|
||||
StandaloneRichTextConfigurationDTO,
|
||||
],
|
||||
resolveType(configuration: Record<string, unknown>) {
|
||||
if (!('configurationType' in configuration)) {
|
||||
@@ -52,6 +54,13 @@ export const WidgetConfiguration = createUnionType({
|
||||
return IframeConfigurationDTO;
|
||||
}
|
||||
|
||||
if (
|
||||
configuration.configurationType ===
|
||||
WidgetConfigurationType.STANDALONE_RICH_TEXT_CONFIG
|
||||
) {
|
||||
return StandaloneRichTextConfigurationDTO;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unknown widget configuration type: ${configuration.configurationType}`,
|
||||
);
|
||||
@@ -64,4 +73,5 @@ export type WidgetConfigurationInterface =
|
||||
| PieChartConfigurationDTO
|
||||
| AggregateChartConfigurationDTO
|
||||
| GaugeChartConfigurationDTO
|
||||
| IframeConfigurationDTO;
|
||||
| IframeConfigurationDTO
|
||||
| StandaloneRichTextConfigurationDTO;
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
export enum WidgetConfigurationType {
|
||||
CHART_CONFIG = 'CHART_CONFIG',
|
||||
IFRAME_CONFIG = 'IFRAME_CONFIG',
|
||||
STANDALONE_RICH_TEXT_CONFIG = 'STANDALONE_RICH_TEXT_CONFIG',
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,13 +3,14 @@ export enum WidgetType {
|
||||
IFRAME = 'IFRAME',
|
||||
FIELDS = 'FIELDS',
|
||||
GRAPH = 'GRAPH',
|
||||
STANDALONE_RICH_TEXT = 'STANDALONE_RICH_TEXT',
|
||||
TIMELINE = 'TIMELINE',
|
||||
TASKS = 'TASKS',
|
||||
NOTES = 'NOTES',
|
||||
FILES = 'FILES',
|
||||
EMAILS = 'EMAILS',
|
||||
CALENDAR = 'CALENDAR',
|
||||
RICH_TEXT = 'RICH_TEXT',
|
||||
FIELD_RICH_TEXT = 'FIELD_RICH_TEXT',
|
||||
WORKFLOW = 'WORKFLOW',
|
||||
WORKFLOW_VERSION = 'WORKFLOW_VERSION',
|
||||
WORKFLOW_RUN = 'WORKFLOW_RUN',
|
||||
|
||||
+2
-2
@@ -142,7 +142,7 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
|
||||
try {
|
||||
validatedConfig = validateAndTransformWidgetConfiguration({
|
||||
validatedConfig = await validateAndTransformWidgetConfiguration({
|
||||
type: pageLayoutWidgetData.type,
|
||||
configuration: pageLayoutWidgetData.configuration,
|
||||
isDashboardV2Enabled,
|
||||
@@ -246,7 +246,7 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
|
||||
try {
|
||||
validatedConfig = validateAndTransformWidgetConfiguration({
|
||||
validatedConfig = await validateAndTransformWidgetConfiguration({
|
||||
type: typeForValidation,
|
||||
configuration: updateData.configuration,
|
||||
isDashboardV2Enabled,
|
||||
|
||||
+138
-66
@@ -4,6 +4,9 @@ import {
|
||||
INVALID_IFRAME_CONFIG_EMPTY_URL,
|
||||
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
|
||||
INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
|
||||
INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS,
|
||||
INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
|
||||
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
TEST_GAUGE_CHART_CONFIG,
|
||||
TEST_HORIZONTAL_BAR_CHART_CONFIG,
|
||||
@@ -13,6 +16,8 @@ import {
|
||||
TEST_NUMBER_CHART_CONFIG,
|
||||
TEST_NUMBER_CHART_CONFIG_MINIMAL,
|
||||
TEST_PIE_CHART_CONFIG,
|
||||
TEST_STANDALONE_RICH_TEXT_CONFIG,
|
||||
TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
|
||||
TEST_VERTICAL_BAR_CHART_CONFIG,
|
||||
TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
} from 'test/integration/constants/widget-configuration-test-data.constants';
|
||||
@@ -20,10 +25,22 @@ import {
|
||||
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';
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util',
|
||||
() => ({
|
||||
transformRichTextV2Value: jest.fn((value) =>
|
||||
Promise.resolve({
|
||||
blocknote: value.blocknote ?? null,
|
||||
markdown: value.markdown ?? null,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('validateAndTransformWidgetConfiguration', () => {
|
||||
describe('IFRAME widget', () => {
|
||||
it('should validate and transform valid iframe configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
it('should validate and transform valid iframe configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: TEST_IFRAME_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -32,31 +49,86 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toMatchObject(TEST_IFRAME_CONFIG);
|
||||
});
|
||||
|
||||
it('should throw error for invalid URL', () => {
|
||||
expect(() =>
|
||||
it('should throw error for invalid URL', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: INVALID_IFRAME_CONFIG_BAD_URL,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/url must be a URL address/);
|
||||
).rejects.toThrow(/url must be a URL address/);
|
||||
});
|
||||
|
||||
it('should throw error for empty URL', () => {
|
||||
expect(() =>
|
||||
it('should throw error for empty URL', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: INVALID_IFRAME_CONFIG_EMPTY_URL,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/url must be a URL address/);
|
||||
).rejects.toThrow(/url must be a URL address/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('STANDALONE_RICH_TEXT widget', () => {
|
||||
it('should validate and transform valid standalone rich text configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.STANDALONE_RICH_TEXT,
|
||||
configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_STANDALONE_RICH_TEXT_CONFIG);
|
||||
});
|
||||
|
||||
it('should validate minimal standalone rich text configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.STANDALONE_RICH_TEXT,
|
||||
configuration: TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL);
|
||||
});
|
||||
|
||||
it('should throw error for missing body', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.STANDALONE_RICH_TEXT,
|
||||
configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow(/body/);
|
||||
});
|
||||
|
||||
it('should throw error when body is wrong type', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.STANDALONE_RICH_TEXT,
|
||||
configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should strip invalid subfields from body', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.STANDALONE_RICH_TEXT,
|
||||
configuration: INVALID_STANDALONE_RICH_TEXT_CONFIG_INVALID_SUBFIELDS,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect((result as any).body.blocknote).toBeDefined();
|
||||
expect((result as any).body.markdown).toBe('valid');
|
||||
expect((result as any).body.invalidField).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GRAPH widget', () => {
|
||||
describe('NUMBER graph', () => {
|
||||
it('should validate full number graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
it('should validate full number graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_NUMBER_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -65,8 +137,8 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG);
|
||||
});
|
||||
|
||||
it('should validate minimal number graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
it('should validate minimal number graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -75,30 +147,30 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL);
|
||||
});
|
||||
|
||||
it('should throw error for partial number graph configuration with missing required fields', () => {
|
||||
expect(() =>
|
||||
it('should throw error for partial number graph configuration with missing required fields', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/aggregateFieldMetadataId.*aggregateOperation/);
|
||||
).rejects.toThrow(/aggregateFieldMetadataId.*aggregateOperation/);
|
||||
});
|
||||
|
||||
it('should throw error for invalid UUID', () => {
|
||||
expect(() =>
|
||||
it('should throw error for invalid UUID', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
).rejects.toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VERTICAL_BAR graph', () => {
|
||||
it('should validate full vertical bar graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
it('should validate full vertical bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_VERTICAL_BAR_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -107,8 +179,8 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG);
|
||||
});
|
||||
|
||||
it('should validate minimal vertical bar graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
it('should validate minimal vertical bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -117,20 +189,20 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL);
|
||||
});
|
||||
|
||||
it('should throw error for partial vertical bar graph configuration with missing required fields', () => {
|
||||
expect(() =>
|
||||
it('should throw error for partial vertical bar graph configuration with missing required fields', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HORIZONTAL_BAR graph', () => {
|
||||
it('should validate full horizontal bar graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
it('should validate full horizontal bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -139,8 +211,8 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG);
|
||||
});
|
||||
|
||||
it('should validate minimal horizontal bar graph configuration', () => {
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
it('should validate minimal horizontal bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -149,24 +221,24 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL);
|
||||
});
|
||||
|
||||
it('should throw error for partial horizontal bar graph configuration with missing required fields', () => {
|
||||
expect(() =>
|
||||
it('should throw error for partial horizontal bar graph configuration with missing required fields', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for unsupported graph type', () => {
|
||||
it('should return null for unsupported graph type', async () => {
|
||||
const configuration = {
|
||||
graphType: 'UNSUPPORTED',
|
||||
viewId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
};
|
||||
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: configuration,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -175,12 +247,12 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for missing graph type', () => {
|
||||
it('should return null for missing graph type', async () => {
|
||||
const configuration = {
|
||||
viewId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
};
|
||||
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: configuration,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -191,40 +263,40 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should throw error for null configuration', () => {
|
||||
expect(() =>
|
||||
it('should throw error for null configuration', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: null,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
).rejects.toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for undefined configuration', () => {
|
||||
expect(() =>
|
||||
it('should throw error for undefined configuration', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: undefined,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
).rejects.toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for non-object configuration', () => {
|
||||
expect(() =>
|
||||
it('should throw error for non-object configuration', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: 'string',
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
).rejects.toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should return null for unsupported widget type', () => {
|
||||
it('should return null for unsupported widget type', async () => {
|
||||
const configuration = { someField: 'value' };
|
||||
|
||||
const result = validateAndTransformWidgetConfiguration({
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: 'UNSUPPORTED' as WidgetType,
|
||||
configuration: configuration,
|
||||
isDashboardV2Enabled: false,
|
||||
@@ -235,72 +307,72 @@ describe('validateAndTransformWidgetConfiguration', () => {
|
||||
});
|
||||
|
||||
describe('Error messages', () => {
|
||||
it('should include validation details in error message', () => {
|
||||
expect(() =>
|
||||
it('should include validation details in error message', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
).rejects.toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature flags', () => {
|
||||
it('should throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is false', () => {
|
||||
expect(() =>
|
||||
it('should throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is false', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_GAUGE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/);
|
||||
).rejects.toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/);
|
||||
});
|
||||
|
||||
it('should not throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is true', () => {
|
||||
expect(() =>
|
||||
it('should not throw error for GAUGE chart type when IS_DASHBOARD_V2_ENABLED is true', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_GAUGE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: true,
|
||||
}),
|
||||
).not.toThrow();
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw error for PIE chart type regardless of IS_DASHBOARD_V2_ENABLED', () => {
|
||||
expect(() =>
|
||||
it('should not throw error for PIE chart type regardless of IS_DASHBOARD_V2_ENABLED', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_PIE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).not.toThrow();
|
||||
).resolves.not.toThrow();
|
||||
|
||||
expect(() =>
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_PIE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: true,
|
||||
}),
|
||||
).not.toThrow();
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw error for LINE chart type regardless of IS_DASHBOARD_V2_ENABLED', () => {
|
||||
expect(() =>
|
||||
it('should not throw error for LINE chart type regardless of IS_DASHBOARD_V2_ENABLED', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_LINE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).not.toThrow();
|
||||
).resolves.not.toThrow();
|
||||
|
||||
expect(() =>
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_LINE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: true,
|
||||
}),
|
||||
).not.toThrow();
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+9
-2
@@ -18,14 +18,21 @@ export const injectWidgetConfigurationDiscriminator = (
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.IFRAME_CONFIG,
|
||||
} as ConfigurationWithDiscriminator;
|
||||
} satisfies ConfigurationWithDiscriminator;
|
||||
}
|
||||
|
||||
if (widgetType === WidgetType.GRAPH && 'graphType' in configuration) {
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.CHART_CONFIG,
|
||||
} as ConfigurationWithDiscriminator;
|
||||
} satisfies ConfigurationWithDiscriminator;
|
||||
}
|
||||
|
||||
if (widgetType === WidgetType.STANDALONE_RICH_TEXT) {
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT_CONFIG,
|
||||
} satisfies ConfigurationWithDiscriminator;
|
||||
}
|
||||
|
||||
return configuration as ConfigurationWithDiscriminator;
|
||||
|
||||
+30
-2
@@ -3,11 +3,13 @@ import { validateSync, type ValidationError } from 'class-validator';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AggregateChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/aggregate-chart-configuration.dto';
|
||||
import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util';
|
||||
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 { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto';
|
||||
import { StandaloneRichTextConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/standalone-rich-text-configuration.dto';
|
||||
import { type WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface';
|
||||
import { BarChartGroupMode } from 'src/engine/core-modules/page-layout/enums/bar-chart-group-mode.enum';
|
||||
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
|
||||
@@ -161,7 +163,31 @@ const validateIframeConfiguration = (
|
||||
return instance;
|
||||
};
|
||||
|
||||
export const validateAndTransformWidgetConfiguration = ({
|
||||
const validateStandaloneRichTextConfiguration = async (
|
||||
configuration: unknown,
|
||||
): Promise<WidgetConfigurationInterface | null> => {
|
||||
const instance = plainToInstance(
|
||||
StandaloneRichTextConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const errors = validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
if (instance.body) {
|
||||
instance.body = await transformRichTextV2Value(instance.body);
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export const validateAndTransformWidgetConfiguration = async ({
|
||||
type,
|
||||
configuration,
|
||||
isDashboardV2Enabled,
|
||||
@@ -169,7 +195,7 @@ export const validateAndTransformWidgetConfiguration = ({
|
||||
type: WidgetType;
|
||||
configuration: unknown;
|
||||
isDashboardV2Enabled: boolean;
|
||||
}): WidgetConfigurationInterface | null => {
|
||||
}): Promise<WidgetConfigurationInterface | null> => {
|
||||
if (!configuration || typeof configuration !== 'object') {
|
||||
throw new Error('Invalid configuration: not an object');
|
||||
}
|
||||
@@ -183,6 +209,8 @@ export const validateAndTransformWidgetConfiguration = ({
|
||||
});
|
||||
case WidgetType.IFRAME:
|
||||
return validateIframeConfiguration(configuration);
|
||||
case WidgetType.STANDALONE_RICH_TEXT:
|
||||
return await validateStandaloneRichTextConfiguration(configuration);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user