[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:
Raphaël Bosi
2026-01-07 16:00:13 +01:00
committed by GitHub
parent 701a713042
commit 4faed25624
71 changed files with 4004 additions and 1220 deletions
@@ -1,12 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
@Module({
imports: [TypeOrmModule.forFeature([PageLayoutWidgetEntity])],
providers: [WorkspaceFlatPageLayoutWidgetMapCacheService],
exports: [WorkspaceFlatPageLayoutWidgetMapCacheService],
providers: [
WorkspaceFlatPageLayoutWidgetMapCacheService,
FlatPageLayoutWidgetTypeValidatorService,
],
exports: [
WorkspaceFlatPageLayoutWidgetMapCacheService,
FlatPageLayoutWidgetTypeValidatorService,
],
})
export class FlatPageLayoutWidgetModule {}
@@ -0,0 +1,229 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import {
type FlatPageLayoutWidgetTypeValidatorForCreation,
type FlatPageLayoutWidgetTypeValidatorForUpdate,
} from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-type-validator.type';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { rejectWidgetType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/reject-widget-type.util';
import { validateGraphFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-creation.util';
import { validateGraphFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-flat-page-layout-widget-for-update.util';
import { validateIframeFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-creation.util';
import { validateIframeFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-update.util';
import { validateStandaloneRichTextFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-creation.util';
import { validateStandaloneRichTextFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-update.util';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
export type GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs =
FlatEntityValidationArgs<'pageLayoutWidget'> & {
updates?: FlatEntityPropertiesUpdates<'pageLayoutWidget'>;
};
export type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs =
FlatEntityValidationArgs<'pageLayoutWidget'>;
export type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs =
FlatEntityValidationArgs<'pageLayoutWidget'> & {
updates: FlatEntityPropertiesUpdates<'pageLayoutWidget'>;
};
@Injectable()
export class FlatPageLayoutWidgetTypeValidatorService {
constructor() {}
private readonly PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_CREATION_HASHMAP: FlatPageLayoutWidgetTypeValidatorForCreation =
{
VIEW: rejectWidgetType(
WidgetType.VIEW,
'Widget type VIEW is not supported yet.',
msg`Widget type VIEW is not supported yet.`,
),
IFRAME: validateIframeFlatPageLayoutWidgetForCreation,
FIELD: rejectWidgetType(
WidgetType.FIELD,
'Widget type FIELD is not supported yet.',
msg`Widget type FIELD is not supported yet.`,
),
FIELDS: rejectWidgetType(
WidgetType.FIELDS,
'Widget type FIELDS is not supported yet.',
msg`Widget type FIELDS is not supported yet.`,
),
GRAPH: validateGraphFlatPageLayoutWidgetForCreation,
STANDALONE_RICH_TEXT:
validateStandaloneRichTextFlatPageLayoutWidgetForCreation,
TIMELINE: rejectWidgetType(
WidgetType.TIMELINE,
'Widget type TIMELINE is not supported yet.',
msg`Widget type TIMELINE is not supported yet.`,
),
TASKS: rejectWidgetType(
WidgetType.TASKS,
'Widget type TASKS is not supported yet.',
msg`Widget type TASKS is not supported yet.`,
),
NOTES: rejectWidgetType(
WidgetType.NOTES,
'Widget type NOTES is not supported yet.',
msg`Widget type NOTES is not supported yet.`,
),
FILES: rejectWidgetType(
WidgetType.FILES,
'Widget type FILES is not supported yet.',
msg`Widget type FILES is not supported yet.`,
),
EMAILS: rejectWidgetType(
WidgetType.EMAILS,
'Widget type EMAILS is not supported yet.',
msg`Widget type EMAILS is not supported yet.`,
),
CALENDAR: rejectWidgetType(
WidgetType.CALENDAR,
'Widget type CALENDAR is not supported yet.',
msg`Widget type CALENDAR is not supported yet.`,
),
FIELD_RICH_TEXT: rejectWidgetType(
WidgetType.FIELD_RICH_TEXT,
'Widget type FIELD_RICH_TEXT is not supported yet.',
msg`Widget type FIELD_RICH_TEXT is not supported yet.`,
),
WORKFLOW: rejectWidgetType(
WidgetType.WORKFLOW,
'Widget type WORKFLOW is not supported yet.',
msg`Widget type WORKFLOW is not supported yet.`,
),
WORKFLOW_VERSION: rejectWidgetType(
WidgetType.WORKFLOW_VERSION,
'Widget type WORKFLOW_VERSION is not supported yet.',
msg`Widget type WORKFLOW_VERSION is not supported yet.`,
),
WORKFLOW_RUN: rejectWidgetType(
WidgetType.WORKFLOW_RUN,
'Widget type WORKFLOW_RUN is not supported yet.',
msg`Widget type WORKFLOW_RUN is not supported yet.`,
),
};
private readonly PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_UPDATE_HASHMAP: FlatPageLayoutWidgetTypeValidatorForUpdate =
{
VIEW: rejectWidgetType(
WidgetType.VIEW,
'Widget type VIEW is not supported yet.',
msg`Widget type VIEW is not supported yet.`,
),
IFRAME: validateIframeFlatPageLayoutWidgetForUpdate,
FIELD: rejectWidgetType(
WidgetType.FIELD,
'Widget type FIELD is not supported yet.',
msg`Widget type FIELD is not supported yet.`,
),
FIELDS: rejectWidgetType(
WidgetType.FIELDS,
'Widget type FIELDS is not supported yet.',
msg`Widget type FIELDS is not supported yet.`,
),
GRAPH: validateGraphFlatPageLayoutWidgetForUpdate,
STANDALONE_RICH_TEXT:
validateStandaloneRichTextFlatPageLayoutWidgetForUpdate,
TIMELINE: rejectWidgetType(
WidgetType.TIMELINE,
'Widget type TIMELINE is not supported yet.',
msg`Widget type TIMELINE is not supported yet.`,
),
TASKS: rejectWidgetType(
WidgetType.TASKS,
'Widget type TASKS is not supported yet.',
msg`Widget type TASKS is not supported yet.`,
),
NOTES: rejectWidgetType(
WidgetType.NOTES,
'Widget type NOTES is not supported yet.',
msg`Widget type NOTES is not supported yet.`,
),
FILES: rejectWidgetType(
WidgetType.FILES,
'Widget type FILES is not supported yet.',
msg`Widget type FILES is not supported yet.`,
),
EMAILS: rejectWidgetType(
WidgetType.EMAILS,
'Widget type EMAILS is not supported yet.',
msg`Widget type EMAILS is not supported yet.`,
),
CALENDAR: rejectWidgetType(
WidgetType.CALENDAR,
'Widget type CALENDAR is not supported yet.',
msg`Widget type CALENDAR is not supported yet.`,
),
FIELD_RICH_TEXT: rejectWidgetType(
WidgetType.FIELD_RICH_TEXT,
'Widget type FIELD_RICH_TEXT is not supported yet.',
msg`Widget type FIELD_RICH_TEXT is not supported yet.`,
),
WORKFLOW: rejectWidgetType(
WidgetType.WORKFLOW,
'Widget type WORKFLOW is not supported yet.',
msg`Widget type WORKFLOW is not supported yet.`,
),
WORKFLOW_VERSION: rejectWidgetType(
WidgetType.WORKFLOW_VERSION,
'Widget type WORKFLOW_VERSION is not supported yet.',
msg`Widget type WORKFLOW_VERSION is not supported yet.`,
),
WORKFLOW_RUN: rejectWidgetType(
WidgetType.WORKFLOW_RUN,
'Widget type WORKFLOW_RUN is not supported yet.',
msg`Widget type WORKFLOW_RUN is not supported yet.`,
),
};
public validateFlatPageLayoutWidgetTypeSpecificitiesForCreation(
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
): FlatPageLayoutWidgetValidationError[] {
const { flatEntityToValidate } = args;
const widgetType = flatEntityToValidate.type;
const pageLayoutWidgetTypeValidator =
this.PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_CREATION_HASHMAP[widgetType];
if (!isDefined(pageLayoutWidgetTypeValidator)) {
return [
{
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: `Unsupported page layout widget type ${widgetType}`,
value: widgetType,
userFriendlyMessage: msg`Unsupported page layout widget type ${widgetType}`,
},
];
}
return pageLayoutWidgetTypeValidator(args);
}
public validateFlatPageLayoutWidgetTypeSpecificitiesForUpdate(
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
): FlatPageLayoutWidgetValidationError[] {
const { flatEntityToValidate } = args;
const widgetType = flatEntityToValidate.type;
const pageLayoutWidgetTypeValidator =
this.PAGE_LAYOUT_WIDGET_TYPE_VALIDATOR_FOR_UPDATE_HASHMAP[widgetType];
if (!isDefined(pageLayoutWidgetTypeValidator)) {
return [
{
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: `Unsupported page layout widget type ${widgetType}`,
value: widgetType,
userFriendlyMessage: msg`Unsupported page layout widget type ${widgetType}`,
},
];
}
return pageLayoutWidgetTypeValidator(args);
}
}
@@ -0,0 +1,25 @@
import {
type GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs,
type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
} from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
export type FlatPageLayoutWidgetTypeValidator = {
[T in WidgetType]: (
args: GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs,
) => FlatPageLayoutWidgetValidationError[];
};
export type FlatPageLayoutWidgetTypeValidatorForCreation = {
[T in WidgetType]: (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
) => FlatPageLayoutWidgetValidationError[];
};
export type FlatPageLayoutWidgetTypeValidatorForUpdate = {
[T in WidgetType]: (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
) => FlatPageLayoutWidgetValidationError[];
};
@@ -0,0 +1,10 @@
import { type MessageDescriptor } from '@lingui/core';
import { type PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export type FlatPageLayoutWidgetValidationError = {
code: PageLayoutWidgetExceptionCode;
message: string;
userFriendlyMessage?: MessageDescriptor;
value?: unknown;
};
@@ -3,7 +3,7 @@ import { v4 } from 'uuid';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
export type FromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreateArgs =
{
@@ -23,6 +23,10 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
['pageLayoutTabId'],
);
validateWidgetConfigurationInput({
configuration: createPageLayoutWidgetInput.configuration,
});
const createdAt = new Date().toISOString();
const pageLayoutWidgetId = v4();
@@ -35,7 +39,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
deletedAt: null,
universalIdentifier: pageLayoutWidgetId,
title: createPageLayoutWidgetInput.title,
type: createPageLayoutWidgetInput.type ?? WidgetType.VIEW,
type: createPageLayoutWidgetInput.type,
objectMetadataId: createPageLayoutWidgetInput.objectMetadataId ?? null,
gridPosition: createPageLayoutWidgetInput.gridPosition,
configuration: createPageLayoutWidgetInput.configuration,
@@ -12,6 +12,7 @@ import {
PageLayoutWidgetException,
PageLayoutWidgetExceptionCode,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export type UpdatePageLayoutWidgetInputWithId = {
@@ -47,6 +48,17 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
);
if (
Object.prototype.hasOwnProperty.call(
updatedEditableFieldProperties,
'configuration',
)
) {
validateWidgetConfigurationInput({
configuration: updatedEditableFieldProperties.configuration,
});
}
return mergeUpdateInExistingRecord({
existing: existingFlatPageLayoutWidgetToUpdate,
properties: FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
@@ -0,0 +1,9 @@
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
export const VALID_GRAPH_CONFIGURATION_TYPES = [
WidgetConfigurationType.AGGREGATE_CHART,
WidgetConfigurationType.BAR_CHART,
WidgetConfigurationType.LINE_CHART,
WidgetConfigurationType.PIE_CHART,
WidgetConfigurationType.GAUGE_CHART,
] as const;
@@ -0,0 +1,6 @@
import { type GraphConfiguration } from './graph-configuration.type';
export type BaseGraphConfiguration = Pick<
GraphConfiguration,
'configurationType' | 'aggregateFieldMetadataId' | 'aggregateOperation'
>;
@@ -0,0 +1,12 @@
import { type AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto';
import { type BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
import { type GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto';
import { type LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
import { type PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
export type GraphConfiguration =
| BarChartConfigurationDTO
| LineChartConfigurationDTO
| PieChartConfigurationDTO
| AggregateChartConfigurationDTO
| GaugeChartConfigurationDTO;
@@ -0,0 +1,6 @@
import { type WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
export type IframeConfiguration = {
configurationType?: WidgetConfigurationType;
url?: string;
};
@@ -0,0 +1,22 @@
import { type msg } from '@lingui/core/macro';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const rejectWidgetType = (
widgetType: WidgetType,
message: string,
userFriendlyMessage: ReturnType<typeof msg>,
) => {
return (): FlatPageLayoutWidgetValidationError[] => {
return [
{
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message,
value: widgetType,
userFriendlyMessage,
},
];
};
};
@@ -0,0 +1,31 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateBarChartConfiguration = (
configuration: BarChartConfigurationDTO,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.primaryAxisGroupByFieldMetadataId)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Primary axis group by field is required for bar chart widget "${widgetTitle}"`,
userFriendlyMessage: msg`Primary axis group by field is required for bar chart`,
});
}
if (!isDefined(configuration.layout)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Layout is required for bar chart widget "${widgetTitle}"`,
userFriendlyMessage: msg`Layout is required for bar chart`,
});
}
return errors;
};
@@ -0,0 +1,31 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type BaseGraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/base-graph-configuration.type';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateBaseGraphFields = (
configuration: BaseGraphConfiguration,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.aggregateFieldMetadataId)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Aggregate field metadata ID is required for graph widget "${widgetTitle}"`,
userFriendlyMessage: msg`Aggregate field is required for graph widget`,
});
}
if (!isDefined(configuration.aggregateOperation)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Aggregate operation is required for graph widget "${widgetTitle}"`,
userFriendlyMessage: msg`Aggregate operation is required for graph widget`,
});
}
return errors;
};
@@ -0,0 +1,27 @@
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
import { validateBarChartConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-bar-chart-configuration.util';
import { validateLineChartConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-line-chart-configuration.util';
import { validatePieChartConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-pie-chart-configuration.util';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
export const validateGraphConfigurationByType = (
configuration: GraphConfiguration,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const configurationType = configuration.configurationType;
switch (configurationType) {
case WidgetConfigurationType.BAR_CHART:
return validateBarChartConfiguration(configuration, widgetTitle);
case WidgetConfigurationType.LINE_CHART:
return validateLineChartConfiguration(configuration, widgetTitle);
case WidgetConfigurationType.PIE_CHART:
return validatePieChartConfiguration(configuration, widgetTitle);
case WidgetConfigurationType.AGGREGATE_CHART:
case WidgetConfigurationType.GAUGE_CHART:
return [];
default:
return [];
}
};
@@ -0,0 +1,44 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { VALID_GRAPH_CONFIGURATION_TYPES } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/constants/valid-graph-configuration-types.constant';
import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateGraphConfigurationType = (
configuration: GraphConfiguration,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.configurationType)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Configuration type is required for widget "${widgetTitle}"`,
userFriendlyMessage: msg`Configuration type is required`,
});
return errors;
}
const isValidGraphConfigurationType =
VALID_GRAPH_CONFIGURATION_TYPES.includes(configuration.configurationType);
if (!isValidGraphConfigurationType) {
const expectedConfigurationTypes = VALID_GRAPH_CONFIGURATION_TYPES.map(
(type) => type.toString(),
).join(', ');
const configurationTypeString = configuration.configurationType.toString();
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Invalid configuration type for graph widget "${widgetTitle}". Expected one of ${expectedConfigurationTypes}, got ${configurationTypeString}`,
userFriendlyMessage: msg`Invalid configuration type for graph widget`,
value: configuration.configurationType,
});
}
return errors;
};
@@ -0,0 +1,54 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
import { validateBaseGraphFields } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util';
import { validateGraphConfigurationByType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util';
import { validateGraphConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateGraphFlatPageLayoutWidgetForCreation = (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
): FlatPageLayoutWidgetValidationError[] => {
const { flatEntityToValidate } = args;
const { configuration, title } = flatEntityToValidate;
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Configuration is required for graph widget "${title}"`,
userFriendlyMessage: msg`Configuration is required for graph widget`,
});
return errors;
}
const graphConfiguration = configuration as GraphConfiguration;
const configurationTypeErrors = validateGraphConfigurationType(
graphConfiguration,
title,
);
errors.push(...configurationTypeErrors);
if (configurationTypeErrors.length > 0) {
return errors;
}
const baseFieldErrors = validateBaseGraphFields(graphConfiguration, title);
errors.push(...baseFieldErrors);
const typeSpecificErrors = validateGraphConfigurationByType(
graphConfiguration,
title,
);
errors.push(...typeSpecificErrors);
return errors;
};
@@ -0,0 +1,46 @@
import { isDefined } from 'twenty-shared/utils';
import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type GraphConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/graph-configuration.type';
import { validateBaseGraphFields } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-base-graph-fields.util';
import { validateGraphConfigurationByType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-by-type.util';
import { validateGraphConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-graph-configuration-type.util';
export const validateGraphFlatPageLayoutWidgetForUpdate = (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
): FlatPageLayoutWidgetValidationError[] => {
const { flatEntityToValidate } = args;
const { configuration, title } = flatEntityToValidate;
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration)) {
return [];
}
const graphConfiguration = configuration as GraphConfiguration;
const configurationTypeErrors = validateGraphConfigurationType(
graphConfiguration,
title,
);
errors.push(...configurationTypeErrors);
if (configurationTypeErrors.length > 0) {
return errors;
}
const baseFieldErrors = validateBaseGraphFields(graphConfiguration, title);
errors.push(...baseFieldErrors);
const typeSpecificErrors = validateGraphConfigurationByType(
graphConfiguration,
title,
);
errors.push(...typeSpecificErrors);
return errors;
};
@@ -0,0 +1,37 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateIframeConfigurationType = (
configuration: IframeConfiguration,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.configurationType)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Configuration type is required for widget "${widgetTitle}"`,
userFriendlyMessage: msg`Configuration type is required`,
});
return errors;
}
if (configuration.configurationType !== WidgetConfigurationType.IFRAME) {
const configurationTypeString = configuration.configurationType.toString();
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Invalid configuration type for iframe widget "${widgetTitle}". Expected IFRAME, got ${configurationTypeString}`,
userFriendlyMessage: msg`Invalid configuration type for iframe widget`,
value: configuration.configurationType,
});
}
return errors;
};
@@ -0,0 +1,42 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
import { validateIframeConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util';
import { validateIframeUrl } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateIframeFlatPageLayoutWidgetForCreation = (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
): FlatPageLayoutWidgetValidationError[] => {
const { flatEntityToValidate } = args;
const { configuration, title } = flatEntityToValidate;
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Configuration is required for iframe widget "${title}"`,
userFriendlyMessage: msg`Configuration is required for iframe widget`,
});
return errors;
}
const iframeConfiguration = configuration as IframeConfiguration;
const configurationTypeErrors = validateIframeConfigurationType(
iframeConfiguration,
title,
);
errors.push(...configurationTypeErrors);
const urlErrors = validateIframeUrl(iframeConfiguration, title);
errors.push(...urlErrors);
return errors;
};
@@ -0,0 +1,34 @@
import { isDefined } from 'twenty-shared/utils';
import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
import { validateIframeConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-configuration-type.util';
import { validateIframeUrl } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-url.util';
export const validateIframeFlatPageLayoutWidgetForUpdate = (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
): FlatPageLayoutWidgetValidationError[] => {
const { flatEntityToValidate } = args;
const { configuration, title } = flatEntityToValidate;
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration)) {
return [];
}
const iframeConfiguration = configuration as IframeConfiguration;
const configurationTypeErrors = validateIframeConfigurationType(
iframeConfiguration,
title,
);
errors.push(...configurationTypeErrors);
const urlErrors = validateIframeUrl(iframeConfiguration, title);
errors.push(...urlErrors);
return errors;
};
@@ -0,0 +1,24 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type IframeConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/types/iframe-configuration.type';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateIframeUrl = (
configuration: IframeConfiguration,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (isDefined(configuration.url) && typeof configuration.url !== 'string') {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`URL must be a string for widget "${widgetTitle}"`,
userFriendlyMessage: msg`URL must be a string`,
value: configuration.url,
});
}
return errors;
};
@@ -0,0 +1,23 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type LineChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateLineChartConfiguration = (
configuration: LineChartConfigurationDTO,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.primaryAxisGroupByFieldMetadataId)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Primary axis group by field is required for line chart widget "${widgetTitle}"`,
userFriendlyMessage: msg`Primary axis group by field is required for line chart`,
});
}
return errors;
};
@@ -0,0 +1,23 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validatePieChartConfiguration = (
configuration: PieChartConfigurationDTO,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.groupByFieldMetadataId)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Group by field is required for pie chart widget "${widgetTitle}"`,
userFriendlyMessage: msg`Group by field is required for pie chart`,
});
}
return errors;
};
@@ -0,0 +1,37 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateStandaloneRichTextBody = (
configuration: StandaloneRichTextConfigurationDTO,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.body)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Body is required for standalone rich text widget "${widgetTitle}"`,
userFriendlyMessage: msg`Body is required for standalone rich text widget`,
});
return errors;
}
if (
typeof configuration.body !== 'object' ||
Array.isArray(configuration.body)
) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Body must be an object for widget "${widgetTitle}"`,
userFriendlyMessage: msg`Body must be an object`,
value: configuration.body,
});
}
return errors;
};
@@ -0,0 +1,43 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { type 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 { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateStandaloneRichTextConfigurationType = (
configuration: StandaloneRichTextConfigurationDTO,
widgetTitle: string,
): FlatPageLayoutWidgetValidationError[] => {
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration.configurationType)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Configuration type is required for widget "${widgetTitle}"`,
userFriendlyMessage: msg`Configuration type is required`,
});
return errors;
}
if (
configuration.configurationType !==
WidgetConfigurationType.STANDALONE_RICH_TEXT
) {
const expectedConfigurationType =
WidgetConfigurationType.STANDALONE_RICH_TEXT;
const configurationType = configuration.configurationType;
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Invalid configuration type for standalone rich text widget "${widgetTitle}". Expected ${expectedConfigurationType}, got ${configurationType}`,
userFriendlyMessage: msg`Invalid configuration type for standalone rich text widget`,
value: configuration.configurationType,
});
}
return errors;
};
@@ -0,0 +1,46 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { validateStandaloneRichTextBody } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util';
import { validateStandaloneRichTextConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util';
import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateStandaloneRichTextFlatPageLayoutWidgetForCreation = (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForCreationArgs,
): FlatPageLayoutWidgetValidationError[] => {
const { flatEntityToValidate } = args;
const { configuration, title } = flatEntityToValidate;
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration)) {
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Configuration is required for standalone rich text widget "${title}"`,
userFriendlyMessage: msg`Configuration is required for standalone rich text widget`,
});
return errors;
}
const standaloneRichTextConfiguration =
configuration as StandaloneRichTextConfigurationDTO;
const configurationTypeErrors = validateStandaloneRichTextConfigurationType(
standaloneRichTextConfiguration,
title,
);
errors.push(...configurationTypeErrors);
const bodyErrors = validateStandaloneRichTextBody(
standaloneRichTextConfiguration,
title,
);
errors.push(...bodyErrors);
return errors;
};
@@ -0,0 +1,38 @@
import { isDefined } from 'twenty-shared/utils';
import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
import { validateStandaloneRichTextBody } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-body.util';
import { validateStandaloneRichTextConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-configuration-type.util';
import { type StandaloneRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/standalone-rich-text-configuration.dto';
export const validateStandaloneRichTextFlatPageLayoutWidgetForUpdate = (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
): FlatPageLayoutWidgetValidationError[] => {
const { flatEntityToValidate } = args;
const { configuration, title } = flatEntityToValidate;
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration)) {
return [];
}
const standaloneRichTextConfiguration =
configuration as StandaloneRichTextConfigurationDTO;
const configurationTypeErrors = validateStandaloneRichTextConfigurationType(
standaloneRichTextConfiguration,
title,
);
errors.push(...configurationTypeErrors);
const bodyErrors = validateStandaloneRichTextBody(
standaloneRichTextConfiguration,
title,
);
errors.push(...bodyErrors);
return errors;
};
@@ -31,7 +31,6 @@ export class CreatePageLayoutWidgetInput {
@Field(() => WidgetType, { nullable: false })
@IsEnum(WidgetType)
@IsOptional()
type: WidgetType;
@Field(() => UUIDScalarType, { nullable: true })
@@ -3,8 +3,6 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
@@ -20,16 +18,13 @@ import {
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import {
PageLayoutWidgetException,
PageLayoutWidgetExceptionCode,
PageLayoutWidgetExceptionMessageKey,
generatePageLayoutWidgetExceptionMessage,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
import { validateAndTransformWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-and-transform-widget-configuration.util';
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@@ -43,7 +38,6 @@ type WidgetMigrationOperations = {
@Injectable()
export class PageLayoutWidgetService {
constructor(
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
@@ -63,56 +57,6 @@ export class PageLayoutWidgetService {
return flatPageLayoutWidgetMaps;
}
private async validateWidgetConfigurationOrThrow({
type,
configuration,
workspaceId,
titleForError,
}: {
type: WidgetType;
configuration: AllPageLayoutWidgetConfiguration;
workspaceId: string;
titleForError: string;
}): Promise<AllPageLayoutWidgetConfiguration> {
const isDashboardV2Enabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_DASHBOARD_V2_ENABLED,
workspaceId,
);
let validatedConfig: AllPageLayoutWidgetConfiguration | null = null;
try {
validatedConfig = await validateAndTransformWidgetConfiguration({
type,
configuration,
isDashboardV2Enabled,
});
} catch (error) {
throw new PageLayoutWidgetException(
generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
titleForError,
type,
error instanceof Error ? error.message : String(error),
),
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
);
}
if (!isDefined(validatedConfig)) {
throw new PageLayoutWidgetException(
generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
titleForError,
type,
),
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
);
}
return validatedConfig;
}
private async validateAndRunWidgetMigration({
workspaceId,
operations,
@@ -189,18 +133,6 @@ export class PageLayoutWidgetService {
createPageLayoutWidgetInput: CreatePageLayoutWidgetInput,
workspaceId: string,
): Promise<PageLayoutWidgetDTO> {
this.validateCreateInput(createPageLayoutWidgetInput);
validateWidgetGridPosition(
createPageLayoutWidgetInput.gridPosition,
createPageLayoutWidgetInput.title,
);
const validatedConfig = await this.getValidatedConfigurationForCreate(
createPageLayoutWidgetInput,
workspaceId,
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
@@ -208,10 +140,7 @@ export class PageLayoutWidgetService {
const flatPageLayoutWidgetToCreate =
fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate({
createPageLayoutWidgetInput: {
...createPageLayoutWidgetInput,
configuration: validatedConfig,
},
createPageLayoutWidgetInput,
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
});
@@ -237,47 +166,6 @@ export class PageLayoutWidgetService {
);
}
private validateCreateInput(input: CreatePageLayoutWidgetInput): void {
if (!isDefined(input.title)) {
throw new PageLayoutWidgetException(
generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.TITLE_REQUIRED,
),
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
);
}
if (!isDefined(input.pageLayoutTabId)) {
throw new PageLayoutWidgetException(
generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_ID_REQUIRED,
),
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
);
}
if (!isDefined(input.gridPosition)) {
throw new PageLayoutWidgetException(
generatePageLayoutWidgetExceptionMessage(
PageLayoutWidgetExceptionMessageKey.GRID_POSITION_REQUIRED,
),
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
);
}
}
private async getValidatedConfigurationForCreate(
input: CreatePageLayoutWidgetInput,
workspaceId: string,
): Promise<AllPageLayoutWidgetConfiguration> {
return await this.validateWidgetConfigurationOrThrow({
type: input.type,
configuration: input.configuration,
workspaceId,
titleForError: input.title,
});
}
async update(
id: string,
workspaceId: string,
@@ -297,21 +185,10 @@ export class PageLayoutWidgetService {
validateWidgetGridPosition(updateData.gridPosition, titleForValidation);
}
const validatedConfig = await this.getValidatedConfigurationForUpdate(
updateData,
existingWidget,
workspaceId,
);
const updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId = {
id,
update: {
...updateData,
...(isDefined(validatedConfig)
? {
configuration: validatedConfig,
}
: {}),
},
};
@@ -361,26 +238,6 @@ export class PageLayoutWidgetService {
return existingWidget;
}
private async getValidatedConfigurationForUpdate(
updateData: UpdatePageLayoutWidgetInput,
existingWidget: FlatPageLayoutWidget,
workspaceId: string,
): Promise<AllPageLayoutWidgetConfiguration | undefined> {
if (!isDefined(updateData.configuration)) {
return undefined;
}
const typeForValidation = updateData.type ?? existingWidget.type;
const titleForError = updateData.title ?? existingWidget.title;
return await this.validateWidgetConfigurationOrThrow({
type: typeForValidation,
configuration: updateData.configuration,
workspaceId,
titleForError,
});
}
async delete(id: string, workspaceId: string): Promise<PageLayoutWidgetDTO> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -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();
});
});
});
@@ -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/);
});
});
});
@@ -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);
});
});
});
@@ -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;
}
};
@@ -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,
});
};
@@ -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,
);
}
};
@@ -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;
};