From 7d69000ab73f9ccefa426a562afc9948ebdecd12 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 29 Jan 2026 14:39:46 +0100 Subject: [PATCH] Update pageLayout* data models for backend recordPageLayout refactor (#17446) Screenshot 2026-01-26 at 16 09 27 --- .../src/generated-metadata/graphql.ts | 10 ++++ .../twenty-front/src/generated/graphql.ts | 10 ++++ .../page-layout/types/PageLayoutTab.ts | 7 ++- .../page-layout/utils/transformPageLayout.ts | 13 +++-- ...382-updatePageLayoutForRecordPageLayout.ts | 47 +++++++++++++++++++ .../all-metadata-relations.constant.ts | 5 ++ ...quired-metadata-for-validation.constant.ts | 1 + ...ata-names-children-first.util.spec.ts.snap | 2 +- ...-to-flat-page-layout-tab-to-create.util.ts | 3 ++ ...tab-entity-to-flat-page-layout-tab.util.ts | 2 + ...-flat-page-layout-widget-to-create.util.ts | 1 + ...-entity-to-flat-page-layout-widget.util.ts | 2 + ...pace-flat-page-layout-map-cache.service.ts | 3 ++ ...nput-to-flat-page-layout-to-create.util.ts | 1 + ...-layout-entity-to-flat-page-layout.util.ts | 21 +++++++++ .../dtos/page-layout-tab.dto.ts | 16 ++++++- .../entities/page-layout-tab.entity.ts | 12 +++++ .../dtos/page-layout-widget.dto.ts | 5 ++ .../entities/page-layout-widget.entity.ts | 4 ++ .../page-layout/dtos/page-layout.dto.ts | 4 ++ .../entities/page-layout.entity.ts | 10 ++++ .../services/page-layout-update.service.ts | 4 ++ ...lat-page-layout-to-page-layout-dto.util.ts | 3 ++ ...dard-page-layout-tab-flat-metadata.util.ts | 3 ++ ...d-page-layout-widget-flat-metadata.util.ts | 1 + ...standard-page-layout-flat-metadata.util.ts | 1 + ...ce-migration-build-orchestrator.service.ts | 2 + ...nb-properties-by-metadata-name.constant.ts | 1 + packages/twenty-shared/src/types/index.ts | 2 + .../page-layout/PageLayoutTabLayoutMode.ts | 5 ++ .../PageLayoutWidgetConditionalDisplay.ts | 2 + 31 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1769679579382-updatePageLayoutForRecordPageLayout.ts create mode 100644 packages/twenty-shared/src/types/page-layout/PageLayoutTabLayoutMode.ts create mode 100644 packages/twenty-shared/src/types/page-layout/PageLayoutWidgetConditionalDisplay.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index b16647e030..f0ac9a2e42 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -3355,6 +3355,7 @@ export type PageInfo = { export type PageLayout = { __typename?: 'PageLayout'; createdAt: Scalars['DateTime']; + defaultTabToFocusOnMobileAndSidePanelId?: Maybe; deletedAt?: Maybe; id: Scalars['UUID']; name: Scalars['String']; @@ -3368,7 +3369,9 @@ export type PageLayoutTab = { __typename?: 'PageLayoutTab'; createdAt: Scalars['DateTime']; deletedAt?: Maybe; + icon?: Maybe; id: Scalars['UUID']; + layoutMode?: Maybe; pageLayoutId: Scalars['UUID']; position: Scalars['Float']; title: Scalars['String']; @@ -3376,6 +3379,12 @@ export type PageLayoutTab = { widgets?: Maybe>; }; +export enum PageLayoutTabLayoutMode { + CANVAS = 'CANVAS', + GRID = 'GRID', + VERTICAL_LIST = 'VERTICAL_LIST' +} + export enum PageLayoutType { DASHBOARD = 'DASHBOARD', RECORD_INDEX = 'RECORD_INDEX', @@ -3384,6 +3393,7 @@ export enum PageLayoutType { export type PageLayoutWidget = { __typename?: 'PageLayoutWidget'; + conditionalDisplay?: Maybe; configuration: WidgetConfiguration; createdAt: Scalars['DateTime']; deletedAt?: Maybe; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 800d4f223f..5e742a0c14 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -3237,6 +3237,7 @@ export type PageInfo = { export type PageLayout = { __typename?: 'PageLayout'; createdAt: Scalars['DateTime']; + defaultTabToFocusOnMobileAndSidePanelId?: Maybe; deletedAt?: Maybe; id: Scalars['UUID']; name: Scalars['String']; @@ -3250,7 +3251,9 @@ export type PageLayoutTab = { __typename?: 'PageLayoutTab'; createdAt: Scalars['DateTime']; deletedAt?: Maybe; + icon?: Maybe; id: Scalars['UUID']; + layoutMode?: Maybe; pageLayoutId: Scalars['UUID']; position: Scalars['Float']; title: Scalars['String']; @@ -3258,6 +3261,12 @@ export type PageLayoutTab = { widgets?: Maybe>; }; +export enum PageLayoutTabLayoutMode { + CANVAS = 'CANVAS', + GRID = 'GRID', + VERTICAL_LIST = 'VERTICAL_LIST' +} + export enum PageLayoutType { DASHBOARD = 'DASHBOARD', RECORD_INDEX = 'RECORD_INDEX', @@ -3266,6 +3275,7 @@ export enum PageLayoutType { export type PageLayoutWidget = { __typename?: 'PageLayoutWidget'; + conditionalDisplay?: Maybe; configuration: WidgetConfiguration; createdAt: Scalars['DateTime']; deletedAt?: Maybe; diff --git a/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts index a8d36d321a..0da4fe3100 100644 --- a/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts @@ -2,11 +2,14 @@ import { type PageLayoutTabLayoutMode } from '@/page-layout/types/PageLayoutTabL import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { type PageLayoutTab as PageLayoutTabGenerated } from '~/generated/graphql'; -export type PageLayoutTab = Omit & { +export type PageLayoutTab = Omit< + PageLayoutTabGenerated, + 'widgets' | 'layoutMode' +> & { widgets: PageLayoutWidget[]; /** * Only available behind IS_RECORD_PAGE_LAYOUT_ENABLED for now. */ layoutMode?: PageLayoutTabLayoutMode; - icon?: string; + icon?: string | null; }; diff --git a/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts b/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts index 7402ce0d48..1c775eaae6 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts @@ -9,11 +9,14 @@ export const transformPageLayout = ( ...pageLayout, tabs: (pageLayout.tabs ?? []) .toSorted((a, b) => a.position - b.position) - .map( - (tab): PageLayoutTab => ({ - ...tab, + .map((tab): PageLayoutTab => { + // TODO: remove this once the frontend consumes the new type + const { layoutMode: _layoutMode, ...tabWithoutLayoutMode } = tab; + + return { + ...tabWithoutLayoutMode, widgets: tab.widgets ?? [], - }), - ), + }; + }), }; }; diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1769679579382-updatePageLayoutForRecordPageLayout.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1769679579382-updatePageLayoutForRecordPageLayout.ts new file mode 100644 index 0000000000..b7c3e86c7a --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1769679579382-updatePageLayoutForRecordPageLayout.ts @@ -0,0 +1,47 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class UpdatePageLayoutForRecordPageLayout1769679579382 + implements MigrationInterface +{ + name = 'UpdatePageLayoutForRecordPageLayout1769679579382'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."pageLayout" ADD "defaultTabToFocusOnMobileAndSidePanelId" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutTab" ADD "icon" character varying`, + ); + await queryRunner.query( + `CREATE TYPE "core"."pageLayoutTab_layoutmode_enum" AS ENUM('GRID', 'VERTICAL_LIST', 'CANVAS')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutTab" ADD "layoutMode" "core"."pageLayoutTab_layoutmode_enum" NOT NULL DEFAULT 'GRID'`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ADD "conditionalDisplay" jsonb`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayout" ADD CONSTRAINT "FK_747fbc25827bdcb9e35cc68a990" FOREIGN KEY ("defaultTabToFocusOnMobileAndSidePanelId") REFERENCES "core"."pageLayoutTab"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."pageLayout" DROP CONSTRAINT "FK_747fbc25827bdcb9e35cc68a990"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" DROP COLUMN "conditionalDisplay"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutTab" DROP COLUMN "layoutMode"`, + ); + await queryRunner.query(`DROP TYPE "core"."pageLayoutTab_layoutmode_enum"`); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutTab" DROP COLUMN "icon"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayout" DROP COLUMN "defaultTabToFocusOnMobileAndSidePanelId"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts index e9da7d71a3..8c1947f601 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts @@ -306,6 +306,11 @@ export const ALL_METADATA_RELATIONS = { foreignKey: 'objectMetadataId', }, application: null, + defaultTabToFocusOnMobileAndSidePanel: { + metadataName: 'pageLayoutTab', + flatEntityForeignKeyAggregator: null, + foreignKey: 'defaultTabToFocusOnMobileAndSidePanelId', + }, }, oneToMany: { tabs: { metadataName: 'pageLayoutTab' }, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts index d79ea7c349..0015d12f8d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts @@ -62,6 +62,7 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { }, pageLayout: { objectMetadata: true, + pageLayoutTab: true, }, pageLayoutTab: { pageLayout: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap index 13d360add5..fd80c8f27f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap @@ -9,13 +9,13 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with "viewField", "commandMenuItem", "index", + "pageLayout", "roleTarget", "rowLevelPermissionPredicateGroup", "viewGroup", "agent", "frontComponent", "logicFunction", - "pageLayout", "pageLayoutTab", "skill", "view", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts index db0767d6e8..6962fe6c9b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts @@ -1,3 +1,4 @@ +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; import { v4 } from 'uuid'; @@ -36,5 +37,7 @@ export const fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate = ({ universalIdentifier: pageLayoutTabId, applicationId: workspaceCustomApplicationId, widgetIds: [], + icon: null, + layoutMode: PageLayoutTabLayoutMode.GRID, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts index 9a0edabb09..5877bf2b5b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts @@ -46,6 +46,8 @@ export const transformPageLayoutTabEntityToFlatPageLayoutTab = ({ universalIdentifier: pageLayoutTabEntity.universalIdentifier, applicationId: pageLayoutTabEntity.applicationId, widgetIds: pageLayoutTabEntity.widgets.map((widget) => widget.id), + icon: pageLayoutTabEntity.icon, + layoutMode: pageLayoutTabEntity.layoutMode, __universal: { universalIdentifier: pageLayoutTabEntity.universalIdentifier, applicationUniversalIdentifier, 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 406402d594..51f1cc3950 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 @@ -44,5 +44,6 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ gridPosition: createPageLayoutWidgetInput.gridPosition, configuration: createPageLayoutWidgetInput.configuration, applicationId: workspaceCustomApplicationId, + conditionalDisplay: null, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts index 3ff09b0e18..21c95e751f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts @@ -87,6 +87,8 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ objectMetadataUniversalIdentifier, gridPosition: pageLayoutWidgetEntityWithoutRelations.gridPosition, configuration: configurationWithUniversalIdentifiers, + conditionalDisplay: + pageLayoutWidgetEntityWithoutRelations.conditionalDisplay, }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts index e1063f1d7b..e43e68fc90 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts @@ -70,6 +70,8 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid createIdToUniversalIdentifierMap(applications); const objectMetadataIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(objectMetadatas); + const pageLayoutTabIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(pageLayoutTabs); const flatPageLayoutMaps = createEmptyFlatEntityMaps(); @@ -81,6 +83,7 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid }, applicationIdToUniversalIdentifierMap, objectMetadataIdToUniversalIdentifierMap, + pageLayoutTabIdToUniversalIdentifierMap, }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util.ts index 1630725e56..321dd83279 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util.ts @@ -37,5 +37,6 @@ export const fromCreatePageLayoutInputToFlatPageLayoutToCreate = ({ universalIdentifier: pageLayoutId, applicationId: workspaceCustomApplicationId, tabIds: [], + defaultTabToFocusOnMobileAndSidePanelId: null, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts index d0c01e8580..88b8d4e462 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts @@ -11,6 +11,7 @@ export const transformPageLayoutEntityToFlatPageLayout = ({ entity: pageLayoutEntity, applicationIdToUniversalIdentifierMap, objectMetadataIdToUniversalIdentifierMap, + pageLayoutTabIdToUniversalIdentifierMap, }: FromEntityToFlatEntityArgs<'pageLayout'>): FlatPageLayout => { const applicationUniversalIdentifier = applicationIdToUniversalIdentifierMap.get(pageLayoutEntity.applicationId); @@ -38,6 +39,23 @@ export const transformPageLayoutEntityToFlatPageLayout = ({ } } + let defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: string | null = + null; + + if (isDefined(pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId)) { + defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier = + pageLayoutTabIdToUniversalIdentifierMap.get( + pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId, + ) ?? null; + + if (!isDefined(defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier)) { + throw new FlatEntityMapsException( + `PageLayoutTab with id ${pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId} not found for pageLayout ${pageLayoutEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + return { createdAt: pageLayoutEntity.createdAt.toISOString(), deletedAt: pageLayoutEntity.deletedAt?.toISOString() ?? null, @@ -50,6 +68,8 @@ export const transformPageLayoutEntityToFlatPageLayout = ({ universalIdentifier: pageLayoutEntity.universalIdentifier, applicationId: pageLayoutEntity.applicationId, tabIds: pageLayoutEntity.tabs.map((tab) => tab.id), + defaultTabToFocusOnMobileAndSidePanelId: + pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId, __universal: { universalIdentifier: pageLayoutEntity.universalIdentifier, applicationUniversalIdentifier, @@ -57,6 +77,7 @@ export const transformPageLayoutEntityToFlatPageLayout = ({ tabUniversalIdentifiers: pageLayoutEntity.tabs.map( (tab) => tab.universalIdentifier, ), + defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier, }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto.ts index 8393f4fb9d..af8ffd6ff2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto.ts @@ -1,10 +1,15 @@ -import { Field, Float, ObjectType } from '@nestjs/graphql'; +import { Field, Float, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto'; +registerEnumType(PageLayoutTabLayoutMode, { + name: 'PageLayoutTabLayoutMode', +}); + @ObjectType('PageLayoutTab') export class PageLayoutTabDTO { @IDField(() => UUIDScalarType) @@ -22,6 +27,15 @@ export class PageLayoutTabDTO { @Field(() => [PageLayoutWidgetDTO], { nullable: true }) widgets?: PageLayoutWidgetDTO[] | null; + @Field(() => String, { nullable: true }) + icon?: string | null; + + @Field(() => PageLayoutTabLayoutMode, { + nullable: true, // Not nullable in the database, but we need to make it nullable here until the frontend consumes the new type + defaultValue: PageLayoutTabLayoutMode.GRID, + }) + layoutMode: PageLayoutTabLayoutMode; + @Field() createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity.ts index 8ac3c0f41c..90d9d4c5df 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity.ts @@ -1,5 +1,6 @@ import { ObjectType } from '@nestjs/graphql'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; import { Column, CreateDateColumn, @@ -52,6 +53,17 @@ export class PageLayoutTabEntity }) widgets: Relation; + @Column({ nullable: true, type: 'varchar' }) + icon: string | null; + + @Column({ + type: 'enum', + enum: Object.values(PageLayoutTabLayoutMode), + nullable: false, + default: PageLayoutTabLayoutMode.GRID, + }) + layoutMode: PageLayoutTabLayoutMode; + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; 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 db394a0ba8..112fc80426 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 @@ -1,6 +1,8 @@ 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 { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface'; @@ -47,6 +49,9 @@ export class PageLayoutWidgetDTO { @Field(() => WidgetConfiguration, { nullable: false }) configuration: AllPageLayoutWidgetConfiguration; + @Field(() => GraphQLJSON, { nullable: true }) + conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null; + @Field() createdAt: Date; 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 344e815050..4ba83efb3d 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,5 +1,6 @@ import { ObjectType } from '@nestjs/graphql'; +import { PageLayoutWidgetConditionalDisplay } from 'twenty-shared/types'; import { Column, CreateDateColumn, @@ -70,6 +71,9 @@ export class PageLayoutWidgetEntity< @JoinColumn({ name: 'objectMetadataId' }) objectMetadata: Relation | null; + @Column({ type: 'jsonb', nullable: true }) + conditionalDisplay: JsonbProperty; + @Column({ type: 'jsonb', nullable: false }) gridPosition: JsonbProperty; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/dtos/page-layout.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/dtos/page-layout.dto.ts index 1ab47473df..6d65af9047 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/dtos/page-layout.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/dtos/page-layout.dto.ts @@ -1,6 +1,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { SerializedRelation } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto'; @@ -28,6 +29,9 @@ export class PageLayoutDTO { @Field(() => [PageLayoutTabDTO], { nullable: true }) tabs?: PageLayoutTabDTO[] | null; + @Field(() => UUIDScalarType, { nullable: true }) + defaultTabToFocusOnMobileAndSidePanelId?: SerializedRelation; + @Field() createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/entities/page-layout.entity.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/entities/page-layout.entity.ts index a82eae73aa..baf2410dc7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/entities/page-layout.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/entities/page-layout.entity.ts @@ -59,6 +59,16 @@ export class PageLayoutEntity }) tabs: Relation; + @Column({ nullable: true, type: 'uuid' }) + defaultTabToFocusOnMobileAndSidePanelId: string | null; + + @ManyToOne(() => PageLayoutTabEntity, { + onDelete: 'SET NULL', + nullable: true, + }) + @JoinColumn({ name: 'defaultTabToFocusOnMobileAndSidePanelId' }) + defaultTabToFocusOnMobileAndSidePanel: Relation | null; + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; 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 689df7d662..6d951200f5 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 @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; @@ -236,6 +237,8 @@ export class PageLayoutUpdateService { universalIdentifier: tabId, applicationId: workspaceCustomApplicationId, widgetIds: [], + icon: null, + layoutMode: PageLayoutTabLayoutMode.GRID, }; }, ); @@ -392,6 +395,7 @@ export class PageLayoutUpdateService { deletedAt: null, universalIdentifier: widgetId, applicationId: workspaceCustomApplicationId, + conditionalDisplay: null, }; }, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util.ts index 3c7670c983..adb2e1156d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util.ts @@ -9,6 +9,7 @@ export const fromFlatPageLayoutToPageLayoutDto = ( updatedAt, deletedAt, tabIds: _tabIds, + defaultTabToFocusOnMobileAndSidePanelId, ...rest } = flatPageLayout; @@ -17,5 +18,7 @@ export const fromFlatPageLayoutToPageLayoutDto = ( createdAt: new Date(createdAt), updatedAt: new Date(updatedAt), deletedAt: deletedAt ? new Date(deletedAt) : null, + defaultTabToFocusOnMobileAndSidePanelId: + defaultTabToFocusOnMobileAndSidePanelId ?? undefined, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts index 6ae4ac3aa6..ce5a61e1a8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts @@ -1,4 +1,5 @@ import { isDefined } from 'class-validator'; +import { PageLayoutTabLayoutMode } from 'twenty-shared/types'; import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant'; @@ -64,5 +65,7 @@ export const createStandardPageLayoutTabFlatMetadata = < createdAt: now, updatedAt: now, deletedAt: null, + icon: null, + layoutMode: PageLayoutTabLayoutMode.GRID, }; }; 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 ff626d1249..ae9f12a9b5 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 @@ -87,5 +87,6 @@ export const createStandardPageLayoutWidgetFlatMetadata = < createdAt: now, updatedAt: now, deletedAt: null, + conditionalDisplay: null, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout/create-standard-page-layout-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout/create-standard-page-layout-flat-metadata.util.ts index 1f050c97f6..63ea1ab1bd 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout/create-standard-page-layout-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout/create-standard-page-layout-flat-metadata.util.ts @@ -41,5 +41,6 @@ export const createStandardPageLayoutFlatMetadata = ({ createdAt: now, updatedAt: now, deletedAt: null, + defaultTabToFocusOnMobileAndSidePanelId: null, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index cb4be06e33..e25404d15d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -798,6 +798,8 @@ export class WorkspaceMigrationBuildOrchestratorService { dependencyOptimisticFlatEntityMaps: { flatObjectMetadataMaps: optimisticAllFlatEntityMaps.flatObjectMetadataMaps, + flatPageLayoutTabMaps: + optimisticAllFlatEntityMaps.flatPageLayoutTabMaps, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts index 1e722aeb25..35dfc3215a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts @@ -42,6 +42,7 @@ export const ALL_JSONB_PROPERTIES_BY_METADATA_NAME = { pageLayoutWidget: { gridPosition: 'gridPosition', configuration: 'configuration', + conditionalDisplay: 'conditionalDisplay', }, commandMenuItem: {}, navigationMenuItem: {}, diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index ccabe115f0..f70471c980 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -129,6 +129,8 @@ export { ObjectRecordGroupByDateGranularity } from './ObjectRecordGroupByDateGra export type { ObjectsPermissions } from './ObjectsPermissions'; export type { ObjectsPermissionsByRoleId } from './ObjectsPermissionsByRoleId'; export type { OrderBy } from './OrderBy'; +export { PageLayoutTabLayoutMode } from './page-layout/PageLayoutTabLayoutMode'; +export type { PageLayoutWidgetConditionalDisplay } from './page-layout/PageLayoutWidgetConditionalDisplay'; export type { PartialFieldMetadataItem } from './PartialFieldMetadataItem'; export type { PartialFieldMetadataItemOption } from './PartialFieldMetadataOption'; export type { QueryCursorDirection } from './QueryCursorDirection'; diff --git a/packages/twenty-shared/src/types/page-layout/PageLayoutTabLayoutMode.ts b/packages/twenty-shared/src/types/page-layout/PageLayoutTabLayoutMode.ts new file mode 100644 index 0000000000..b9d52e14fc --- /dev/null +++ b/packages/twenty-shared/src/types/page-layout/PageLayoutTabLayoutMode.ts @@ -0,0 +1,5 @@ +export enum PageLayoutTabLayoutMode { + GRID = 'GRID', + VERTICAL_LIST = 'VERTICAL_LIST', + CANVAS = 'CANVAS', +} diff --git a/packages/twenty-shared/src/types/page-layout/PageLayoutWidgetConditionalDisplay.ts b/packages/twenty-shared/src/types/page-layout/PageLayoutWidgetConditionalDisplay.ts new file mode 100644 index 0000000000..b5e853d8aa --- /dev/null +++ b/packages/twenty-shared/src/types/page-layout/PageLayoutWidgetConditionalDisplay.ts @@ -0,0 +1,2 @@ +// TODO: Loose typing for now. Add proper typing once product is clear. +export type PageLayoutWidgetConditionalDisplay = object;