Add front component widget (#17440)

Modified the data model and introduced a seed to introduce front
components widgets.
This commit is contained in:
Raphaël Bosi
2026-01-26 13:45:58 +01:00
committed by GitHub
parent e0d4492013
commit 7e7a535af0
36 changed files with 596 additions and 190 deletions
@@ -10,6 +10,8 @@ import {
} 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 { validateFrontComponentFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-front-component-flat-page-layout-widget-for-creation.util';
import { validateFrontComponentFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-front-component-flat-page-layout-widget-for-update.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';
@@ -58,6 +60,7 @@ export class FlatPageLayoutWidgetTypeValidatorService {
GRAPH: validateGraphFlatPageLayoutWidgetForCreation,
STANDALONE_RICH_TEXT:
validateStandaloneRichTextFlatPageLayoutWidgetForCreation,
FRONT_COMPONENT: validateFrontComponentFlatPageLayoutWidgetForCreation,
TIMELINE: rejectWidgetType(
WidgetType.TIMELINE,
'Widget type TIMELINE is not supported yet.',
@@ -131,6 +134,7 @@ export class FlatPageLayoutWidgetTypeValidatorService {
GRAPH: validateGraphFlatPageLayoutWidgetForUpdate,
STANDALONE_RICH_TEXT:
validateStandaloneRichTextFlatPageLayoutWidgetForUpdate,
FRONT_COMPONENT: validateFrontComponentFlatPageLayoutWidgetForUpdate,
TIMELINE: rejectWidgetType(
WidgetType.TIMELINE,
'Widget type TIMELINE is not supported yet.',
@@ -0,0 +1,39 @@
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 FrontComponentConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/front-component-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 validateFrontComponentConfigurationType = (
configuration: FrontComponentConfigurationDTO,
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.FRONT_COMPONENT
) {
const configurationTypeString = String(configuration.configurationType);
errors.push({
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`Invalid configuration type for front component widget "${widgetTitle}". Expected FRONT_COMPONENT, got ${configurationTypeString}`,
userFriendlyMessage: msg`Invalid configuration type for front component widget`,
value: configuration.configurationType,
});
}
return errors;
};
@@ -0,0 +1,38 @@
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 { validateFrontComponentConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-front-component-configuration-type.util';
import { type FrontComponentConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/front-component-configuration.dto';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const validateFrontComponentFlatPageLayoutWidgetForCreation = (
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 front component widget "${title}"`,
userFriendlyMessage: msg`Configuration is required for front component widget`,
});
return errors;
}
const frontComponentConfiguration =
configuration as FrontComponentConfigurationDTO;
const configurationTypeErrors = validateFrontComponentConfigurationType(
frontComponentConfiguration,
title,
);
errors.push(...configurationTypeErrors);
return errors;
};
@@ -0,0 +1,30 @@
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 { validateFrontComponentConfigurationType } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-front-component-configuration-type.util';
import { type FrontComponentConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/front-component-configuration.dto';
export const validateFrontComponentFlatPageLayoutWidgetForUpdate = (
args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs,
): FlatPageLayoutWidgetValidationError[] => {
const { flatEntityToValidate } = args;
const { configuration, title } = flatEntityToValidate;
const errors: FlatPageLayoutWidgetValidationError[] = [];
if (!isDefined(configuration)) {
return [];
}
const frontComponentConfiguration =
configuration as FrontComponentConfigurationDTO;
const configurationTypeErrors = validateFrontComponentConfigurationType(
frontComponentConfiguration,
title,
);
errors.push(...configurationTypeErrors);
return errors;
};
@@ -0,0 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';
@ObjectType('FrontComponentCode')
export class FrontComponentCodeDTO {
@IsString()
@IsNotEmpty()
@Field()
sourceCode: string;
}
@@ -10,6 +10,7 @@ import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
import { FrontComponentCodeDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component-code.dto';
import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
import { UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
@@ -42,6 +43,18 @@ export class FrontComponentResolver {
return await this.frontComponentService.findById(id, workspace.id);
}
@Query(() => FrontComponentCodeDTO)
@UseGuards(NoPermissionGuard)
async getFrontComponentCode(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<FrontComponentCodeDTO> {
return await this.frontComponentService.getFrontComponentCode(
id,
workspace.id,
);
}
@Mutation(() => FrontComponentDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async createFrontComponent(
@@ -11,6 +11,7 @@ import { fromDeleteFrontComponentInputToFlatFrontComponentOrThrow } from 'src/en
import { fromFlatFrontComponentToFrontComponentDto } from 'src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util';
import { fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow } from 'src/engine/metadata-modules/flat-front-component/utils/from-update-front-component-input-to-flat-front-component-to-update-or-throw.util';
import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
import { type FrontComponentCodeDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component-code.dto';
import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
import { type UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
import {
@@ -234,4 +235,14 @@ export class FrontComponentService {
return frontComponent;
}
async getFrontComponentCode(
_id: string,
_workspaceId: string,
): Promise<FrontComponentCodeDTO> {
// TODO: get the source code from the front component here, we will implement a mock version for now
const sourceCode = '';
return { sourceCode };
}
}
@@ -6,6 +6,7 @@ import { FieldConfigurationDTO } from 'src/engine/metadata-modules/page-layout-w
import { FieldRichTextConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/field-rich-text-configuration.dto';
import { FieldsConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/fields-configuration.dto';
import { FilesConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/files-configuration.dto';
import { FrontComponentConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/front-component-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';
@@ -31,6 +32,7 @@ export const ALL_WIDGET_CONFIGURATION_TYPE_VALIDATOR_BY_WIDGET_CONFIGURATION_TYP
GAUGE_CHART: GaugeChartConfigurationDTO,
BAR_CHART: BarChartConfigurationDTO,
CALENDAR: CalendarConfigurationDTO,
FRONT_COMPONENT: FrontComponentConfigurationDTO,
EMAILS: EmailsConfigurationDTO,
FIELD: FieldConfigurationDTO,
FIELD_RICH_TEXT: FieldRichTextConfigurationDTO,
@@ -0,0 +1,22 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsIn, IsNotEmpty, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutWidgetConfigurationBase } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configurationt-base.type';
@ObjectType('FrontComponentConfiguration')
export class FrontComponentConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
@Field(() => WidgetConfigurationType)
@IsIn([WidgetConfigurationType.FRONT_COMPONENT])
@IsNotEmpty()
configurationType: WidgetConfigurationType.FRONT_COMPONENT;
@Field(() => UUIDScalarType)
@IsNotEmpty()
@IsUUID()
frontComponentId: string;
}
@@ -25,6 +25,7 @@ export enum WidgetConfigurationType {
WORKFLOW = 'WORKFLOW',
WORKFLOW_VERSION = 'WORKFLOW_VERSION',
WORKFLOW_RUN = 'WORKFLOW_RUN',
FRONT_COMPONENT = 'FRONT_COMPONENT',
}
registerEnumType(WidgetConfigurationType, {
@@ -15,4 +15,5 @@ export enum WidgetType {
WORKFLOW = 'WORKFLOW',
WORKFLOW_VERSION = 'WORKFLOW_VERSION',
WORKFLOW_RUN = 'WORKFLOW_RUN',
FRONT_COMPONENT = 'FRONT_COMPONENT',
}
@@ -2,6 +2,7 @@ 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 { FrontComponentConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/front-component-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';
@@ -113,6 +114,12 @@ export const validateWidgetConfigurationInput = ({
configuration,
);
break;
case WidgetConfigurationType.FRONT_COMPONENT:
errors = validateWidgetConfigurationByDto(
FrontComponentConfigurationDTO,
configuration,
);
break;
case WidgetConfigurationType.VIEW:
throw new PageLayoutWidgetException(
'View configuration is not supported yet',