New Timeline (#4936)

Refactored the code to introduce two different concepts:
- AuditLogs (immutable, raw data)
- TimelineActivities (user-friendly, transformed data)

Still some work needed:
- Add message, files, calendar events to timeline (~2 hours if done
naively)
- Refactor repository to try to abstract concept when we can (tbd, wait
for Twenty ORM)
- Introduce ability to display child timelines on parent timeline with
filtering (~2 days)
- Improve UI: add links to open note/task, improve diff display, etc
(half a day)
- Decide the path forward for Task vs Notes: either introduce a new
field type "Record Type" and start going into that direction ; or split
in two objects?
- Trigger updates when a field is changed (will be solved by real-time /
websockets: 2 weeks)
- Integrate behavioral events (1 day for POC, 1 week for
clean/documented)

<img width="1248" alt="Screenshot 2024-04-12 at 09 24 49"
src="https://github.com/twentyhq/twenty/assets/6399865/9428db1a-ab2b-492c-8b0b-d4d9a36e81fa">
This commit is contained in:
Félix Malfait
2024-04-19 17:52:57 +02:00
committed by GitHub
parent 9c8cb52952
commit d145684966
56 changed files with 1314 additions and 368 deletions
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class AuditLogRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async insert(
name: string,
properties: string,
workspaceMemberId: string | null,
objectName: string,
objectMetadataId: string,
recordId: string,
workspaceId: string,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."auditLog"
("name", "properties", "workspaceMemberId", "objectName", "objectMetadataId", "recordId")
VALUES ($1, $2, $3, $4, $5, $6)`,
[
name,
properties,
workspaceMemberId,
objectName,
objectMetadataId,
recordId,
],
workspaceId,
);
}
}
@@ -0,0 +1,136 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { objectRecordDiffMerge } from 'src/engine/integrations/event-emitter/utils/object-record-diff-merge';
@Injectable()
export class TimelineActivityRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async upsertOne(
name: string,
properties: Record<string, any>,
objectName: string,
recordId: string,
workspaceId: string,
workspaceMemberId?: string,
linkedRecordCachedName?: string,
linkedRecordId?: string,
linkedObjectMetadataId?: string,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const recentTimelineActivity = await this.findRecentTimelineActivity(
dataSourceSchema,
name,
objectName,
recordId,
workspaceMemberId,
linkedRecordId,
workspaceId,
);
if (recentTimelineActivity.length !== 0) {
const newProps = objectRecordDiffMerge(
recentTimelineActivity[0].properties,
properties,
);
return this.updateTimelineActivity(
dataSourceSchema,
recentTimelineActivity[0].id,
newProps,
workspaceMemberId,
workspaceId,
);
}
return this.insertTimelineActivity(
dataSourceSchema,
name,
properties,
objectName,
recordId,
workspaceMemberId,
linkedRecordCachedName ?? '',
linkedRecordId,
linkedObjectMetadataId,
workspaceId,
);
}
private async findRecentTimelineActivity(
dataSourceSchema: string,
name: string,
objectName: string,
recordId: string,
workspaceMemberId: string | undefined,
linkedRecordId: string | undefined,
workspaceId: string,
) {
return this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."timelineActivity"
WHERE "${objectName}Id" = $1
AND ("name" = $2 OR "name" = $3)
AND "workspaceMemberId" = $4
AND "linkedRecordId" = $5
AND "createdAt" >= NOW() - interval '10 minutes'`,
[
recordId,
name,
name.replace(/\.updated$/, '.created'),
workspaceMemberId,
linkedRecordId,
],
workspaceId,
);
}
private async updateTimelineActivity(
dataSourceSchema: string,
id: string,
properties: Record<string, any>,
workspaceMemberId: string | undefined,
workspaceId: string,
) {
return this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."timelineActivity"
SET "properties" = $2, "workspaceMemberId" = $3
WHERE "id" = $1`,
[id, properties, workspaceMemberId],
workspaceId,
);
}
private async insertTimelineActivity(
dataSourceSchema: string,
name: string,
properties: Record<string, any>,
objectName: string,
recordId: string,
workspaceMemberId: string | undefined,
linkedRecordCachedName: string,
linkedRecordId: string | undefined,
linkedObjectMetadataId: string | undefined,
workspaceId: string,
) {
return this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."timelineActivity"
("name", "properties", "workspaceMemberId", "${objectName}Id", "linkedRecordCachedName", "linkedRecordId", "linkedObjectMetadataId")
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
name,
properties,
workspaceMemberId,
recordId,
linkedRecordCachedName ?? '',
linkedRecordId,
linkedObjectMetadataId,
],
workspaceId,
);
}
}