855664daa2
## 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. -->
213 lines
5.3 KiB
JSON
213 lines
5.3 KiB
JSON
{
|
|
"name": "twenty-shared",
|
|
"private": true,
|
|
"sideEffects": false,
|
|
"main": "dist/index.cjs",
|
|
"module": "dist/index.mjs",
|
|
"types": "dist/index.d.ts",
|
|
"license": "AGPL-3.0",
|
|
"scripts": {
|
|
"build": "npx vite build"
|
|
},
|
|
"engines": {
|
|
"node": "^24.5.0",
|
|
"npm": "please-use-yarn",
|
|
"yarn": "^4.0.2"
|
|
},
|
|
"devDependencies": {
|
|
"@babel/preset-env": "^7.26.9",
|
|
"@babel/preset-typescript": "^7.24.6",
|
|
"@lingui/core": "^5.9.5",
|
|
"@prettier/sync": "^0.5.2",
|
|
"@swc/core": "^1.15.11",
|
|
"@swc/jest": "^0.2.39",
|
|
"@types/babel__preset-env": "^7",
|
|
"@types/handlebars": "^4.1.0",
|
|
"@types/jest": "^30.0.0",
|
|
"@types/lodash.camelcase": "^4.3.7",
|
|
"@types/lodash.escaperegexp": "^4.1.9",
|
|
"@types/qs": "6.9.16",
|
|
"@types/uuid": "^9.0.2",
|
|
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
|
"babel-plugin-module-resolver": "^5.0.2",
|
|
"glob": "^11.1.0",
|
|
"jest": "29.7.0",
|
|
"jest-environment-jsdom": "30.0.0-beta.3",
|
|
"prettier": "^3.1.1",
|
|
"slash": "^5.1.0",
|
|
"tsc-alias": "^1.8.16",
|
|
"tsx": "^4.19.3",
|
|
"typescript": "^5.9.3",
|
|
"vite": "^8.0.0",
|
|
"vite-plugin-dts": "^4.5.4",
|
|
"vite-tsconfig-paths": "^4.2.1"
|
|
},
|
|
"dependencies": {
|
|
"@dagrejs/dagre": "^1.1.8",
|
|
"@sniptt/guards": "^0.2.0",
|
|
"ai": "6.0.97",
|
|
"class-validator": "^0.14.0",
|
|
"expr-eval-fork": "3.0.3",
|
|
"handlebars": "^4.7.9",
|
|
"libphonenumber-js": "^1.10.26",
|
|
"lodash.camelcase": "^4.3.0",
|
|
"lodash.escaperegexp": "^4.1.2",
|
|
"microdiff": "^1.3.2",
|
|
"qs": "^6.15.2",
|
|
"react-router-dom": "^6.4.4",
|
|
"temporal-polyfill": "^0.3.0",
|
|
"transliteration": "^2.3.5",
|
|
"uuid": "^11.1.1",
|
|
"zod": "^4.1.11"
|
|
},
|
|
"exports": {
|
|
".": {
|
|
"types": "./dist/index.d.ts",
|
|
"import": "./dist/index.mjs",
|
|
"require": "./dist/index.cjs"
|
|
},
|
|
"./ai": {
|
|
"types": "./dist/ai/index.d.ts",
|
|
"import": "./dist/ai.mjs",
|
|
"require": "./dist/ai.cjs"
|
|
},
|
|
"./application": {
|
|
"types": "./dist/application/index.d.ts",
|
|
"import": "./dist/application.mjs",
|
|
"require": "./dist/application.cjs"
|
|
},
|
|
"./constants": {
|
|
"types": "./dist/constants/index.d.ts",
|
|
"import": "./dist/constants.mjs",
|
|
"require": "./dist/constants.cjs"
|
|
},
|
|
"./database-events": {
|
|
"types": "./dist/database-events/index.d.ts",
|
|
"import": "./dist/database-events.mjs",
|
|
"require": "./dist/database-events.cjs"
|
|
},
|
|
"./i18n": {
|
|
"types": "./dist/i18n/index.d.ts",
|
|
"import": "./dist/i18n.mjs",
|
|
"require": "./dist/i18n.cjs"
|
|
},
|
|
"./logic-function": {
|
|
"types": "./dist/logic-function/index.d.ts",
|
|
"import": "./dist/logic-function.mjs",
|
|
"require": "./dist/logic-function.cjs"
|
|
},
|
|
"./metadata": {
|
|
"types": "./dist/metadata/index.d.ts",
|
|
"import": "./dist/metadata.mjs",
|
|
"require": "./dist/metadata.cjs"
|
|
},
|
|
"./testing": {
|
|
"types": "./dist/testing/index.d.ts",
|
|
"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",
|
|
"require": "./dist/translations.cjs"
|
|
},
|
|
"./types": {
|
|
"types": "./dist/types/index.d.ts",
|
|
"import": "./dist/types.mjs",
|
|
"require": "./dist/types.cjs"
|
|
},
|
|
"./utils": {
|
|
"types": "./dist/utils/index.d.ts",
|
|
"import": "./dist/utils.mjs",
|
|
"require": "./dist/utils.cjs"
|
|
},
|
|
"./vite": {
|
|
"types": "./dist/vite/index.d.ts",
|
|
"import": "./dist/vite.mjs",
|
|
"require": "./dist/vite.cjs"
|
|
},
|
|
"./workflow": {
|
|
"types": "./dist/workflow/index.d.ts",
|
|
"import": "./dist/workflow.mjs",
|
|
"require": "./dist/workflow.cjs"
|
|
},
|
|
"./workspace": {
|
|
"types": "./dist/workspace/index.d.ts",
|
|
"import": "./dist/workspace.mjs",
|
|
"require": "./dist/workspace.cjs"
|
|
}
|
|
},
|
|
"files": [
|
|
"dist",
|
|
"ai",
|
|
"application",
|
|
"constants",
|
|
"database-events",
|
|
"i18n",
|
|
"logic-function",
|
|
"metadata",
|
|
"testing",
|
|
"timeline",
|
|
"translations",
|
|
"types",
|
|
"utils",
|
|
"vite",
|
|
"workflow",
|
|
"workspace"
|
|
],
|
|
"typesVersions": {
|
|
"*": {
|
|
"ai": [
|
|
"dist/ai/index.d.ts"
|
|
],
|
|
"application": [
|
|
"dist/application/index.d.ts"
|
|
],
|
|
"constants": [
|
|
"dist/constants/index.d.ts"
|
|
],
|
|
"database-events": [
|
|
"dist/database-events/index.d.ts"
|
|
],
|
|
"i18n": [
|
|
"dist/i18n/index.d.ts"
|
|
],
|
|
"logic-function": [
|
|
"dist/logic-function/index.d.ts"
|
|
],
|
|
"metadata": [
|
|
"dist/metadata/index.d.ts"
|
|
],
|
|
"testing": [
|
|
"dist/testing/index.d.ts"
|
|
],
|
|
"timeline": [
|
|
"dist/timeline/index.d.ts"
|
|
],
|
|
"translations": [
|
|
"dist/translations/index.d.ts"
|
|
],
|
|
"types": [
|
|
"dist/types/index.d.ts"
|
|
],
|
|
"utils": [
|
|
"dist/utils/index.d.ts"
|
|
],
|
|
"vite": [
|
|
"dist/vite/index.d.ts"
|
|
],
|
|
"workflow": [
|
|
"dist/workflow/index.d.ts"
|
|
],
|
|
"workspace": [
|
|
"dist/workspace/index.d.ts"
|
|
]
|
|
}
|
|
}
|
|
}
|