[Fix] fix timelineActivities on notes and tasks (#17814)
[Fixes sentry](https://twenty-v7.sentry.io/issues/7238708876/?environment=prod&environment=prod-eu&project=4507072499810304&query=Field%20metadata%20for%20field&referrer=issue-stream&sort=date): _Field metadata for field "targetTargetPersonId" is missing in object metadata timelineActivity_ Regression caused by migration of note and task targets - fields were renamed from personId to targetPersonId, impacting the event names, while we still expected them in their previous shape. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
This commit is contained in:
@@ -1503,7 +1503,6 @@ export enum FeatureFlagKey {
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
}
|
||||
|
||||
|
||||
@@ -1475,7 +1475,6 @@ export enum FeatureFlagKey {
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
}
|
||||
|
||||
|
||||
+1
-12
@@ -1,28 +1,17 @@
|
||||
import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/useLinkedObjectsTitle';
|
||||
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
// do we need to test this?
|
||||
export const useTimelineActivities = (
|
||||
targetableObject: ActivityTargetableObject,
|
||||
) => {
|
||||
const isTimelineActivityMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
);
|
||||
|
||||
const targetableObjectFieldIdName = isTimelineActivityMigrated
|
||||
? `target${capitalize(targetableObject.targetObjectNameSingular)}Id`
|
||||
: getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
const targetableObjectFieldIdName = `target${capitalize(targetableObject.targetObjectNameSingular)}Id`;
|
||||
|
||||
const { objectMetadataItem: timelineActivityMetadata } =
|
||||
useObjectMetadataItem({
|
||||
|
||||
-1
@@ -10,7 +10,6 @@ export enum FeatureFlagKey {
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
|
||||
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
|
||||
+1
-4
@@ -90,10 +90,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
const isFeatureFlagEnabled =
|
||||
(objectMetadataNameSingular === 'timelineActivity' &&
|
||||
existingFeatureFlagsMap[
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED
|
||||
]) ||
|
||||
objectMetadataNameSingular === 'timelineActivity' ||
|
||||
(objectMetadataNameSingular === 'attachment' &&
|
||||
existingFeatureFlagsMap[FeatureFlagKey.IS_ATTACHMENT_MIGRATED]) ||
|
||||
(objectMetadataNameSingular === 'noteTarget' &&
|
||||
|
||||
-5
@@ -56,11 +56,6 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
|
||||
-8
@@ -5,7 +5,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
@@ -75,17 +74,10 @@ export class CalendarEventParticipantListener {
|
||||
.filter(isDefined);
|
||||
});
|
||||
|
||||
const isFeatureFlagTimelineActivityMigrated =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
batchEvent.workspaceId,
|
||||
);
|
||||
|
||||
await this.timelineActivityRepository.upsertTimelineActivities({
|
||||
objectSingularName: 'person',
|
||||
workspaceId: batchEvent.workspaceId,
|
||||
payloads: timelineActivityPayloads.filter(isDefined),
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-8
@@ -5,7 +5,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
@@ -74,17 +73,10 @@ export class MessageParticipantListener {
|
||||
.filter(isDefined);
|
||||
});
|
||||
|
||||
const isFeatureFlagTimelineActivityMigrated =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
batchEvent.workspaceId,
|
||||
);
|
||||
|
||||
await this.timelineActivityRepository.upsertTimelineActivities({
|
||||
objectSingularName: 'person',
|
||||
workspaceId: batchEvent.workspaceId,
|
||||
payloads: timelineActivityPayloads.filter(isDefined),
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+5
-25
@@ -16,7 +16,6 @@ type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = {
|
||||
})[];
|
||||
workspaceId: string;
|
||||
objectSingularName: string;
|
||||
isFeatureFlagTimelineActivityMigrated: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -29,7 +28,6 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
@@ -38,7 +36,6 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
|
||||
const payloadsWithDiff = payloads
|
||||
@@ -61,10 +58,7 @@ export class TimelineActivityRepository {
|
||||
[];
|
||||
|
||||
const timelineActivityPropertyName =
|
||||
await this.getTimelineActivityPropertyName(
|
||||
objectSingularName,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
);
|
||||
await this.getTimelineActivityPropertyName(objectSingularName);
|
||||
|
||||
for (const payload of payloadsWithDiff) {
|
||||
const recentTimelineActivity = recentTimelineActivities.find(
|
||||
@@ -98,7 +92,6 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
payloads: payloadsToInsert,
|
||||
workspaceId,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
}, authContext);
|
||||
}
|
||||
@@ -107,7 +100,6 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
|
||||
const timelineActivityTypeORMRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
@@ -121,10 +113,7 @@ export class TimelineActivityRepository {
|
||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||
|
||||
const timelineActivityPropertyName =
|
||||
await this.getTimelineActivityPropertyName(
|
||||
objectSingularName,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
);
|
||||
await this.getTimelineActivityPropertyName(objectSingularName);
|
||||
|
||||
const whereConditions: Record<string, unknown> = {
|
||||
[timelineActivityPropertyName]: In(
|
||||
@@ -148,7 +137,6 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
|
||||
if (payloads.length === 0) {
|
||||
return;
|
||||
@@ -164,10 +152,7 @@ export class TimelineActivityRepository {
|
||||
);
|
||||
|
||||
const timelineActivityPropertyName =
|
||||
await this.getTimelineActivityPropertyName(
|
||||
objectSingularName,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
);
|
||||
await this.getTimelineActivityPropertyName(objectSingularName);
|
||||
|
||||
return timelineActivityTypeORMRepository.insert(
|
||||
payloads.map((payload) => ({
|
||||
@@ -208,12 +193,7 @@ export class TimelineActivityRepository {
|
||||
});
|
||||
}
|
||||
|
||||
private async getTimelineActivityPropertyName(
|
||||
objectSingularName: string,
|
||||
isFeatureFlagTimelineActivityMigrated: boolean,
|
||||
) {
|
||||
return isFeatureFlagTimelineActivityMigrated
|
||||
? `${buildTimelineActivityRelatedMorphFieldMetadataName(objectSingularName)}Id`
|
||||
: `${objectSingularName}Id`;
|
||||
private async getTimelineActivityPropertyName(objectSingularName: string) {
|
||||
return `${buildTimelineActivityRelatedMorphFieldMetadataName(objectSingularName)}Id`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecordBaseEvent } from 'twenty-shared/database-events';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
import { type ObjectRecordBaseEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
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 { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
@@ -19,6 +18,7 @@ import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.work
|
||||
import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { type TimelineActivityPayload } from 'src/modules/timeline/types/timeline-activity-payload';
|
||||
import { extractObjectSingularNameFromTargetColumnName } from 'src/modules/timeline/utils/extract-object-singular-name-from-target-column-name.util';
|
||||
|
||||
type ActivityType = 'note' | 'task';
|
||||
|
||||
@@ -67,7 +67,7 @@ export class TimelineActivityService {
|
||||
const payloadsByObjectSingularName = timelineActivitiesPayloads.reduce(
|
||||
(acc, payload) => {
|
||||
const computedObjectSingularName =
|
||||
payload.overrideObjectSingularName ?? objectSingularName;
|
||||
payload.objectSingularName ?? objectSingularName;
|
||||
|
||||
acc[computedObjectSingularName] = [
|
||||
...(acc[computedObjectSingularName] || []),
|
||||
@@ -79,18 +79,11 @@ export class TimelineActivityService {
|
||||
{} as Record<string, TimelineActivityPayload[]>,
|
||||
);
|
||||
|
||||
const isFeatureFlagTimelineActivityMigrated =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const objectSingularName in payloadsByObjectSingularName) {
|
||||
await this.timelineActivityRepository.upsertTimelineActivities({
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads: payloadsByObjectSingularName[objectSingularName],
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -255,7 +248,7 @@ export class TimelineActivityService {
|
||||
linkedRecordId: activityId,
|
||||
linkedObjectMetadataId: objectMetadata.id,
|
||||
properties: event.properties,
|
||||
overrideObjectSingularName: objectMetadata.nameSingular,
|
||||
objectSingularName: objectMetadata.nameSingular,
|
||||
} satisfies TimelineActivityPayload;
|
||||
});
|
||||
})
|
||||
@@ -361,9 +354,12 @@ export class TimelineActivityService {
|
||||
targetColumnName
|
||||
];
|
||||
|
||||
const objectSingularName =
|
||||
extractObjectSingularNameFromTargetColumnName(targetColumnName);
|
||||
|
||||
return {
|
||||
name: `linked-${activityType}.${action}`,
|
||||
overrideObjectSingularName: targetColumnName.replace(/Id$/, ''),
|
||||
objectSingularName,
|
||||
recordId,
|
||||
linkedRecordCachedName: activity.title,
|
||||
linkedRecordId: activity.id,
|
||||
|
||||
@@ -8,5 +8,5 @@ export type TimelineActivityPayload = {
|
||||
workspaceMemberId?: string;
|
||||
name: string;
|
||||
recordId: string;
|
||||
overrideObjectSingularName?: string;
|
||||
objectSingularName?: string;
|
||||
};
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { extractObjectSingularNameFromTargetColumnName } from 'src/modules/timeline/utils/extract-object-singular-name-from-target-column-name.util';
|
||||
|
||||
describe('extractObjectSingularNameFromTargetColumnName', () => {
|
||||
it('should extract "person" from "targetPersonId"', () => {
|
||||
expect(
|
||||
extractObjectSingularNameFromTargetColumnName('targetPersonId'),
|
||||
).toBe('person');
|
||||
});
|
||||
|
||||
it('should extract "arrowTarget" from "targetArrowTargetId"', () => {
|
||||
expect(
|
||||
extractObjectSingularNameFromTargetColumnName('targetArrowTargetId'),
|
||||
).toBe('arrowTarget');
|
||||
});
|
||||
|
||||
it('should extract "idCard" from "targetIdCardId"', () => {
|
||||
expect(
|
||||
extractObjectSingularNameFromTargetColumnName('targetIdCardId'),
|
||||
).toBe('idCard');
|
||||
});
|
||||
|
||||
it('should extract "cardId" from "targetCardIdId"', () => {
|
||||
expect(
|
||||
extractObjectSingularNameFromTargetColumnName('targetCardIdId'),
|
||||
).toBe('cardId');
|
||||
});
|
||||
});
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
const ID_SUFFIX = 'Id';
|
||||
const TARGET_PREFIX = 'target';
|
||||
|
||||
export const extractObjectSingularNameFromTargetColumnName = (
|
||||
targetColumnName: string,
|
||||
): string => {
|
||||
let result = targetColumnName;
|
||||
|
||||
if (result.endsWith(ID_SUFFIX)) {
|
||||
result = result.slice(0, -ID_SUFFIX.length);
|
||||
}
|
||||
|
||||
if (
|
||||
result.startsWith(TARGET_PREFIX) &&
|
||||
result.length > TARGET_PREFIX.length
|
||||
) {
|
||||
result =
|
||||
result.charAt(TARGET_PREFIX.length).toLowerCase() +
|
||||
result.slice(TARGET_PREFIX.length + 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user