diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts
index 9d993e2b27..c1c743d974 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts
@@ -4,6 +4,7 @@ import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/us
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
+import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
@@ -34,6 +35,18 @@ export const useTimelineActivities = (
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
});
+ const { objectMetadataItems } = useFilteredObjectMetadataItems();
+
+ const noteObjectMetadataItem = objectMetadataItems.find(
+ (objectMetadataItem) =>
+ objectMetadataItem.nameSingular === CoreObjectNameSingular.Note,
+ );
+
+ const taskObjectMetadataItem = objectMetadataItems.find(
+ (objectMetadataItem) =>
+ objectMetadataItem.nameSingular === CoreObjectNameSingular.Task,
+ );
+
const hasTimelineActivityField = timelineActivityMetadata.fields.some(
(field) =>
isDefined(field.morphRelations) &&
@@ -97,12 +110,24 @@ export const useTimelineActivities = (
objectMetadataItemId: timelineActivityMetadata.id,
});
- const activityIds = timelineActivities
- .filter((timelineActivity) => timelineActivity.name.match(/note|task/i))
+ const noteAndTaskObjectMetadataIds = [
+ noteObjectMetadataItem?.id,
+ taskObjectMetadataItem?.id,
+ ].filter(isDefined);
+
+ // Notes and tasks expose a title that we resolve to label their timeline rows.
+ const noteAndTaskLinkedRecordIds = timelineActivities
+ .filter(
+ (timelineActivity) =>
+ isDefined(timelineActivity.linkedObjectMetadataId) &&
+ noteAndTaskObjectMetadataIds.includes(
+ timelineActivity.linkedObjectMetadataId,
+ ),
+ )
.map((timelineActivity) => timelineActivity.linkedRecordId)
.filter(isDefined);
- useLinkedObjectsTitle(activityIds);
+ useLinkedObjectsTitle(noteAndTaskLinkedRecordIds);
const firstQueryLoading =
loadingTimelineActivities && timelineActivities.length === 0;
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx
index 38408aa09d..0befb301ca 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx
@@ -3,33 +3,23 @@ import { t } from '@lingui/core/macro';
import { type EventRowDynamicComponentProps } from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent.types';
import { EventRowItem } from '@/activities/timeline-activities/rows/components/EventRowItem';
+import {
+ StyledEventRowContainer,
+ StyledEventRowContent,
+ StyledEventRowDate,
+ StyledEventRowLinkedRecord,
+} from '@/activities/timeline-activities/rows/components/EventRowStyles';
import { isTimelineActivityWithLinkedRecord } from '@/activities/timeline-activities/types/TimelineActivity';
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
+import { parseTimelineActivityAction } from 'twenty-shared/timeline';
import { type CoreObjectNameSingular } from 'twenty-shared/types';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isNonEmptyString } from '@sniptt/guards';
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
-import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
type EventRowActivityProps = EventRowDynamicComponentProps;
-const StyledLinkedActivity = styled.span`
- color: ${themeCssVariables.font.color.primary};
- cursor: pointer;
- overflow: hidden;
- text-decoration: underline;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
-`;
-
-const StyledRowContainer = styled.div`
- align-items: center;
- display: flex;
- gap: ${themeCssVariables.spacing[1]};
- justify-content: space-between;
-`;
-
const StyledEventRow = styled.div`
display: flex;
flex-direction: column;
@@ -37,21 +27,6 @@ const StyledEventRow = styled.div`
width: 100%;
`;
-const StyledRow = styled.div`
- align-items: center;
- display: flex;
- gap: ${themeCssVariables.spacing[1]};
- overflow: hidden;
-`;
-
-const StyledItemTitleDate = styled.div`
- @media (max-width: ${MOBILE_VIEWPORT}px) {
- display: none;
- }
- color: ${themeCssVariables.font.color.tertiary};
- padding: 0 ${themeCssVariables.spacing[1]};
-`;
-
export const StyledEventRowItemText = styled.span`
color: ${themeCssVariables.font.color.primary};
`;
@@ -62,9 +37,9 @@ export const EventRowActivity = ({
objectNameSingular,
createdAt,
}: EventRowActivityProps & { objectNameSingular: CoreObjectNameSingular }) => {
- const [eventLinkedObject, eventAction] = event.name.split('.');
+ const eventAction = parseTimelineActivityAction(event.name);
- const eventObject = eventLinkedObject.replace('linked-', '');
+ const eventObject = objectNameSingular;
if (!isTimelineActivityWithLinkedRecord(event)) {
throw new Error('Could not find linked record id for event');
@@ -97,13 +72,13 @@ export const EventRowActivity = ({
return (
-
-
+
+
{authorFullName}
{t`${eventAction} a related ${eventObject}`}
-
openRecordInSidePanel({
recordId: event.linkedRecordId,
@@ -112,10 +87,10 @@ export const EventRowActivity = ({
}
>
-
-
- {createdAt}
-
+
+
+ {createdAt}
+
);
};
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx
index 326f93de2b..fa6631e485 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx
@@ -1,13 +1,27 @@
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
import { ObjectMetadataIcon } from '@/object-metadata/components/ObjectMetadataIcon';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
+import {
+ parseTimelineActivityAction,
+ type TimelineActivityAction,
+} from 'twenty-shared/timeline';
import {
IconCirclePlus,
IconEditCircle,
+ type IconComponent,
IconRestore,
IconTrash,
} from 'twenty-ui/icon';
+const RECORD_CHANGE_ICONS: Partial<
+ Record
+> = {
+ created: IconCirclePlus,
+ updated: IconEditCircle,
+ deleted: IconTrash,
+ restored: IconRestore,
+};
+
export const EventIconDynamicComponent = ({
event,
linkedObjectMetadataItem,
@@ -15,19 +29,12 @@ export const EventIconDynamicComponent = ({
event: TimelineActivity;
linkedObjectMetadataItem: EnrichedObjectMetadataItem | null;
}) => {
- const [, eventAction] = event.name.split('.');
+ const action = parseTimelineActivityAction(event.name);
- if (eventAction === 'created') {
- return ;
- }
- if (eventAction === 'updated') {
- return ;
- }
- if (eventAction === 'deleted') {
- return ;
- }
- if (eventAction === 'restored') {
- return ;
+ const ActionIcon = RECORD_CHANGE_ICONS[action];
+
+ if (ActionIcon) {
+ return ;
}
return ;
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx
index 557058185e..1e84556d3b 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx
@@ -1,72 +1,84 @@
import { EventRowActivity } from '@/activities/timeline-activities/rows/activity/components/EventRowActivity';
import { EventRowCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent';
import { type EventRowDynamicComponentProps } from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent.types';
+import { EventRowGenericLinked } from '@/activities/timeline-activities/rows/generic/components/EventRowGenericLinked';
import { EventRowMainObject } from '@/activities/timeline-activities/rows/main-object/components/EventRowMainObject';
import { EventRowMessage } from '@/activities/timeline-activities/rows/message/components/EventRowMessage';
import { CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
-export const EventRowDynamicComponent = ({
- labelIdentifierValue,
- event,
- mainObjectMetadataItem,
- linkedObjectMetadataItem,
- authorFullName,
- createdAt,
-}: EventRowDynamicComponentProps) => {
- switch (linkedObjectMetadataItem?.nameSingular) {
- case 'calendarEvent':
- return (
-
- );
- case 'message':
+export const EventRowDynamicComponent = (
+ props: EventRowDynamicComponentProps,
+) => {
+ const { linkedObjectMetadataItem } = props;
+
+ if (!isDefined(linkedObjectMetadataItem)) {
+ return (
+
+ );
+ }
+
+ switch (linkedObjectMetadataItem.nameSingular) {
+ case CoreObjectNameSingular.Message:
return (
);
- case 'task':
+ case CoreObjectNameSingular.CalendarEvent:
return (
-
);
- case 'note':
+ case CoreObjectNameSingular.Note:
return (
+ );
+ case CoreObjectNameSingular.Task:
+ return (
+
);
default:
return (
-
);
}
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowStyles.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowStyles.tsx
new file mode 100644
index 0000000000..134fcd4b90
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowStyles.tsx
@@ -0,0 +1,35 @@
+import { styled } from '@linaria/react';
+
+import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
+
+export const StyledEventRowContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ justify-content: space-between;
+`;
+
+export const StyledEventRowContent = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ overflow: hidden;
+`;
+
+export const StyledEventRowDate = styled.div`
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ display: none;
+ }
+ color: ${themeCssVariables.font.color.tertiary};
+ padding: 0 ${themeCssVariables.spacing[1]};
+`;
+
+export const StyledEventRowLinkedRecord = styled.span`
+ color: ${themeCssVariables.font.color.primary};
+ cursor: pointer;
+ overflow: hidden;
+ text-decoration: underline;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+`;
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/generic/components/EventRowGenericLinked.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/generic/components/EventRowGenericLinked.tsx
new file mode 100644
index 0000000000..019d089e93
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/generic/components/EventRowGenericLinked.tsx
@@ -0,0 +1,75 @@
+import { t } from '@lingui/core/macro';
+import { type KeyboardEvent } from 'react';
+
+import { type EventRowDynamicComponentProps } from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent.types';
+import { EventRowItem } from '@/activities/timeline-activities/rows/components/EventRowItem';
+import {
+ StyledEventRowContainer,
+ StyledEventRowContent,
+ StyledEventRowDate,
+ StyledEventRowLinkedRecord,
+} from '@/activities/timeline-activities/rows/components/EventRowStyles';
+import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
+import { isDefined } from 'twenty-shared/utils';
+import { isNonEmptyString } from '@sniptt/guards';
+import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
+
+type EventRowGenericLinkedProps = EventRowDynamicComponentProps;
+
+export const EventRowGenericLinked = ({
+ event,
+ authorFullName,
+ linkedObjectMetadataItem,
+ createdAt,
+}: EventRowGenericLinkedProps) => {
+ const { openRecordInSidePanel } = useOpenRecordInSidePanel();
+
+ const objectLabel =
+ linkedObjectMetadataItem?.labelSingular?.toLowerCase() ?? t`record`;
+
+ const linkedRecordName = isNonEmptyString(event.linkedRecordCachedName)
+ ? event.linkedRecordCachedName
+ : t`Untitled`;
+
+ const canOpen =
+ isDefined(event.linkedRecordId) &&
+ isDefined(linkedObjectMetadataItem?.nameSingular);
+
+ const handleOpen = () => {
+ if (!canOpen) {
+ return;
+ }
+
+ openRecordInSidePanel({
+ recordId: event.linkedRecordId as string,
+ objectNameSingular: linkedObjectMetadataItem?.nameSingular as string,
+ });
+ };
+
+ const handleKeyDown = (keyboardEvent: KeyboardEvent) => {
+ if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
+ keyboardEvent.preventDefault();
+ handleOpen();
+ }
+ };
+
+ return (
+
+
+ {authorFullName}
+
+ {t`linked a ${objectLabel}`}
+
+
+
+
+
+ {createdAt}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts
index 6c5a8a9472..48a7249a07 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts
@@ -10,16 +10,28 @@ const mainObjectMetadataItem = {
updatableFields: [{ name: 'field1' }, { name: 'field2' }, { name: 'field3' }],
} as EnrichedObjectMetadataItem;
+const NOTE_OBJECT_METADATA_ID = '20202020-0000-4000-8000-00000000note';
+const TASK_OBJECT_METADATA_ID = '20202020-0000-4000-8000-00000000task';
+
const noteObjectMetadataItem = {
+ id: NOTE_OBJECT_METADATA_ID,
nameSingular: 'note',
namePlural: 'notes',
readableFields: [{ name: 'title' }, { name: 'body' }],
} as EnrichedObjectMetadataItem;
+const taskObjectMetadataItem = {
+ id: TASK_OBJECT_METADATA_ID,
+ nameSingular: 'task',
+ namePlural: 'tasks',
+ readableFields: [{ name: 'title' }, { name: 'body' }],
+} as EnrichedObjectMetadataItem;
+
const filter = (events: TimelineActivity[]) =>
filterOutInvalidTimelineActivities(events, 'company', [
mainObjectMetadataItem,
noteObjectMetadataItem,
+ taskObjectMetadataItem,
]);
describe('filterOutInvalidTimelineActivities', () => {
@@ -99,16 +111,64 @@ describe('filterOutInvalidTimelineActivities', () => {
expect(filter(events)).toEqual(events);
});
- it('keeps linked note/task update events even without a diff', () => {
+ it('keeps linked note/task rows that carry no diff', () => {
const events = [
- { id: '1', name: 'linked-task.updated', properties: {} },
- { id: '2', name: 'linked-note.updated', properties: {} },
+ {
+ id: '1',
+ name: 'linked-task.updated',
+ linkedObjectMetadataId: TASK_OBJECT_METADATA_ID,
+ properties: {},
+ },
+ {
+ id: '2',
+ name: 'linked-note.updated',
+ linkedObjectMetadataId: NOTE_OBJECT_METADATA_ID,
+ properties: {},
+ },
] as TimelineActivity[];
expect(filter(events)).toEqual(events);
});
it('validates linked note diffs against the note readable fields', () => {
+ const events = [
+ {
+ id: '1',
+ name: 'linked-note.updated',
+ linkedObjectMetadataId: NOTE_OBJECT_METADATA_ID,
+ properties: {
+ diff: {
+ title: { before: 'a', after: 'b' },
+ field1: { before: 'c', after: 'd' },
+ },
+ },
+ },
+ ] as TimelineActivity[];
+
+ expect(filter(events)).toEqual([
+ {
+ id: '1',
+ name: 'linked-note.updated',
+ linkedObjectMetadataId: NOTE_OBJECT_METADATA_ID,
+ properties: { diff: { title: { before: 'a', after: 'b' } } },
+ },
+ ]);
+ });
+
+ it('drops linked note updates whose diff has no readable note fields', () => {
+ const events = [
+ {
+ id: '1',
+ name: 'linked-note.updated',
+ linkedObjectMetadataId: NOTE_OBJECT_METADATA_ID,
+ properties: { diff: { field1: { before: 'c', after: 'd' } } },
+ },
+ ] as TimelineActivity[];
+
+ expect(filter(events)).toEqual([]);
+ });
+
+ it('resolves the linked object from the name for legacy rows without linkedObjectMetadataId', () => {
const events = [
{
id: '1',
@@ -130,16 +190,4 @@ describe('filterOutInvalidTimelineActivities', () => {
},
]);
});
-
- it('drops linked note updates whose diff has no readable note fields', () => {
- const events = [
- {
- id: '1',
- name: 'linked-note.updated',
- properties: { diff: { field1: { before: 'c', after: 'd' } } },
- },
- ] as TimelineActivity[];
-
- expect(filter(events)).toEqual([]);
- });
});
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts
index 35a86761be..1ab01231bf 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts
@@ -2,6 +2,7 @@ import { type TimelineActivity } from '@/activities/timeline-activities/types/Ti
import { findFieldMetadataItemByDiffKey } from '@/activities/timeline-activities/utils/findFieldMetadataItemByDiffKey';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { parseTimelineActivityAction } from 'twenty-shared/timeline';
import { isDefined } from 'twenty-shared/utils';
const keepActivityWithReadableDiff = (
@@ -27,6 +28,26 @@ const keepActivityWithReadableDiff = (
};
};
+// Activities created before the linkedObjectMetadataId column was populated
+// encode the linked object in their name, e.g. "linked-note.updated".
+const findLegacyObjectMetadataItemFromName = (
+ timelineActivity: TimelineActivity,
+ objectMetadataItems: EnrichedObjectMetadataItem[],
+): EnrichedObjectMetadataItem | undefined => {
+ if (!timelineActivity.name.startsWith('linked-')) {
+ return undefined;
+ }
+
+ const linkedObjectNameSingular = timelineActivity.name
+ .split('.')[0]
+ .replace('linked-', '');
+
+ return objectMetadataItems.find(
+ (objectMetadataItem) =>
+ objectMetadataItem.nameSingular === linkedObjectNameSingular,
+ );
+};
+
export const filterOutInvalidTimelineActivities = (
timelineActivities: TimelineActivity[],
mainObjectSingularName: string,
@@ -43,22 +64,28 @@ export const filterOutInvalidTimelineActivities = (
return timelineActivities
.map((timelineActivity) => {
- const [objectName, action] = timelineActivity.name.split('.');
+ const linkedObjectMetadataItem = isDefined(
+ timelineActivity.linkedObjectMetadataId,
+ )
+ ? objectMetadataItems.find(
+ (objectMetadataItem) =>
+ objectMetadataItem.id === timelineActivity.linkedObjectMetadataId,
+ )
+ : findLegacyObjectMetadataItemFromName(
+ timelineActivity,
+ objectMetadataItems,
+ );
- if (objectName.startsWith('linked-')) {
+ const action = parseTimelineActivityAction(timelineActivity.name);
+
+ if (isDefined(linkedObjectMetadataItem)) {
if (!isDefined(timelineActivity.properties?.diff)) {
return timelineActivity;
}
- const linkedObjectMetadataItem = objectMetadataItems.find(
- (objectMetadataItem) =>
- objectMetadataItem.nameSingular ===
- objectName.replace('linked-', ''),
- );
-
return keepActivityWithReadableDiff(
timelineActivity,
- linkedObjectMetadataItem?.readableFields ?? [],
+ linkedObjectMetadataItem.readableFields ?? [],
);
}
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts
index 37c62f8aeb..8b240fa399 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts
@@ -112,12 +112,10 @@ export class TimelineActivitySeederService {
constructor(private readonly objectMetadataService: ObjectMetadataService) {}
private getLinkedActivityName(activityType: string): string {
- // Notes and tasks use the legacy format: linked-{type}.created
if (activityType === 'note' || activityType === 'task') {
return `linked-${activityType}.created`;
}
- // Calendar events and messages use the new format: {type}.linked
return `${activityType}.linked`;
}
diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts
index 9dabdf9dbd..a2062c1238 100644
--- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts
+++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts
@@ -61,7 +61,7 @@ export class CalendarEventParticipantListener {
}
return {
- name: 'message.linked',
+ name: 'calendarEvent.linked',
properties: {},
objectSingularName: 'person',
recordId: participant.personId,
diff --git a/packages/twenty-shared/package.json b/packages/twenty-shared/package.json
index a0b3941be8..62db6afdd0 100644
--- a/packages/twenty-shared/package.json
+++ b/packages/twenty-shared/package.json
@@ -106,6 +106,11 @@
"import": "./dist/testing.mjs",
"require": "./dist/testing.cjs"
},
+ "./timeline": {
+ "types": "./dist/timeline/index.d.ts",
+ "import": "./dist/timeline.mjs",
+ "require": "./dist/timeline.cjs"
+ },
"./translations": {
"types": "./dist/translations/index.d.ts",
"import": "./dist/translations.mjs",
@@ -147,6 +152,7 @@
"logic-function",
"metadata",
"testing",
+ "timeline",
"translations",
"types",
"utils",
@@ -180,6 +186,9 @@
"testing": [
"dist/testing/index.d.ts"
],
+ "timeline": [
+ "dist/timeline/index.d.ts"
+ ],
"translations": [
"dist/translations/index.d.ts"
],
diff --git a/packages/twenty-shared/project.json b/packages/twenty-shared/project.json
index 9d42cea4df..d33ac40eda 100644
--- a/packages/twenty-shared/project.json
+++ b/packages/twenty-shared/project.json
@@ -28,6 +28,8 @@
"{projectRoot}/metadata/dist",
"{projectRoot}/testing/package.json",
"{projectRoot}/testing/dist",
+ "{projectRoot}/timeline/package.json",
+ "{projectRoot}/timeline/dist",
"{projectRoot}/translations/package.json",
"{projectRoot}/translations/dist",
"{projectRoot}/types/package.json",
diff --git a/packages/twenty-shared/src/timeline/TimelineActivityAction.ts b/packages/twenty-shared/src/timeline/TimelineActivityAction.ts
new file mode 100644
index 0000000000..8a702b855e
--- /dev/null
+++ b/packages/twenty-shared/src/timeline/TimelineActivityAction.ts
@@ -0,0 +1,6 @@
+export type TimelineActivityAction =
+ | 'created'
+ | 'updated'
+ | 'deleted'
+ | 'restored'
+ | 'linked';
diff --git a/packages/twenty-shared/src/timeline/__tests__/parseTimelineActivityAction.spec.ts b/packages/twenty-shared/src/timeline/__tests__/parseTimelineActivityAction.spec.ts
new file mode 100644
index 0000000000..0e00e06e5e
--- /dev/null
+++ b/packages/twenty-shared/src/timeline/__tests__/parseTimelineActivityAction.spec.ts
@@ -0,0 +1,21 @@
+import { parseTimelineActivityAction } from '@/timeline/parseTimelineActivityAction';
+
+describe('parseTimelineActivityAction', () => {
+ it.each([
+ ['company.created', 'created'],
+ ['company.updated', 'updated'],
+ ['company.deleted', 'deleted'],
+ ['company.restored', 'restored'],
+ ['linked-note.created', 'created'],
+ ['linked-task.updated', 'updated'],
+ ['message.linked', 'linked'],
+ ['calendarEvent.linked', 'linked'],
+ ])('parses the action from "%s" as "%s"', (name, expected) => {
+ expect(parseTimelineActivityAction(name)).toBe(expected);
+ });
+
+ it('falls back to "linked" for null or unrecognized names', () => {
+ expect(parseTimelineActivityAction(null)).toBe('linked');
+ expect(parseTimelineActivityAction('deal.went_cold')).toBe('linked');
+ });
+});
diff --git a/packages/twenty-shared/src/timeline/index.ts b/packages/twenty-shared/src/timeline/index.ts
new file mode 100644
index 0000000000..95f9c2a985
--- /dev/null
+++ b/packages/twenty-shared/src/timeline/index.ts
@@ -0,0 +1,11 @@
+/*
+ * _____ _
+ *|_ _|_ _____ _ __ | |_ _ _
+ * | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
+ * | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
+ * |_| \_/\_/ \___|_| |_|\__|\__, |
+ * |___/
+ */
+
+export { parseTimelineActivityAction } from './parseTimelineActivityAction';
+export type { TimelineActivityAction } from './TimelineActivityAction';
diff --git a/packages/twenty-shared/src/timeline/parseTimelineActivityAction.ts b/packages/twenty-shared/src/timeline/parseTimelineActivityAction.ts
new file mode 100644
index 0000000000..1a91c4927a
--- /dev/null
+++ b/packages/twenty-shared/src/timeline/parseTimelineActivityAction.ts
@@ -0,0 +1,23 @@
+import { type TimelineActivityAction } from '@/timeline/TimelineActivityAction';
+import { isDefined } from '@/utils/validation';
+
+const TIMELINE_ACTIVITY_ACTIONS: TimelineActivityAction[] = [
+ 'created',
+ 'updated',
+ 'deleted',
+ 'restored',
+ 'linked',
+];
+
+const isTimelineActivityAction = (
+ value: string | null | undefined,
+): value is TimelineActivityAction =>
+ isDefined(value) && (TIMELINE_ACTIVITY_ACTIONS as string[]).includes(value);
+
+export const parseTimelineActivityAction = (
+ name: string | null | undefined,
+): TimelineActivityAction => {
+ const action = name?.split('.')[1];
+
+ return isTimelineActivityAction(action) ? action : 'linked';
+};