[PAGE LAYOUTS] Add widgets validation (#16635)
- Add widget validation - Remove 'None' option for primary axis group by - Fix error message parsing by passing the operation type in `useMetadataErrorHandler`
This commit is contained in:
-378
@@ -1,378 +0,0 @@
|
||||
import {
|
||||
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
INVALID_IFRAME_CONFIG_BAD_URL,
|
||||
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,
|
||||
TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
TEST_IFRAME_CONFIG,
|
||||
TEST_LINE_CHART_CONFIG,
|
||||
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';
|
||||
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { validateAndTransformWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/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', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: TEST_IFRAME_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_IFRAME_CONFIG);
|
||||
});
|
||||
|
||||
it('should throw error for invalid URL', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: INVALID_IFRAME_CONFIG_BAD_URL,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow(/url must be a URL address/);
|
||||
});
|
||||
|
||||
it('should throw error for empty URL', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: INVALID_IFRAME_CONFIG_EMPTY_URL,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).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', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_NUMBER_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG);
|
||||
});
|
||||
|
||||
it('should validate minimal number graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL);
|
||||
});
|
||||
|
||||
it('should throw error for partial number graph configuration with missing required fields', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow(/aggregateFieldMetadataId.*aggregateOperation/);
|
||||
});
|
||||
|
||||
it('should throw error for invalid UUID', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VERTICAL_BAR graph', () => {
|
||||
it('should validate full vertical bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_VERTICAL_BAR_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG);
|
||||
});
|
||||
|
||||
it('should validate minimal vertical bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL);
|
||||
});
|
||||
|
||||
it('should throw error for partial vertical bar graph configuration with missing required fields', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HORIZONTAL_BAR graph', () => {
|
||||
it('should validate full horizontal bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG);
|
||||
});
|
||||
|
||||
it('should validate minimal horizontal bar graph configuration', async () => {
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL);
|
||||
});
|
||||
|
||||
it('should throw error for partial horizontal bar graph configuration with missing required fields', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for unsupported graph type', async () => {
|
||||
const configuration = {
|
||||
graphType: 'UNSUPPORTED',
|
||||
viewId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
};
|
||||
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: configuration,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for missing graph type', async () => {
|
||||
const configuration = {
|
||||
viewId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
};
|
||||
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: configuration,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should throw error for null configuration', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: null,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for undefined configuration', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: undefined,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for non-object configuration', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: 'string',
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should return null for unsupported widget type', async () => {
|
||||
const configuration = { someField: 'value' };
|
||||
|
||||
const result = await validateAndTransformWidgetConfiguration({
|
||||
type: 'UNSUPPORTED' as WidgetType,
|
||||
configuration: configuration,
|
||||
isDashboardV2Enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error messages', () => {
|
||||
it('should include validation details in error message', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).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', async () => {
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_GAUGE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: false,
|
||||
}),
|
||||
).rejects.toThrow(/IS_DASHBOARD_V2_ENABLED feature flag/);
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_PIE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: true,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
await expect(
|
||||
validateAndTransformWidgetConfiguration({
|
||||
type: WidgetType.GRAPH,
|
||||
configuration: TEST_LINE_CHART_CONFIG,
|
||||
isDashboardV2Enabled: true,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
import {
|
||||
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
INVALID_IFRAME_CONFIG_BAD_URL,
|
||||
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_MISSING_BODY,
|
||||
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
TEST_GAUGE_CHART_CONFIG,
|
||||
TEST_HORIZONTAL_BAR_CHART_CONFIG,
|
||||
TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
TEST_IFRAME_CONFIG,
|
||||
TEST_LINE_CHART_CONFIG,
|
||||
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';
|
||||
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
|
||||
|
||||
describe('validateWidgetConfigurationInput', () => {
|
||||
describe('IFRAME widget', () => {
|
||||
it('should not throw for valid iframe configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_IFRAME_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for invalid URL', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: {
|
||||
...INVALID_IFRAME_CONFIG_BAD_URL,
|
||||
configurationType: WidgetConfigurationType.IFRAME,
|
||||
},
|
||||
}),
|
||||
).toThrow(/url must be a URL address/);
|
||||
});
|
||||
|
||||
it('should throw error for empty URL', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: {
|
||||
...INVALID_IFRAME_CONFIG_EMPTY_URL,
|
||||
configurationType: WidgetConfigurationType.IFRAME,
|
||||
},
|
||||
}),
|
||||
).toThrow(/url must be a URL address/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('STANDALONE_RICH_TEXT widget', () => {
|
||||
it('should not throw for valid standalone rich text configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for minimal standalone rich text configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for missing body', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: {
|
||||
...INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
|
||||
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
|
||||
},
|
||||
}),
|
||||
).toThrow(/body/);
|
||||
});
|
||||
|
||||
it('should throw error when body is wrong type', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: {
|
||||
...INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
|
||||
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GRAPH widget', () => {
|
||||
describe('AGGREGATE_CHART graph', () => {
|
||||
it('should not throw for full aggregate chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_NUMBER_CHART_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for minimal aggregate chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for partial aggregate chart configuration with missing required fields', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
|
||||
}),
|
||||
).toThrow(/aggregateFieldMetadataId.*aggregateOperation/);
|
||||
});
|
||||
|
||||
it('should throw error for invalid UUID', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
|
||||
}),
|
||||
).toThrow(/aggregateFieldMetadataId must be a UUID/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BAR_CHART graph', () => {
|
||||
describe('VERTICAL layout', () => {
|
||||
it('should not throw for full vertical bar chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_VERTICAL_BAR_CHART_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for minimal vertical bar chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for partial vertical bar chart configuration with missing required fields', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
}),
|
||||
).toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HORIZONTAL layout', () => {
|
||||
it('should not throw for full horizontal bar chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for minimal horizontal bar chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for partial horizontal bar chart configuration with missing required fields', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration:
|
||||
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
|
||||
}),
|
||||
).toThrow(/primaryAxisGroupByFieldMetadataId/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PIE_CHART graph', () => {
|
||||
it('should not throw for pie chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_PIE_CHART_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LINE_CHART graph', () => {
|
||||
it('should not throw for line chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_LINE_CHART_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GAUGE_CHART graph', () => {
|
||||
it('should not throw for gauge chart configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: TEST_GAUGE_CHART_CONFIG,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should throw error for null configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: null,
|
||||
}),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for undefined configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: undefined,
|
||||
}),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for non-object configuration', () => {
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: 'string',
|
||||
}),
|
||||
).toThrow('Invalid configuration: not an object');
|
||||
});
|
||||
|
||||
it('should throw error for missing configurationType', () => {
|
||||
const configuration = { someField: 'value' };
|
||||
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: configuration,
|
||||
}),
|
||||
).toThrow('Invalid configuration: missing configuration type');
|
||||
});
|
||||
|
||||
it('should throw error for unsupported configurationType', () => {
|
||||
const configuration = {
|
||||
configurationType: 'UNSUPPORTED_TYPE',
|
||||
someField: 'value',
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateWidgetConfigurationInput({
|
||||
configuration: configuration,
|
||||
}),
|
||||
).toThrow(/Invalid configuration type: UNSUPPORTED_TYPE/);
|
||||
});
|
||||
});
|
||||
});
|
||||
+167
-130
@@ -1,6 +1,6 @@
|
||||
import { WIDGET_GRID_MAX_COLUMNS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-columns.constant';
|
||||
import { WIDGET_GRID_MAX_ROWS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-rows.constant';
|
||||
import { PageLayoutWidgetException } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
|
||||
|
||||
describe('validateWidgetGridPosition', () => {
|
||||
@@ -12,176 +12,213 @@ describe('validateWidgetGridPosition', () => {
|
||||
};
|
||||
|
||||
describe('Valid grid positions', () => {
|
||||
it('should not throw for valid grid position', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(validGridPosition, 'Test Widget'),
|
||||
).not.toThrow();
|
||||
it('should return empty array for valid grid position', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
validGridPosition,
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not throw for widget at max column boundary', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: WIDGET_GRID_MAX_COLUMNS - 1,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'Test Widget',
|
||||
),
|
||||
).not.toThrow();
|
||||
it('should return empty array for widget at max column boundary', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: WIDGET_GRID_MAX_COLUMNS - 1,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not throw for widget at max row boundary', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS - 1,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'Test Widget',
|
||||
),
|
||||
).not.toThrow();
|
||||
it('should return empty array for widget at max row boundary', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS - 1,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not throw for widget spanning to column grid edge', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: 8,
|
||||
rowSpan: 1,
|
||||
columnSpan: 4,
|
||||
},
|
||||
'Test Widget',
|
||||
),
|
||||
).not.toThrow();
|
||||
it('should return empty array for widget spanning to column grid edge', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: 8,
|
||||
rowSpan: 1,
|
||||
columnSpan: 4,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not throw for widget spanning to row grid edge', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS - 5,
|
||||
column: 0,
|
||||
rowSpan: 5,
|
||||
columnSpan: 6,
|
||||
},
|
||||
'Test Widget',
|
||||
),
|
||||
).not.toThrow();
|
||||
it('should return empty array for widget spanning to row grid edge', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS - 5,
|
||||
column: 0,
|
||||
rowSpan: 5,
|
||||
columnSpan: 6,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid row positions', () => {
|
||||
it('should throw for row exceeding max rows', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{ ...validGridPosition, row: WIDGET_GRID_MAX_ROWS },
|
||||
'Test Widget',
|
||||
),
|
||||
).toThrow(PageLayoutWidgetException);
|
||||
it('should return error for row exceeding max rows', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{ ...validGridPosition, row: WIDGET_GRID_MAX_ROWS },
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].code).toBe(
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when widget extends beyond grid height', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS - 2,
|
||||
column: 0,
|
||||
rowSpan: 5,
|
||||
columnSpan: 6,
|
||||
},
|
||||
'Test Widget',
|
||||
it('should return error when widget extends beyond grid height', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS - 2,
|
||||
column: 0,
|
||||
rowSpan: 5,
|
||||
columnSpan: 6,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
errors.some((error) =>
|
||||
error.message.includes('extends beyond grid height'),
|
||||
),
|
||||
).toThrow(/extends beyond grid height/);
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid column positions', () => {
|
||||
it('should throw for column exceeding max columns', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{ ...validGridPosition, column: WIDGET_GRID_MAX_COLUMNS },
|
||||
'Test Widget',
|
||||
),
|
||||
).toThrow(PageLayoutWidgetException);
|
||||
it('should return error for column exceeding max columns', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{ ...validGridPosition, column: WIDGET_GRID_MAX_COLUMNS },
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].code).toBe(
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Widget extending beyond grid', () => {
|
||||
it('should throw when widget extends beyond grid width', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: 10,
|
||||
rowSpan: 1,
|
||||
columnSpan: 3,
|
||||
},
|
||||
'Test Widget',
|
||||
it('should return error when widget extends beyond grid width', () => {
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: 10,
|
||||
rowSpan: 1,
|
||||
columnSpan: 3,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
errors.some((error) =>
|
||||
error.message.includes('extends beyond grid width'),
|
||||
),
|
||||
).toThrow(/extends beyond grid width/);
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error messages', () => {
|
||||
it('should include max columns value in error', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: 10,
|
||||
rowSpan: 1,
|
||||
columnSpan: 5,
|
||||
},
|
||||
'Test Widget',
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: 0,
|
||||
column: 10,
|
||||
rowSpan: 1,
|
||||
columnSpan: 5,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
errors.some((error) =>
|
||||
error.message.includes(WIDGET_GRID_MAX_COLUMNS.toString()),
|
||||
),
|
||||
).toThrow(new RegExp(WIDGET_GRID_MAX_COLUMNS.toString()));
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should include max rows value in error for row start', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS + 10,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'Test Widget',
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS + 10,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
errors.some((error) =>
|
||||
error.message.includes(WIDGET_GRID_MAX_ROWS.toString()),
|
||||
),
|
||||
).toThrow(new RegExp(WIDGET_GRID_MAX_ROWS.toString()));
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should include max rows value in error for row extension', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: 95,
|
||||
column: 0,
|
||||
rowSpan: 10,
|
||||
columnSpan: 6,
|
||||
},
|
||||
'Test Widget',
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: 95,
|
||||
column: 0,
|
||||
rowSpan: 10,
|
||||
columnSpan: 6,
|
||||
},
|
||||
'Test Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
errors.some((error) =>
|
||||
error.message.includes(WIDGET_GRID_MAX_ROWS.toString()),
|
||||
),
|
||||
).toThrow(new RegExp(WIDGET_GRID_MAX_ROWS.toString()));
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should include widget title in error message', () => {
|
||||
expect(() =>
|
||||
validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'My Custom Widget',
|
||||
),
|
||||
).toThrow(/My Custom Widget/);
|
||||
const errors = validateWidgetGridPosition(
|
||||
{
|
||||
row: WIDGET_GRID_MAX_ROWS,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
'My Custom Widget',
|
||||
);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
errors.some((error) => error.message.includes('My Custom Widget')),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync, type ValidationError } from 'class-validator';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util';
|
||||
import { AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
|
||||
import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
|
||||
import { GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
|
||||
import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-configuration.dto';
|
||||
import { LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
|
||||
import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
|
||||
import { StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
|
||||
import { BarChartGroupMode } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-group-mode.enum';
|
||||
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
|
||||
|
||||
const formatValidationErrors = (errors: ValidationError[]): string => {
|
||||
return errors
|
||||
.map((err) => {
|
||||
const constraints = err.constraints
|
||||
? Object.values(err.constraints).join(', ')
|
||||
: 'Unknown error';
|
||||
|
||||
return `${err.property}: ${constraints}`;
|
||||
})
|
||||
.join('; ');
|
||||
};
|
||||
|
||||
const validateGraphConfiguration = ({
|
||||
configuration,
|
||||
isDashboardV2Enabled,
|
||||
}: {
|
||||
configuration: Record<string, unknown>;
|
||||
isDashboardV2Enabled: boolean;
|
||||
}): AllPageLayoutWidgetConfiguration | null => {
|
||||
const configurationType = configuration.configurationType as GraphType;
|
||||
|
||||
if (
|
||||
!configurationType ||
|
||||
!Object.values(GraphType).includes(configurationType)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configurationType === GraphType.GAUGE_CHART && !isDashboardV2Enabled) {
|
||||
throw new Error(
|
||||
`Chart type ${configurationType} requires IS_DASHBOARD_V2_ENABLED feature flag`,
|
||||
);
|
||||
}
|
||||
|
||||
switch (configurationType) {
|
||||
case GraphType.BAR_CHART: {
|
||||
const instance = plainToInstance(BarChartConfigurationDTO, configuration);
|
||||
|
||||
const errors = validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(instance.secondaryAxisGroupByFieldMetadataId) &&
|
||||
!isDefined(instance.groupMode)
|
||||
) {
|
||||
instance.groupMode = BarChartGroupMode.STACKED;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
case GraphType.LINE_CHART: {
|
||||
const instance = plainToInstance(
|
||||
LineChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const errors = validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(instance.secondaryAxisGroupByFieldMetadataId) &&
|
||||
!isDefined(instance.isStacked)
|
||||
) {
|
||||
instance.isStacked = true;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
case GraphType.PIE_CHART: {
|
||||
const instance = plainToInstance(PieChartConfigurationDTO, configuration);
|
||||
|
||||
const errors = validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
case GraphType.AGGREGATE_CHART: {
|
||||
const instance = plainToInstance(
|
||||
AggregateChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const errors = validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
case GraphType.GAUGE_CHART: {
|
||||
const instance = plainToInstance(
|
||||
GaugeChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const errors = validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const validateIframeConfiguration = (
|
||||
configuration: unknown,
|
||||
): AllPageLayoutWidgetConfiguration | null => {
|
||||
const instance = plainToInstance(IframeConfigurationDTO, configuration);
|
||||
|
||||
const errors = validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
const validateStandaloneRichTextConfiguration = async (
|
||||
configuration: unknown,
|
||||
): Promise<AllPageLayoutWidgetConfiguration | 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,
|
||||
}: {
|
||||
type: WidgetType;
|
||||
configuration: unknown;
|
||||
isDashboardV2Enabled: boolean;
|
||||
}): Promise<AllPageLayoutWidgetConfiguration | null> => {
|
||||
if (!configuration || typeof configuration !== 'object') {
|
||||
throw new Error('Invalid configuration: not an object');
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case WidgetType.GRAPH:
|
||||
return validateGraphConfiguration({
|
||||
configuration: configuration as Record<string, unknown>,
|
||||
isDashboardV2Enabled,
|
||||
});
|
||||
case WidgetType.IFRAME:
|
||||
return validateIframeConfiguration(configuration);
|
||||
case WidgetType.STANDALONE_RICH_TEXT:
|
||||
return await validateStandaloneRichTextConfiguration(configuration);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
if (Array.isArray(error)) {
|
||||
const errorMessage = formatValidationErrors(error);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { type ClassConstructor } from 'class-transformer/types/interfaces';
|
||||
import { validateSync, type ValidationError } from 'class-validator';
|
||||
|
||||
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
|
||||
|
||||
export const validateWidgetConfigurationByDto = <
|
||||
T extends AllPageLayoutWidgetConfiguration,
|
||||
>(
|
||||
DtoClass: ClassConstructor<T>,
|
||||
configuration: unknown,
|
||||
): ValidationError[] => {
|
||||
const instance = plainToInstance(DtoClass, configuration);
|
||||
|
||||
return validateSync(instance, {
|
||||
whitelist: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
};
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import { isNotEmptyObject, type ValidationError } from 'class-validator';
|
||||
|
||||
import { AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
|
||||
import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
|
||||
import { GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
|
||||
import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-configuration.dto';
|
||||
import { LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
|
||||
import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
|
||||
import { StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import {
|
||||
PageLayoutWidgetException,
|
||||
PageLayoutWidgetExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { validateWidgetConfigurationByDto } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-by-dto.util';
|
||||
|
||||
const formatValidationErrors = (
|
||||
errors: ValidationError[],
|
||||
parentProperty?: string,
|
||||
): string => {
|
||||
return errors
|
||||
.map((err) => {
|
||||
const propertyPath = parentProperty
|
||||
? `${parentProperty}.${err.property}`
|
||||
: err.property;
|
||||
|
||||
if (err.constraints) {
|
||||
const constraints = Object.values(err.constraints).join(', ');
|
||||
|
||||
return `${propertyPath}: ${constraints}`;
|
||||
}
|
||||
|
||||
if (err.children && err.children.length > 0) {
|
||||
return formatValidationErrors(err.children, propertyPath);
|
||||
}
|
||||
|
||||
return `${propertyPath}: Unknown error`;
|
||||
})
|
||||
.join('; ');
|
||||
};
|
||||
|
||||
export const validateWidgetConfigurationInput = ({
|
||||
configuration,
|
||||
}: {
|
||||
configuration: unknown;
|
||||
}): void => {
|
||||
if (!isNotEmptyObject(configuration)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
'Invalid configuration: not an object',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const configurationRecord = configuration as Record<string, unknown>;
|
||||
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
configurationRecord,
|
||||
'configurationType',
|
||||
)
|
||||
) {
|
||||
throw new PageLayoutWidgetException(
|
||||
'Invalid configuration: missing configuration type',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const configurationType =
|
||||
configurationRecord.configurationType as WidgetConfigurationType;
|
||||
|
||||
let errors: ValidationError[] = [];
|
||||
|
||||
switch (configurationType) {
|
||||
case WidgetConfigurationType.BAR_CHART:
|
||||
errors = validateWidgetConfigurationByDto(
|
||||
BarChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
break;
|
||||
case WidgetConfigurationType.LINE_CHART:
|
||||
errors = validateWidgetConfigurationByDto(
|
||||
LineChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
break;
|
||||
case WidgetConfigurationType.PIE_CHART:
|
||||
errors = validateWidgetConfigurationByDto(
|
||||
PieChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
break;
|
||||
case WidgetConfigurationType.AGGREGATE_CHART:
|
||||
errors = validateWidgetConfigurationByDto(
|
||||
AggregateChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
break;
|
||||
case WidgetConfigurationType.GAUGE_CHART:
|
||||
errors = validateWidgetConfigurationByDto(
|
||||
GaugeChartConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
break;
|
||||
case WidgetConfigurationType.IFRAME:
|
||||
errors = validateWidgetConfigurationByDto(
|
||||
IframeConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
break;
|
||||
case WidgetConfigurationType.STANDALONE_RICH_TEXT:
|
||||
errors = validateWidgetConfigurationByDto(
|
||||
StandaloneRichTextConfigurationDTO,
|
||||
configuration,
|
||||
);
|
||||
break;
|
||||
case WidgetConfigurationType.VIEW:
|
||||
throw new PageLayoutWidgetException(
|
||||
'View configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.FIELD:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Field configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.FIELDS:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Fields configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.TIMELINE:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Timeline configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.TASKS:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Tasks configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.NOTES:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Notes configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.FILES:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Files configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.EMAILS:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Emails configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.CALENDAR:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Calendar configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.FIELD_RICH_TEXT:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Field rich text configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.WORKFLOW:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Workflow configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.WORKFLOW_VERSION:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Workflow version configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
case WidgetConfigurationType.WORKFLOW_RUN:
|
||||
throw new PageLayoutWidgetException(
|
||||
'Workflow run configuration is not supported yet',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
default:
|
||||
throw new PageLayoutWidgetException(
|
||||
`Invalid configuration type: ${configurationType}`,
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new PageLayoutWidgetException(
|
||||
formatValidationErrors(errors),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
};
|
||||
+28
-18
@@ -1,11 +1,13 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { WIDGET_GRID_MAX_COLUMNS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-columns.constant';
|
||||
import { WIDGET_GRID_MAX_ROWS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-rows.constant';
|
||||
import {
|
||||
PageLayoutWidgetException,
|
||||
PageLayoutWidgetExceptionCode,
|
||||
PageLayoutWidgetExceptionMessageKey,
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
type GridPosition = {
|
||||
row: number;
|
||||
@@ -17,54 +19,62 @@ type GridPosition = {
|
||||
export const validateWidgetGridPosition = (
|
||||
gridPosition: GridPosition,
|
||||
widgetTitle: string,
|
||||
): void => {
|
||||
): FlatEntityValidationError[] => {
|
||||
const errors: FlatEntityValidationError[] = [];
|
||||
|
||||
const { row, column, rowSpan, columnSpan } = gridPosition;
|
||||
|
||||
if (column >= WIDGET_GRID_MAX_COLUMNS) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`column ${column} exceeds grid width (max column is ${WIDGET_GRID_MAX_COLUMNS - 1})`,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
userFriendlyMessage: msg`Widget extends beyond grid width`,
|
||||
});
|
||||
}
|
||||
|
||||
if (column + columnSpan > WIDGET_GRID_MAX_COLUMNS) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`widget extends beyond grid width (column ${column} + columnSpan ${columnSpan} > ${WIDGET_GRID_MAX_COLUMNS})`,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
userFriendlyMessage: msg`Widget extends beyond grid width`,
|
||||
});
|
||||
}
|
||||
|
||||
if (row >= WIDGET_GRID_MAX_ROWS) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`row ${row} exceeds maximum allowed rows (${WIDGET_GRID_MAX_ROWS})`,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
userFriendlyMessage: msg`Widget row exceeds grid height`,
|
||||
});
|
||||
}
|
||||
|
||||
if (row + rowSpan > WIDGET_GRID_MAX_ROWS) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`widget extends beyond grid height (row ${row} + rowSpan ${rowSpan} > ${WIDGET_GRID_MAX_ROWS})`,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
userFriendlyMessage: msg`Widget extends beyond grid height`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user