[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;
};