From c15bcd3bec6f2e1269ddbec0c2a97aaa67d01eb5 Mon Sep 17 00:00:00 2001 From: ehconitin Date: Mon, 1 Jun 2026 20:28:01 +0530 Subject: [PATCH] fix(kanban): exclude position changes from timeline activities --- .../services/timeline-activity.service.ts | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts index 174f1e2b4c0..39ba204439f 100644 --- a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; import { type ObjectRecordBaseEvent } from 'twenty-shared/database-events'; -import { type ObjectRecord } from 'twenty-shared/types'; +import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; @@ -49,9 +50,16 @@ export class TimelineActivityService { const { objectSingularName } = parseEventNameOrThrow(name); + const eventsWithoutPositionDiff = + await this.excludePositionFieldsFromEventsDiff({ + events, + objectMetadata, + workspaceId, + }); + const timelineActivitiesPayloads = await this.transformEventsToTimelineActivityPayloads({ - events, + events: eventsWithoutPositionDiff, objectMetadata, workspaceId, name, @@ -88,6 +96,63 @@ export class TimelineActivityService { } } + // Position changes reach other consumers (SSE, webhooks, workflows) but render + // blank in the timeline, so exclude them to avoid empty activity rows. + private async excludePositionFieldsFromEventsDiff({ + events, + objectMetadata, + workspaceId, + }: { + events: ObjectRecordBaseEvent[]; + objectMetadata: FlatObjectMetadata; + workspaceId: string; + }): Promise { + const someEventHasDiff = events.some((event) => + isDefined(event.properties.diff), + ); + + if (!someEventHasDiff) { + return events; + } + + const { flatFieldMetadataMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFieldMetadataMaps'], + }, + ); + + const positionFieldNames = new Set( + getFlatFieldsFromFlatObjectMetadata(objectMetadata, flatFieldMetadataMaps) + .filter((field) => field.type === FieldMetadataType.POSITION) + .map((field) => field.name), + ); + + if (positionFieldNames.size === 0) { + return events; + } + + return events.map((event) => { + const diff = event.properties.diff; + + if (!isDefined(diff)) { + return event; + } + + const diffWithoutPositionFields = Object.fromEntries( + Object.entries(diff).filter( + ([fieldName]) => !positionFieldNames.has(fieldName), + ), + ); + + return { + ...event, + properties: { ...event.properties, diff: diffWithoutPositionFields }, + }; + }); + } + private async transformEventsToTimelineActivityPayloads({ events, workspaceId,