diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 4956e9dcbf..0b7080642f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1047,6 +1047,7 @@ export type CreatePageLayoutWidgetInput = { gridPosition: GridPositionInput; objectMetadataId?: InputMaybe; pageLayoutTabId: Scalars['UUID']; + position?: InputMaybe; title: Scalars['String']; type: WidgetType; }; @@ -3424,11 +3425,34 @@ export type PageLayoutWidget = { id: Scalars['UUID']; objectMetadataId?: Maybe; pageLayoutTabId: Scalars['UUID']; + position?: Maybe; title: Scalars['String']; type: WidgetType; updatedAt: Scalars['DateTime']; }; +export type PageLayoutWidgetCanvasPosition = { + __typename?: 'PageLayoutWidgetCanvasPosition'; + layoutMode: PageLayoutTabLayoutMode; +}; + +export type PageLayoutWidgetGridPosition = { + __typename?: 'PageLayoutWidgetGridPosition'; + column: Scalars['Int']; + columnSpan: Scalars['Int']; + layoutMode: PageLayoutTabLayoutMode; + row: Scalars['Int']; + rowSpan: Scalars['Int']; +}; + +export type PageLayoutWidgetPosition = PageLayoutWidgetCanvasPosition | PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition; + +export type PageLayoutWidgetVerticalListPosition = { + __typename?: 'PageLayoutWidgetVerticalListPosition'; + index: Scalars['Int']; + layoutMode: PageLayoutTabLayoutMode; +}; + export type PermissionFlag = { __typename?: 'PermissionFlag'; flag: PermissionFlagType; @@ -4713,6 +4737,7 @@ export type UpdatePageLayoutWidgetInput = { configuration?: InputMaybe; gridPosition?: InputMaybe; objectMetadataId?: InputMaybe; + position?: InputMaybe; title?: InputMaybe; type?: InputMaybe; }; @@ -4723,6 +4748,7 @@ export type UpdatePageLayoutWidgetWithIdInput = { id: Scalars['UUID']; objectMetadataId?: InputMaybe; pageLayoutTabId: Scalars['UUID']; + position?: InputMaybe; title: Scalars['String']; type: WidgetType; }; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 6e3cade8cc..74edf7979a 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1028,6 +1028,7 @@ export type CreatePageLayoutWidgetInput = { gridPosition: GridPositionInput; objectMetadataId?: InputMaybe; pageLayoutTabId: Scalars['UUID']; + position?: InputMaybe; title: Scalars['String']; type: WidgetType; }; @@ -3329,11 +3330,34 @@ export type PageLayoutWidget = { id: Scalars['UUID']; objectMetadataId?: Maybe; pageLayoutTabId: Scalars['UUID']; + position?: Maybe; title: Scalars['String']; type: WidgetType; updatedAt: Scalars['DateTime']; }; +export type PageLayoutWidgetCanvasPosition = { + __typename?: 'PageLayoutWidgetCanvasPosition'; + layoutMode: PageLayoutTabLayoutMode; +}; + +export type PageLayoutWidgetGridPosition = { + __typename?: 'PageLayoutWidgetGridPosition'; + column: Scalars['Int']; + columnSpan: Scalars['Int']; + layoutMode: PageLayoutTabLayoutMode; + row: Scalars['Int']; + rowSpan: Scalars['Int']; +}; + +export type PageLayoutWidgetPosition = PageLayoutWidgetCanvasPosition | PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition; + +export type PageLayoutWidgetVerticalListPosition = { + __typename?: 'PageLayoutWidgetVerticalListPosition'; + index: Scalars['Int']; + layoutMode: PageLayoutTabLayoutMode; +}; + export type PermissionFlag = { __typename?: 'PermissionFlag'; flag: PermissionFlagType; @@ -4539,6 +4563,7 @@ export type UpdatePageLayoutWidgetInput = { configuration?: InputMaybe; gridPosition?: InputMaybe; objectMetadataId?: InputMaybe; + position?: InputMaybe; title?: InputMaybe; type?: InputMaybe; }; @@ -4549,6 +4574,7 @@ export type UpdatePageLayoutWidgetWithIdInput = { id: Scalars['UUID']; objectMetadataId?: InputMaybe; pageLayoutTabId: Scalars['UUID']; + position?: InputMaybe; title: Scalars['String']; type: WidgetType; }; diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1770046227329-add-page-layout-widget-position-column.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1770046227329-add-page-layout-widget-position-column.ts new file mode 100644 index 0000000000..5733b881f8 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1770046227329-add-page-layout-widget-position-column.ts @@ -0,0 +1,19 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddPageLayoutWidgetPositionColumn1770046227329 + implements MigrationInterface +{ + name = 'AddPageLayoutWidgetPositionColumn1770046227329'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ADD "position" jsonb`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" DROP COLUMN "position"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts index e3c5bc0439..ab30a3996b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts @@ -5,5 +5,6 @@ export const FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES = [ 'type', 'objectMetadataId', 'gridPosition', + 'position', 'configuration', ] as const satisfies (keyof FlatPageLayoutWidget)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts index 51f1cc3950..a3d8d06058 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts @@ -42,6 +42,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ type: createPageLayoutWidgetInput.type, objectMetadataId: createPageLayoutWidgetInput.objectMetadataId ?? null, gridPosition: createPageLayoutWidgetInput.gridPosition, + position: createPageLayoutWidgetInput.position ?? null, configuration: createPageLayoutWidgetInput.configuration, applicationId: workspaceCustomApplicationId, conditionalDisplay: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts index 9a7fc18b69..4ab1bfb4ad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input.ts @@ -11,6 +11,7 @@ import { ValidateNested, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { PageLayoutWidgetPosition } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input'; @@ -43,6 +44,11 @@ export class CreatePageLayoutWidgetInput { @Type(() => GridPositionInput) gridPosition: GridPositionInput; + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + position?: PageLayoutWidgetPosition; + @Field(() => GraphQLJSON, { nullable: false }) @IsObject() @IsOptional() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input.ts index e7ee7d4838..37a6a33ea1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input.ts @@ -11,6 +11,7 @@ import { ValidateNested, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { PageLayoutWidgetPosition } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input'; @@ -50,6 +51,11 @@ export class UpdatePageLayoutWidgetWithIdInput { @IsNotEmpty() gridPosition: GridPositionInput; + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + position?: PageLayoutWidgetPosition; + @Field(() => GraphQLJSON, { nullable: true }) @IsObject() @IsOptional() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts index 86463ef0f6..dbe78b3f75 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts @@ -10,6 +10,7 @@ import { ValidateNested, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { PageLayoutWidgetPosition } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input'; @@ -39,6 +40,11 @@ export class UpdatePageLayoutWidgetInput { @IsOptional() gridPosition?: GridPositionInput; + @Field(() => GraphQLJSON, { nullable: true }) + @IsObject() + @IsOptional() + position?: PageLayoutWidgetPosition; + @Field(() => GraphQLJSON, { nullable: true }) @IsObject() @IsOptional() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-canvas-position.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-canvas-position.dto.ts new file mode 100644 index 0000000000..0c1d6eb18e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-canvas-position.dto.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IsIn, IsNotEmpty } from 'class-validator'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; + +@ObjectType('PageLayoutWidgetCanvasPosition') +export class PageLayoutWidgetCanvasPositionDTO { + @Field(() => PageLayoutTabLayoutMode) + @IsIn([PageLayoutTabLayoutMode.CANVAS]) + @IsNotEmpty() + layoutMode: PageLayoutTabLayoutMode.CANVAS; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-grid-position.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-grid-position.dto.ts new file mode 100644 index 0000000000..cc2c3e7863 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-grid-position.dto.ts @@ -0,0 +1,36 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql'; + +import { IsIn, IsInt, IsNotEmpty, Min } from 'class-validator'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; + +@ObjectType('PageLayoutWidgetGridPosition') +export class PageLayoutWidgetGridPositionDTO { + @Field(() => PageLayoutTabLayoutMode) + @IsIn([PageLayoutTabLayoutMode.GRID]) + @IsNotEmpty() + layoutMode: PageLayoutTabLayoutMode.GRID; + + @Field(() => Int) + @IsInt() + @Min(0) + @IsNotEmpty() + row: number; + + @Field(() => Int) + @IsInt() + @Min(0) + @IsNotEmpty() + column: number; + + @Field(() => Int) + @IsInt() + @Min(1) + @IsNotEmpty() + rowSpan: number; + + @Field(() => Int) + @IsInt() + @Min(1) + @IsNotEmpty() + columnSpan: number; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-position.union.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-position.union.ts new file mode 100644 index 0000000000..72602bf03f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-position.union.ts @@ -0,0 +1,29 @@ +import { createUnionType } from '@nestjs/graphql'; + +import { + PageLayoutTabLayoutMode, + type PageLayoutWidgetPosition, +} from 'twenty-shared/types'; + +import { PageLayoutWidgetCanvasPositionDTO } from './page-layout-widget-canvas-position.dto'; +import { PageLayoutWidgetGridPositionDTO } from './page-layout-widget-grid-position.dto'; +import { PageLayoutWidgetVerticalListPositionDTO } from './page-layout-widget-vertical-list-position.dto'; + +export const PageLayoutWidgetPositionUnion = createUnionType({ + name: 'PageLayoutWidgetPosition', + types: () => [ + PageLayoutWidgetGridPositionDTO, + PageLayoutWidgetVerticalListPositionDTO, + PageLayoutWidgetCanvasPositionDTO, + ], + resolveType({ layoutMode }: PageLayoutWidgetPosition) { + switch (layoutMode) { + case PageLayoutTabLayoutMode.GRID: + return PageLayoutWidgetGridPositionDTO; + case PageLayoutTabLayoutMode.VERTICAL_LIST: + return PageLayoutWidgetVerticalListPositionDTO; + case PageLayoutTabLayoutMode.CANVAS: + return PageLayoutWidgetCanvasPositionDTO; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-vertical-list-position.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-vertical-list-position.dto.ts new file mode 100644 index 0000000000..12e679e639 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-vertical-list-position.dto.ts @@ -0,0 +1,18 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql'; + +import { IsIn, IsInt, IsNotEmpty, Min } from 'class-validator'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; + +@ObjectType('PageLayoutWidgetVerticalListPosition') +export class PageLayoutWidgetVerticalListPositionDTO { + @Field(() => PageLayoutTabLayoutMode) + @IsIn([PageLayoutTabLayoutMode.VERTICAL_LIST]) + @IsNotEmpty() + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST; + + @Field(() => Int) + @IsInt() + @Min(0) + @IsNotEmpty() + index: number; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto.ts index 112fc80426..16a02db45e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto.ts @@ -2,9 +2,13 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; import GraphQLJSON from 'graphql-type-json'; -import { PageLayoutWidgetConditionalDisplay } from 'twenty-shared/types'; +import { + PageLayoutWidgetConditionalDisplay, + PageLayoutWidgetPosition, +} from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { PageLayoutWidgetPositionUnion } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-position.union'; import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface'; import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type'; @@ -46,6 +50,9 @@ export class PageLayoutWidgetDTO { @Field(() => GridPositionDTO, { nullable: false }) gridPosition: GridPositionDTO; + @Field(() => PageLayoutWidgetPositionUnion, { nullable: true }) + position?: PageLayoutWidgetPosition | null; + @Field(() => WidgetConfiguration, { nullable: false }) configuration: AllPageLayoutWidgetConfiguration; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts index 4ba83efb3d..c398fa14df 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts @@ -1,6 +1,9 @@ import { ObjectType } from '@nestjs/graphql'; -import { PageLayoutWidgetConditionalDisplay } from 'twenty-shared/types'; +import { + PageLayoutWidgetConditionalDisplay, + PageLayoutWidgetPosition, +} from 'twenty-shared/types'; import { Column, CreateDateColumn, @@ -77,6 +80,9 @@ export class PageLayoutWidgetEntity< @Column({ type: 'jsonb', nullable: false }) gridPosition: JsonbProperty; + @Column({ type: 'jsonb', nullable: true }) + position: JsonbProperty; + @Column({ type: 'jsonb', nullable: false }) configuration: JsonbProperty< PageLayoutWidgetConfigurationTypeSettings diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception.ts index 95188cbfe9..f6bed41599 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception.ts @@ -16,7 +16,7 @@ export enum PageLayoutWidgetExceptionMessageKey { PAGE_LAYOUT_TAB_NOT_FOUND = 'PAGE_LAYOUT_TAB_NOT_FOUND', PAGE_LAYOUT_WIDGET_NOT_DELETED = 'PAGE_LAYOUT_WIDGET_NOT_DELETED', GRID_POSITION_REQUIRED = 'GRID_POSITION_REQUIRED', - INVALID_WIDGET_GRID_POSITION = 'INVALID_WIDGET_GRID_POSITION', + INVALID_WIDGET_POSITION = 'INVALID_WIDGET_POSITION', INVALID_WIDGET_CONFIGURATION = 'INVALID_WIDGET_CONFIGURATION', } @@ -66,15 +66,15 @@ export const generatePageLayoutWidgetExceptionMessage = ( return 'Page layout widget is not deleted and cannot be restored'; case PageLayoutWidgetExceptionMessageKey.GRID_POSITION_REQUIRED: return 'Grid position is required'; - case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION: + case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION: if (widgetTitle && detailedError) { - return `Invalid grid position for widget "${widgetTitle}": ${detailedError}`; + return `Invalid widget position for widget "${widgetTitle}": ${detailedError}`; } if (detailedError) { - return `Invalid grid position: ${detailedError}`; + return `Invalid widget position: ${detailedError}`; } - return 'Invalid widget grid position'; + return 'Invalid widget position'; case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION: if (widgetTitle && widgetType && detailedError) { return `Invalid configuration for widget "${widgetTitle}" of type ${widgetType}: ${detailedError}`; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts index e4e031a310..7a347da834 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts @@ -23,7 +23,6 @@ import { generatePageLayoutWidgetExceptionMessage, } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception'; import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util'; -import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service'; @@ -200,16 +199,7 @@ export class PageLayoutWidgetService { const existingFlatPageLayoutWidgetMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId); - const existingWidget = this.getExistingWidgetOrThrow( - id, - existingFlatPageLayoutWidgetMaps, - ); - - if (updateData.gridPosition) { - const titleForValidation = updateData.title ?? existingWidget.title; - - validateWidgetGridPosition(updateData.gridPosition, titleForValidation); - } + this.getExistingWidgetOrThrow(id, existingFlatPageLayoutWidgetMaps); const updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId = { id, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-grid-position.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-grid-position.util.ts new file mode 100644 index 0000000000..405c5ff60d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-grid-position.util.ts @@ -0,0 +1,74 @@ +import { msg } from '@lingui/core/macro'; +import { type PageLayoutWidgetGridPosition } from 'twenty-shared/types'; + +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 { + 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/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; + +export const validatePageLayoutWidgetGridPosition = ( + position: PageLayoutWidgetGridPosition, + widgetTitle: string, +): FlatEntityValidationError[] => { + const errors: FlatEntityValidationError[] = []; + + const { row, column, rowSpan, columnSpan } = position; + + if (column >= WIDGET_GRID_MAX_COLUMNS) { + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, + widgetTitle, + undefined, + `column ${column} exceeds grid width (max column is ${WIDGET_GRID_MAX_COLUMNS - 1})`, + ), + userFriendlyMessage: msg`Widget extends beyond grid width`, + }); + } + + if (column + columnSpan > WIDGET_GRID_MAX_COLUMNS) { + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, + widgetTitle, + undefined, + `widget extends beyond grid width (column ${column} + columnSpan ${columnSpan} > ${WIDGET_GRID_MAX_COLUMNS})`, + ), + userFriendlyMessage: msg`Widget extends beyond grid width`, + }); + } + + if (row >= WIDGET_GRID_MAX_ROWS) { + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, + widgetTitle, + undefined, + `row ${row} exceeds maximum allowed rows (${WIDGET_GRID_MAX_ROWS})`, + ), + userFriendlyMessage: msg`Widget row exceeds grid height`, + }); + } + + if (row + rowSpan > WIDGET_GRID_MAX_ROWS) { + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, + widgetTitle, + undefined, + `widget extends beyond grid height (row ${row} + rowSpan ${rowSpan} > ${WIDGET_GRID_MAX_ROWS})`, + ), + userFriendlyMessage: msg`Widget extends beyond grid height`, + }); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-vertical-list-position.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-vertical-list-position.util.ts new file mode 100644 index 0000000000..0dab0b7156 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-vertical-list-position.util.ts @@ -0,0 +1,44 @@ +import { msg } from '@lingui/core/macro'; +import { type PageLayoutWidgetVerticalListPosition } from 'twenty-shared/types'; + +import { + 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/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; + +export const validatePageLayoutWidgetVerticalListPosition = ( + position: PageLayoutWidgetVerticalListPosition, + widgetTitle: string, +): FlatEntityValidationError[] => { + const errors: FlatEntityValidationError[] = []; + + if (position.index < 0) { + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, + widgetTitle, + undefined, + `index ${position.index} must be a non-negative integer`, + ), + userFriendlyMessage: msg`Widget index must be a non-negative integer`, + }); + } + + if (!Number.isInteger(position.index)) { + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: generatePageLayoutWidgetExceptionMessage( + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, + widgetTitle, + undefined, + `index ${position.index} must be an integer`, + ), + userFriendlyMessage: msg`Widget index must be an integer`, + }); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts index 47b5d65937..2106a71f04 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util.ts @@ -16,6 +16,7 @@ type GridPosition = { columnSpan: number; }; +// TODO: remove in favor of validatePageLayoutWidgetGridPosition once gridPosition is deprecated export const validateWidgetGridPosition = ( gridPosition: GridPosition, widgetTitle: string, @@ -28,7 +29,7 @@ export const validateWidgetGridPosition = ( errors.push({ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, message: generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION, + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, widgetTitle, undefined, `column ${column} exceeds grid width (max column is ${WIDGET_GRID_MAX_COLUMNS - 1})`, @@ -41,7 +42,7 @@ export const validateWidgetGridPosition = ( errors.push({ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, message: generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION, + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, widgetTitle, undefined, `widget extends beyond grid width (column ${column} + columnSpan ${columnSpan} > ${WIDGET_GRID_MAX_COLUMNS})`, @@ -54,7 +55,7 @@ export const validateWidgetGridPosition = ( errors.push({ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, message: generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION, + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, widgetTitle, undefined, `row ${row} exceeds maximum allowed rows (${WIDGET_GRID_MAX_ROWS})`, @@ -67,7 +68,7 @@ export const validateWidgetGridPosition = ( errors.push({ code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, message: generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION, + PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION, widgetTitle, undefined, `widget extends beyond grid height (row ${row} + rowSpan ${rowSpan} > ${WIDGET_GRID_MAX_ROWS})`, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts index ee290db8c9..726572463b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts @@ -390,6 +390,7 @@ export class PageLayoutUpdateService { type: widgetInput.type, objectMetadataId: widgetInput.objectMetadataId ?? null, gridPosition: widgetInput.gridPosition, + position: widgetInput.position ?? null, configuration: widgetInput.configuration ?? null, workspaceId, createdAt: now.toISOString(), @@ -416,6 +417,7 @@ export class PageLayoutUpdateService { type: widgetInput.type, objectMetadataId: widgetInput.objectMetadataId ?? null, gridPosition: widgetInput.gridPosition, + position: widgetInput.position ?? null, configuration: widgetInput.configuration ?? null, updatedAt: now.toISOString(), }; @@ -436,6 +438,7 @@ export class PageLayoutUpdateService { type: widgetInput.type, objectMetadataId: widgetInput.objectMetadataId ?? null, gridPosition: widgetInput.gridPosition, + position: widgetInput.position ?? null, configuration: widgetInput.configuration ?? null, deletedAt: null, updatedAt: now.toISOString(), diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template.ts index 635d5d989f..b0bc931b75 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template.ts @@ -1,4 +1,9 @@ -import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; +import { + PageLayoutTabLayoutMode, + type PageLayoutWidgetCanvasPosition, + type PageLayoutWidgetGridPosition, + type PageLayoutWidgetVerticalListPosition, +} from 'twenty-shared/types'; import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type'; @@ -24,6 +29,43 @@ export const GRID_POSITIONS = { }, } as const satisfies Record; +export const GRID_LAYOUT_POSITIONS = { + FULL_WIDTH: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 0, + column: 0, + rowSpan: 12, + columnSpan: 12, + }, + HALF_HEIGHT: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 0, + column: 0, + rowSpan: 6, + columnSpan: 12, + }, + RICH_TEXT: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 12, + column: 0, + rowSpan: 6, + columnSpan: 12, + }, +} as const satisfies Record; + +export const VERTICAL_LIST_LAYOUT_POSITIONS = { + FIRST: { + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, + index: 0, + }, +} as const satisfies Record; + +export const CANVAS_LAYOUT_POSITIONS = { + DEFAULT: { + layoutMode: PageLayoutTabLayoutMode.CANVAS, + }, +} as const satisfies Record; + export const TAB_PROPS = { home: { title: 'Home', @@ -92,55 +134,66 @@ export const WIDGET_PROPS = { title: 'Fields', type: WidgetType.FIELDS, gridPosition: GRID_POSITIONS.FULL_WIDTH, + position: VERTICAL_LIST_LAYOUT_POSITIONS.FIRST, }, timeline: { title: 'Timeline', type: WidgetType.TIMELINE, gridPosition: GRID_POSITIONS.HALF_HEIGHT, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, tasks: { title: 'Tasks', type: WidgetType.TASKS, gridPosition: GRID_POSITIONS.HALF_HEIGHT, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, notes: { title: 'Notes', type: WidgetType.NOTES, gridPosition: GRID_POSITIONS.HALF_HEIGHT, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, files: { title: 'Files', type: WidgetType.FILES, gridPosition: GRID_POSITIONS.HALF_HEIGHT, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, emails: { title: 'Emails', type: WidgetType.EMAILS, gridPosition: GRID_POSITIONS.HALF_HEIGHT, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, calendar: { title: 'Calendar', type: WidgetType.CALENDAR, gridPosition: GRID_POSITIONS.HALF_HEIGHT, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, richText: { title: 'Note', type: WidgetType.FIELD_RICH_TEXT, gridPosition: GRID_POSITIONS.RICH_TEXT, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, workflow: { title: 'Flow', type: WidgetType.WORKFLOW, gridPosition: GRID_POSITIONS.FULL_WIDTH, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, workflowVersion: { title: 'Flow', type: WidgetType.WORKFLOW_VERSION, gridPosition: GRID_POSITIONS.FULL_WIDTH, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, workflowRun: { title: 'Flow', type: WidgetType.WORKFLOW_RUN, gridPosition: GRID_POSITIONS.FULL_WIDTH, + position: CANVAS_LAYOUT_POSITIONS.DEFAULT, }, } as const; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.types.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.types.ts index b124ec8428..3afd5017f2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.types.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.types.ts @@ -1,4 +1,7 @@ -import { type PageLayoutTabLayoutMode } from 'twenty-shared/types'; +import { + type PageLayoutTabLayoutMode, + type PageLayoutWidgetPosition, +} from 'twenty-shared/types'; import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type'; @@ -8,6 +11,7 @@ export type StandardPageLayoutWidgetDefinition = { title?: string; type?: WidgetType; gridPosition?: GridPosition; + position?: PageLayoutWidgetPosition; }; export type StandardPageLayoutTabDefinition = { @@ -24,6 +28,7 @@ export type StandardRecordPageWidgetDefinition = { title: string; type: WidgetType; gridPosition: GridPosition; + position?: PageLayoutWidgetPosition; }; export type StandardRecordPageTabDefinition = { diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-note-page-layout.config.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-note-page-layout.config.ts index 70af7bb007..74a6db8f60 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-note-page-layout.config.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-note-page-layout.config.ts @@ -21,7 +21,10 @@ const NOTE_PAGE_TABS = { }, richText: { universalIdentifier: '20202020-ac04-4004-8004-a0be5a11a412', - ...WIDGET_PROPS.richText, + title: WIDGET_PROPS.richText.title, + type: WIDGET_PROPS.richText.type, + gridPosition: WIDGET_PROPS.richText.gridPosition, + position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 }, }, }, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-page-layout-config.type.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-page-layout-config.type.ts index 8733f5774c..b5f37d2c24 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-page-layout-config.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-page-layout-config.type.ts @@ -1,14 +1,18 @@ -import { type PageLayoutTabLayoutMode } from 'twenty-shared/types'; +import { + type PageLayoutTabLayoutMode, + type PageLayoutWidgetPosition, +} from 'twenty-shared/types'; -import { type PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type'; +import { type PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; export type StandardPageLayoutWidgetConfig = { universalIdentifier: string; title?: string; type?: WidgetType; gridPosition?: GridPosition; + position?: PageLayoutWidgetPosition; }; export type StandardPageLayoutTabConfig = { diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-task-page-layout.config.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-task-page-layout.config.ts index 888e62d1f4..4434dabde3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-task-page-layout.config.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-task-page-layout.config.ts @@ -21,7 +21,10 @@ const TASK_PAGE_TABS = { }, richText: { universalIdentifier: '20202020-ac05-4005-8005-ba5ca11a5512', - ...WIDGET_PROPS.richText, + title: WIDGET_PROPS.richText.title, + type: WIDGET_PROPS.richText.type, + gridPosition: WIDGET_PROPS.richText.gridPosition, + position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 }, }, }, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts index e5bd9c3b42..5937756358 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts @@ -88,6 +88,7 @@ const computeRecordPageWidgets = ({ title: widget.title, type: widget.type, gridPosition: widget.gridPosition, + position: widget.position ?? null, configuration: { configurationType: WidgetConfigurationType.FIELDS, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/compute-my-first-dashboard-widgets.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/compute-my-first-dashboard-widgets.util.ts index 359e748b5b..4c1c4a7871 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/compute-my-first-dashboard-widgets.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/compute-my-first-dashboard-widgets.util.ts @@ -1,4 +1,5 @@ import { CalendarStartDay } from 'twenty-shared/constants'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; import { v4 } from 'uuid'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; @@ -65,6 +66,13 @@ const createWelcomeRichText = ({ title: 'Untitled Rich Text', type: WidgetType.STANDALONE_RICH_TEXT, gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 6 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 0, + column: 0, + rowSpan: 6, + columnSpan: 6, + }, configuration: { configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT as const, @@ -232,6 +240,13 @@ const createDealsByCompany = ({ title: 'Deals by Company', type: WidgetType.GRAPH, gridPosition: { row: 0, column: 6, rowSpan: 6, columnSpan: 6 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 0, + column: 6, + rowSpan: 6, + columnSpan: 6, + }, configuration: { configurationType: WidgetConfigurationType.PIE_CHART, groupByFieldMetadataId: opportunityFields.company.id, @@ -270,6 +285,13 @@ const createPipelineValueByStage = ({ title: 'Pipeline Value by Stage', type: WidgetType.GRAPH, gridPosition: { row: 6, column: 0, rowSpan: 6, columnSpan: 6 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 6, + column: 0, + rowSpan: 6, + columnSpan: 6, + }, configuration: { configurationType: WidgetConfigurationType.BAR_CHART, aggregateFieldMetadataId: opportunityFields.amount.id, @@ -314,6 +336,13 @@ const createRevenueTimeline = ({ title: 'Revenue Timeline', type: WidgetType.GRAPH, gridPosition: { row: 6, column: 6, rowSpan: 6, columnSpan: 6 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 6, + column: 6, + rowSpan: 6, + columnSpan: 6, + }, configuration: { configurationType: WidgetConfigurationType.LINE_CHART, aggregateFieldMetadataId: opportunityFields.amount.id, @@ -353,6 +382,13 @@ const createOpportunitiesByOwner = ({ title: 'Opportunities by Owner', type: WidgetType.GRAPH, gridPosition: { row: 12, column: 0, rowSpan: 6, columnSpan: 6 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 12, + column: 0, + rowSpan: 6, + columnSpan: 6, + }, configuration: { configurationType: WidgetConfigurationType.BAR_CHART, aggregateFieldMetadataId: opportunityFields.id.id, @@ -394,6 +430,13 @@ const createStockMarketIframe = ({ title: 'Stock market (Iframe)', type: WidgetType.IFRAME, gridPosition: { row: 12, column: 6, rowSpan: 8, columnSpan: 6 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 12, + column: 6, + rowSpan: 8, + columnSpan: 6, + }, configuration: { configurationType: WidgetConfigurationType.IFRAME as const, url: 'https://www.tradingview.com/embed-widget/hotlists/?locale=en', @@ -421,6 +464,13 @@ const createDealsCreatedThisMonth = ({ title: 'Deals created this month', type: WidgetType.GRAPH, gridPosition: { row: 18, column: 0, rowSpan: 2, columnSpan: 3 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 18, + column: 0, + rowSpan: 2, + columnSpan: 3, + }, configuration: { configurationType: WidgetConfigurationType.AGGREGATE_CHART, aggregateFieldMetadataId: opportunityFields.id.id, @@ -464,6 +514,13 @@ const createDealValueCreatedThisMonth = ({ title: 'Deal value created this month', type: WidgetType.GRAPH, gridPosition: { row: 18, column: 3, rowSpan: 2, columnSpan: 3 }, + position: { + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 18, + column: 3, + rowSpan: 2, + columnSpan: 3, + }, configuration: { configurationType: WidgetConfigurationType.AGGREGATE_CHART, aggregateFieldMetadataId: opportunityFields.amount.id, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts index 5bfef08850..16f5498d99 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts @@ -1,3 +1,4 @@ +import { type PageLayoutWidgetPosition } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; @@ -19,6 +20,7 @@ export type CreateStandardPageLayoutWidgetContext = { title: string; type: WidgetType; gridPosition: GridPosition; + position: PageLayoutWidgetPosition | null; configuration: AllPageLayoutWidgetConfiguration; objectMetadataId: string | null; }; @@ -40,6 +42,7 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({ title, type, gridPosition, + position, configuration, objectMetadataId, }, @@ -73,6 +76,7 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({ title, type, gridPosition, + position, configuration, objectMetadataId, createdAt: now, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts index 9ee622e53a..98cb55087d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service.ts @@ -2,10 +2,15 @@ import { Injectable } from '@nestjs/common'; import { msg, t } from '@lingui/core/macro'; import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; +import { + PageLayoutTabLayoutMode, + PageLayoutWidgetPosition, +} from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service'; import { PageLayoutTabExceptionCode } from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception'; import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum'; @@ -13,6 +18,8 @@ import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums import { PageLayoutWidgetExceptionCode } 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 { GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type'; +import { validatePageLayoutWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-grid-position.util'; +import { validatePageLayoutWidgetVerticalListPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-vertical-list-position.util'; import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util'; import { FailedFlatEntityValidation, @@ -80,6 +87,12 @@ export class FlatPageLayoutWidgetValidatorService { pageLayoutTabId: updatedFlatPageLayoutWidget.pageLayoutTabId, }; + const referencedPageLayoutTab = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: updatedFlatPageLayoutWidget.pageLayoutTabId, + flatEntityMaps: + optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps, + }); + const gridPositionErrors = this.validateGridPosition({ gridPosition: updatedFlatPageLayoutWidget.gridPosition, widgetTitle: updatedFlatPageLayoutWidget.title, @@ -87,6 +100,14 @@ export class FlatPageLayoutWidgetValidatorService { validationResult.errors.push(...gridPositionErrors); + const positionErrors = this.validatePosition({ + position: updatedFlatPageLayoutWidget.position, + pageLayoutTab: referencedPageLayoutTab, + widgetTitle: updatedFlatPageLayoutWidget.title, + }); + + validationResult.errors.push(...positionErrors); + const featureFlagErrors = this.validateFeatureFlags({ type: updatedFlatPageLayoutWidget.type, configuration: updatedFlatPageLayoutWidget.configuration, @@ -209,6 +230,14 @@ export class FlatPageLayoutWidgetValidatorService { validationResult.errors.push(...gridPositionErrors); + const positionErrors = this.validatePosition({ + position: flatPageLayoutWidgetToValidate.position, + pageLayoutTab: referencedPageLayoutTab, + widgetTitle: flatPageLayoutWidgetToValidate.title, + }); + + validationResult.errors.push(...positionErrors); + const featureFlagErrors = this.validateFeatureFlags({ type: flatPageLayoutWidgetToValidate.type, configuration: flatPageLayoutWidgetToValidate.configuration, @@ -295,4 +324,54 @@ export class FlatPageLayoutWidgetValidatorService { return []; } + + private validatePosition({ + position, + pageLayoutTab, + widgetTitle, + }: { + position: PageLayoutWidgetPosition | null | undefined; + pageLayoutTab: FlatPageLayoutTab | undefined; + widgetTitle: string; + }): FlatEntityValidationError[] { + if (!isDefined(position)) { + return []; + } + + const errors: FlatEntityValidationError[] = []; + + if ( + isDefined(pageLayoutTab) && + position.layoutMode !== pageLayoutTab.layoutMode + ) { + const layoutMode = position.layoutMode; + const tabLayoutMode = pageLayoutTab.layoutMode; + + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: t`Position layoutMode "${layoutMode}" does not match tab layoutMode "${tabLayoutMode}"`, + userFriendlyMessage: msg`Widget position type must match the tab layout mode`, + }); + } + + switch (position.layoutMode) { + case PageLayoutTabLayoutMode.GRID: + errors.push( + ...validatePageLayoutWidgetGridPosition(position, widgetTitle), + ); + break; + case PageLayoutTabLayoutMode.VERTICAL_LIST: + errors.push( + ...validatePageLayoutWidgetVerticalListPosition( + position, + widgetTitle, + ), + ); + break; + case PageLayoutTabLayoutMode.CANVAS: + break; + } + + return errors; + } } diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index 7c8b71415a..3ad653918a 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -131,6 +131,12 @@ export { ObjectRecordGroupByDateGranularity } from './ObjectRecordGroupByDateGra export type { ObjectsPermissions } from './ObjectsPermissions'; export type { ObjectsPermissionsByRoleId } from './ObjectsPermissionsByRoleId'; export type { OrderBy } from './OrderBy'; +export type { + PageLayoutWidgetGridPosition, + PageLayoutWidgetVerticalListPosition, + PageLayoutWidgetCanvasPosition, + PageLayoutWidgetPosition, +} from './page-layout/page-layout-widget-position.type'; export { PageLayoutTabLayoutMode } from './page-layout/PageLayoutTabLayoutMode'; export type { PageLayoutWidgetConditionalDisplay } from './page-layout/PageLayoutWidgetConditionalDisplay'; export type { PartialFieldMetadataItem } from './PartialFieldMetadataItem'; diff --git a/packages/twenty-shared/src/types/page-layout/page-layout-widget-position.type.ts b/packages/twenty-shared/src/types/page-layout/page-layout-widget-position.type.ts new file mode 100644 index 0000000000..0164582715 --- /dev/null +++ b/packages/twenty-shared/src/types/page-layout/page-layout-widget-position.type.ts @@ -0,0 +1,23 @@ +import type { PageLayoutTabLayoutMode } from './PageLayoutTabLayoutMode'; + +export type PageLayoutWidgetGridPosition = { + layoutMode: PageLayoutTabLayoutMode.GRID; + row: number; + column: number; + rowSpan: number; + columnSpan: number; +}; + +export type PageLayoutWidgetVerticalListPosition = { + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST; + index: number; +}; + +export type PageLayoutWidgetCanvasPosition = { + layoutMode: PageLayoutTabLayoutMode.CANVAS; +}; + +export type PageLayoutWidgetPosition = + | PageLayoutWidgetGridPosition + | PageLayoutWidgetVerticalListPosition + | PageLayoutWidgetCanvasPosition;