mirror of
https://github.com/twentyhq/twenty.git
synced 2026-08-02 02:30:26 -04:00
## What & why
The timeline-activity system's contract is a magic `name` string
(`"company.updated"`, `"linked-note.created"`, `"message.linked"`)
decoded by `String.split('.')` in **four** different frontend spots and
produced by a hardcoded `if`-ladder + two listeners. It is not
extensible and it already harbored a latent bug.
This PR replaces that stringly-typed protocol with an explicit,
persisted **`kind`** contract consumed through registries on both ends.
Adding a new timeline activity type becomes: add a producer + register a
presenter — no edits to a central switch.
This is **Layer A** of a larger plan (see
`packages/twenty-server/docs/TIMELINE_ACTIVITIES_REFACTOR.md` and
`TIMELINE_ACTIVITIES_PR_A.md`). Layer B (timeline projection /
"inheritance") and Layer C (user-defined aggregation rules) are
intentionally **out of scope** here.
## 🐛 Bug fixed along the way
`calendar-event-participant.listener.ts` was writing calendar-event
timeline rows with `name: 'message.linked'` (copy-paste from the message
listener). It rendered "correctly" only by luck — the frontend routed on
`linkedObjectMetadataId → nameSingular`, never on `name`. This PR fixes
it at the source (`calendarEvent.linked` / `kind:
'linkedCalendarEvent'`), and the shared resolver also corrects
historical rows that carry the wrong `name`.
## Changes
**`twenty-shared`** — new `timeline` module
- `TimelineActivityKind` (`recordChange | linkedNote | linkedTask |
linkedMessage | linkedCalendarEvent | linkedRecord`) +
`resolveTimelineActivityDescriptor`, the **single** place that decodes
an activity into `{ kind, action }`. Reads the persisted `kind` when
present and falls back to legacy `name`/`linkedObjectMetadataId` parsing
(back-compat shim). Unit-tested (20 cases).
**`twenty-server`**
- Persist a nullable `kind` field on the `timelineActivity` standard
object (entity shape + field-metadata builder + universalIdentifier).
- Producers (`timeline-activity.service.ts`, the two participant
listeners) set `kind` explicitly; dev seeder populates it.
- Fix the `calendarEvent.linked` mislabel.
**`twenty-front`**
- Static `TIMELINE_ACTIVITY_PRESENTERS` registry replaces the render
`switch`, the icon `if`-chain, the diff-validation name-parsing, and the
`name.match(/note|task/i)` title-prefetch hack.
- New `EventRowGenericLinked` so an unknown linked object type renders a
real "linked a {object}" row instead of falling through to the wrong
(main-object) renderer.
## Migration / compatibility
- The `kind` column on this **workspace** standard object is created by
the normal workspace metadata sync — no hand-written migration. It is
**nullable**, so pre-upgrade rows degrade gracefully through the
resolver shim (they resolve correctly from `linkedObjectMetadataId` +
`name`). An optional backfill workspace command could populate `kind` on
old rows later; not required for correctness.
- No GraphQL breaking change — `kind` is additive, `name` is retained
for display/search.
## Test plan
- `twenty-shared` unit tests (resolver) ✅
- `typecheck` + `lint:diff-with-main` green on `twenty-front`,
`twenty-server`, `twenty-shared` ✅
- Reset + reseed a workspace: `kind` is populated for all seeded rows
(recordChange / linkedMessage / linkedNote / linkedTask /
linkedCalendarEvent) with no nulls ✅
- Manual end-to-end verification via Playwright on person / company
record timelines — screenshots in a follow-up comment.
Screenshots attesting the rendering (incl. the calendar fix) are posted
as a comment below.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YRueWMo4UyaX2em8R2cdio)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21950?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
83 lines
2.9 KiB
JSON
83 lines
2.9 KiB
JSON
{
|
|
"name": "twenty-shared",
|
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
"sourceRoot": "packages/twenty-shared/src",
|
|
"projectType": "library",
|
|
"tags": ["scope:shared"],
|
|
"targets": {
|
|
"build": {
|
|
"executor": "nx:run-commands",
|
|
"cache": true,
|
|
"inputs": ["production", "^production"],
|
|
"dependsOn": ["generateBarrels", "^build"],
|
|
"outputs": [
|
|
"{projectRoot}/dist",
|
|
"{projectRoot}/ai/package.json",
|
|
"{projectRoot}/ai/dist",
|
|
"{projectRoot}/application/package.json",
|
|
"{projectRoot}/application/dist",
|
|
"{projectRoot}/constants/package.json",
|
|
"{projectRoot}/constants/dist",
|
|
"{projectRoot}/database-events/package.json",
|
|
"{projectRoot}/database-events/dist",
|
|
"{projectRoot}/i18n/package.json",
|
|
"{projectRoot}/i18n/dist",
|
|
"{projectRoot}/logic-function/package.json",
|
|
"{projectRoot}/logic-function/dist",
|
|
"{projectRoot}/metadata/package.json",
|
|
"{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",
|
|
"{projectRoot}/types/dist",
|
|
"{projectRoot}/utils/package.json",
|
|
"{projectRoot}/utils/dist",
|
|
"{projectRoot}/vite/package.json",
|
|
"{projectRoot}/vite/dist",
|
|
"{projectRoot}/workflow/package.json",
|
|
"{projectRoot}/workflow/dist",
|
|
"{projectRoot}/workspace/package.json",
|
|
"{projectRoot}/workspace/dist"
|
|
],
|
|
"options": {
|
|
"cwd": "{projectRoot}",
|
|
"commands": [
|
|
"npx vite build",
|
|
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
|
|
],
|
|
"parallel": false
|
|
}
|
|
},
|
|
"build:individual": {
|
|
"executor": "nx:run-commands",
|
|
"cache": true,
|
|
"dependsOn": ["build"],
|
|
"inputs": ["production", "^production"],
|
|
"outputs": ["{projectRoot}/dist/individual"],
|
|
"options": {
|
|
"cwd": "{projectRoot}",
|
|
"command": "npx vite build -c vite.config.individual.ts"
|
|
}
|
|
},
|
|
"generateBarrels": {
|
|
"executor": "nx:run-commands",
|
|
"cache": true,
|
|
"inputs": ["production", "{projectRoot}/scripts/generateBarrels.ts"],
|
|
"outputs": [
|
|
"{projectRoot}/src/index.ts",
|
|
"{projectRoot}/src/*/index.ts",
|
|
"{projectRoot}/package.json"
|
|
],
|
|
"options": { "command": "tsx {projectRoot}/scripts/generateBarrels.ts" }
|
|
},
|
|
"typecheck": {},
|
|
"test": {},
|
|
"lint": { "options": {}, "configurations": { "fix": {} } },
|
|
"fmt": { "options": { "files": "src" }, "configurations": { "fix": {} } }
|
|
}
|
|
}
|