snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
---
title: Extension des objets
description: Ajoutez des champs aux objets standard de Twenty (Person, Company, …) ou aux objets d'autres applications en utilisant defineField.
icon: wand-magic-sparkles
---
Utilisez `defineField()` pour ajouter un champ à un objet que vous ne possédez pas — un objet standard de Twenty comme Person ou Company, ou un objet fourni par une autre application installée. Contrairement aux champs en ligne déclarés à l'intérieur de [`defineObject`](/l/fr/developers/extend/apps/data/objects), les champs autonomes nécessitent un `objectUniversalIdentifier` pour préciser l'objet qu'ils étendent.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
## Points clés
* `objectUniversalIdentifier` identifie l'objet cible. Pour les objets standard de Twenty, importez la constante depuis `twenty-sdk` :
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* Lorsque vous définissez des champs **en ligne dans `defineObject()`**, vous n'avez **pas** besoin de `objectUniversalIdentifier` — il est hérité de l'objet parent.
* `defineField()` est la seule manière d'ajouter des champs à des objets que vous n'avez pas créés avec `defineObject()`.
* L’emplacement du fichier est à votre discrétion. La convention est `src/fields/\<name>.field.ts`, mais le SDK détecte les champs n'importe où dans `src/`.
* La `writability` peut également être définie par champ : `MetadataWritability.APPLICATION` limite les écritures à la logique interne de votre application, `MetadataWritability.SYSTEM` est réservé aux données gérées par la plateforme. La `writability` d’un champ ne peut être que plus stricte que le niveau de son objet, et les lectures ne sont pas affectées. Voir [Objects](/l/fr/developers/extend/apps/data/objects) pour la sémantique complète.
* Pour ajouter un onglet à une mise en page standard (par exemple la page de détail de Task ou de Company), utilisez [`definePageLayoutTab`](/l/fr/developers/extend/apps/layout/page-layouts#definepagelayouttab) avec `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` depuis `twenty-sdk/define`.
## Ajouter une relation à un objet existant
Pour ajouter un champ de relation (par exemple pour lier votre objet personnalisé à un `Person` standard), utilisez `defineField()` avec `FieldType.RELATION`. Le modèle est le même que pour les relations en ligne, mais avec `objectUniversalIdentifier` défini explicitement. Voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle bidirectionnel.

View File

@@ -0,0 +1,143 @@
---
title: Objets
description: Déclarez de nouveaux types d’enregistrements — des tables personnalisées avec leurs propres champs — à l’aide de defineObject.
icon: table
---
Les **objets** personnalisés sont de nouveaux types d’enregistrements que votre application ajoute à un espace de travail — carte postale, facture, abonnement, tout ce qui est spécifique à votre domaine. Chaque objet déclare son schéma (champs, relations, valeurs par défaut) et un identifiant universel stable qui survit aux synchronisations et aux déploiements.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
## Points clés
* Le `universalIdentifier` doit être unique et stable entre les déploiements.
* Chaque champ nécessite un `name`, un `type`, un `label` et son propre `universalIdentifier` stable.
* Le tableau `fields` est facultatif — vous pouvez définir des objets sans champs personnalisés.
* `openRecordIn` définit où les enregistrements de cet objet s’ouvrent lorsqu’on clique dessus : `ObjectOpenRecordIn.USER_CHOICE` (la valeur par défaut, qui suit la préférence de chaque membre de l’espace de travail dans Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` ou `ObjectOpenRecordIn.RECORD_PAGE`. Épinglez-le sur `RECORD_PAGE` pour les enregistrements qui ont besoin d’une page complète pour être utilisables, comme les flux de travail et les tableaux de bord, ou sur `SIDE_PANEL` pour les enregistrements qui n’ont de sens qu’en tant que panneau rapide, comme les événements de calendrier.
* `writability` contrôle qui peut écrire des enregistrements de l’objet en général, avant que les autorisations de rôle ne s’appliquent : `MetadataWritability.OPEN` (la valeur par défaut — les rôles de l’espace de travail décident), `MetadataWritability.APPLICATION` (seules les fonctions logiques propres à votre application peuvent créer, mettre à jour ou supprimer des enregistrements ; utilisez ceci pour des objets de type configuration dont les enregistrements confèrent un comportement, afin que les membres de l’espace de travail ayant un large accès aux enregistrements ne puissent pas les modifier via l’API), ou `MetadataWritability.SYSTEM` (réservé aux données gérées par la plateforme). Les opérations de lecture ne sont pas affectées — ceci est imposé côté serveur, contrairement à `isUIEditable`, qui ne fait que masquer les éléments de l’interface utilisateur. Cela existe aussi au niveau de chaque champ, où cela ne peut être que plus strict que le niveau de l’objet.
* Les champs en ligne définis ici n’ont **pas** besoin d’un `objectUniversalIdentifier` — il est hérité de l’objet parent. Utilisez [`defineField()`](/l/fr/developers/extend/apps/data/extending-objects) pour ajouter des champs aux objets que vous ne possédez pas.
* Vous pouvez générer de nouveaux objets avec `yarn twenty dev:add object`, qui vous guide à travers le nommage, les champs et les relations. Voir [Architecture → Scaffolding entities](/l/fr/developers/extend/apps/getting-started/scaffolding).
<Note>
**Les champs de base sont ajoutés automatiquement.** Lorsque vous définissez un objet personnalisé, Twenty crée pour vous des champs standard comme `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` et `deletedAt`. Vous n’avez pas besoin de les déclarer dans votre tableau `fields` — uniquement vos champs personnalisés. Vous pouvez remplacer un champ par défaut en en déclarant un avec le même nom, mais c’est rarement une bonne idée.
</Note>
## Types de champ
L’ensemble complet des valeurs de `FieldType`, exportées depuis `twenty-sdk/define` :
| Catégorie | Types |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Texte | `TEXT`, `RICH_TEXT`, `ARRAY` (de chaînes), `RAW_JSON` |
| Numérique | `NUMBER` (`universalSettings.dataType` : `'float'` / `'int'` / `'bigint'`), `NUMERIC` (précision arbitraire), `RATING`, `POSITION` |
| Dates | `DATE`, `DATE_TIME` |
| Choix | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Composés | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identifiants et relations | `UUID`, `RELATION`, `MORPH_RELATION` (voir [Relations](/l/fr/developers/extend/apps/data/relations)) |
| Système | `TS_VECTOR` (vecteur de recherche en texte intégral, géré par le serveur) |
Les types composés stockent plusieurs sous-champs (par exemple `FULL_NAME` = prénom + nom de famille ; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` et `MULTI_SELECT` nécessitent un tableau `options` comme dans l’exemple ci-dessus.
## Valeurs par défaut
Les valeurs par défaut de type chaîne littérale doivent être entourées de guillemets simples **à l’intérieur** de la chaîne — `defaultValue: "'Draft'"`, et non `defaultValue: "Draft"`. C’est pourquoi le champ `status` ci-dessus utilise `` `'${PostCardStatus.DRAFT}'` ``.
Les chaînes sans guillemets sont réservées aux valeurs par défaut calculées, évaluées lors de la création d’un enregistrement :
* `'uuid'` — génère un UUID (pour les champs `UUID`)
* `'now'` — l’horodatage actuel (pour les champs `DATE_TIME`)
La même convention s’applique aux sous-champs de type chaîne des valeurs par défaut composites (par exemple `{ source: "'MANUAL'" }` sur un champ `ACTOR`) ainsi qu’aux valeurs `SELECT`/`MULTI_SELECT`. Une valeur par défaut littérale de type chaîne laissée sans guillemets génère un avertissement lors de la compilation de votre application.
## Nullabilité
`isNullable` détermine si un champ accepte `NULL`. Sa valeur par défaut est `true` — omettez-le pour les champs facultatifs. Définissez `isNullable: false` pour rendre un champ obligatoire au niveau de la base de données.
Les modifications de `isNullable` sont appliquées à chaque synchronisation, y compris celles qui mettent à jour un champ existant — vous pouvez donc modifier la nullabilité d’un champ en éditant le manifeste puis en relançant la synchronisation.
<Note>
**Rendre un champ existant non nullable nécessite une valeur par défaut.** Lorsque vous modifiez un champ avec `isNullable: false`, vous devez également fournir une `defaultValue` non nulle. La valeur par défaut remplit rétroactivement toutes les lignes `NULL` existantes avant que la contrainte `NOT NULL` ne soit appliquée ; sans cela, la synchronisation échoue avec `Default value cannot be null for non-nullable fields`. Les champs de relation et les champs `TS_VECTOR` sont toujours nullables, donc `isNullable` n’a aucun effet sur eux.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## Et après
* **Connectez cet objet aux autres** — voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle de relation bidirectionnelle.
* **Ajoutez des champs aux objets d’autres applications** — voir [Extending Objects](/l/fr/developers/extend/apps/data/extending-objects) pour `defineField()`.
* **Afficher cet objet dans l’interface utilisateur** — voir [Éléments du menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items) pour ajouter une entrée dans la barre latérale ; voir [Vues](/l/fr/developers/extend/apps/layout/views) pour ajouter des configurations de liste personnalisées.

View File

@@ -0,0 +1,101 @@
---
title: Vue d'ensemble
description: Façonnez les données que votre application ajoute à un espace de travail — objets, champs et relations.
icon: database
---
La **couche de données** d’une application Twenty correspond aux données que votre application *ajoute* à un espace de travail — les nouveaux types d’enregistrements qu’elle déclare, les colonnes qu’elle ajoute aux objets existants et la manière dont ces enregistrements sont reliés les uns aux autres.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Objets" icon="table" href="/l/fr/developers/extend/apps/data/objects">
`defineObject` — déclarez de nouveaux types d’enregistrements avec leurs propres champs.
</Card>
<Card title="Extension des objets" icon="wand-magic-sparkles" href="/l/fr/developers/extend/apps/data/extending-objects">
`defineField` — ajoutez des champs aux objets standard ou à ceux d’autres applications.
</Card>
<Card title="Relations" icon="diagram-project" href="/l/fr/developers/extend/apps/data/relations">
Connexions bidirectionnelles `MANY_TO_ONE` / `ONE_TO_MANY` entre objets.
</Card>
<Card title="Types d'activités de la chronologie" icon="clock-rotate-left" href="/l/fr/developers/extend/apps/data/timeline-activity-types">
Définissez des événements d'audit automatiques et des événements d'application explicites pour les chronologies d'enregistrements.
</Card>
</CardGroup>
## Aperçu des entités
| Entité | Objectif | Défini avec |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Objet** | Un nouveau type d’enregistrement personnalisé (par ex. PostCard, Invoice) avec ses propres champs | `defineObject()` |
| **Champ** | Une colonne d’un objet. Les champs autonomes peuvent étendre des objets que vous n’avez pas créés (par ex. ajouter `loyaltyTier` à Company) | `defineField()` |
| **Relation** | Un lien bidirectionnel entre deux objets — les deux côtés étant déclarés comme des champs | `defineField()` avec `FieldType.RELATION` |
| **Index** | Un index de base de données pour accélérer une requête récurrente sur l’un de vos objets | `defineIndex()` |
| **Type d'activité de la chronologie** | Un événement d'audit automatique ou un événement de domaine explicite affiché dans les chronologies des enregistrements. | `defineTimelineActivityType()` |
Le SDK les détecte via une analyse AST au moment de la compilation, donc l’organisation des fichiers vous appartient — la convention est `src/objects/`, `src/fields/`, `src/indexes/` et `src/timeline-activity-types/`. Des UUID `universalIdentifier` stables relient tout d’un déploiement à l’autre.
## Index (optionnels)
Les applications peuvent fournir des index avec leurs objets pour que les requêtes récurrentes restent rapides. Le cas le plus courant est une colonne de statut ou de clé étrangère que vous lisez fréquemment.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### Index uniques
`defineIndex` accepte `isUnique: true` pour l’unicité sur une ou plusieurs colonnes. C’est la primitive recommandée — `defineField({ isUnique: true })` est obsolète et sera supprimé dans une future version.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Autres contraintes
* Les clauses `WHERE` partielles restent sous le contrôle de l’administrateur — les applications ne peuvent pas les déclarer.
* Chaque objet est limité à 10 index personnalisés (les index propres au framework ne comptent pas).
Ordonnez le tableau `fields` dans l’ordre dans lequel Postgres doit l’utiliser — colonne la plus à gauche en premier, comme dans un annuaire téléphonique. Les index ne sont pas gratuits : chaque écriture dans la table les met à jour. Ajoutez-en un uniquement lorsque vous avez une requête qui en a besoin.
<Note>
Vous cherchez **Application Config** ou **Roles & Permissions** ? Ces éléments décrivent l’application elle-même plutôt que les données qu’elle ajoute — ils se trouvent sous [Config](/l/fr/developers/extend/apps/config/overview). Vous cherchez **Connections** (Linear, GitHub, Slack OAuth) ? Ceux-ci existent pour être appelés *depuis* des fonctions logiques et se trouvent sous [Logic](/l/fr/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Relations
description: Reliez des objets entre eux avec des relations MANY_TO_ONE / ONE_TO_MANY bidirectionnelles.
icon: diagram-project
---
Les relations relient deux objets entre eux. Dans Twenty, les relations sont toujours **bidirectionnelles** — chaque relation a deux côtés, et chaque côté est déclaré comme un champ qui référence l'autre.
| Type de relation | Description | Possède une clé étrangère ? |
| ---------------- | ------------------------------------------------------------------------------------ | --------------------------- |
| `MANY_TO_ONE` | De nombreux enregistrements de cet objet pointent vers un enregistrement de la cible | Oui (`joinColumnName`) |
| `ONE_TO_MANY` | Un enregistrement de cet objet possède de nombreux enregistrements de la cible | Non (le côté inverse) |
## Fonctionnement des relations
Chaque relation nécessite **deux champs** qui se référencent mutuellement :
1. Le côté **MANY_TO_ONE** — réside sur l'objet qui détient la clé étrangère.
2. Le côté **ONE_TO_MANY** — réside sur l'objet qui possède la collection.
Les deux champs utilisent `FieldType.RELATION` et se référencent mutuellement via `relationTargetFieldMetadataUniversalIdentifier`.
## Exemple : une carte postale a de nombreux destinataires
Un `PostCard` peut être envoyé à de nombreux enregistrements `PostCardRecipient`. Chaque destinataire appartient à exactement une carte postale.
**Étape 1: Définir le côté ONE_TO_MANY sur PostCard** (le côté "one") :
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Étape 2: Définir le côté MANY_TO_ONE sur PostCardRecipient** (le côté "many" — détient la clé étrangère) :
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Imports circulaires :** les deux champs de relation se référencent l'un l'autre via leur `universalIdentifier`. Pour éviter les problèmes d'import circulaire, exportez les ID de vos champs en tant que constantes nommées depuis chaque fichier, puis importez-les dans l'autre. Le système de build les résout au moment de la compilation.
</Note>
## Relations de jonction
Un objet de jonction connecte un enregistrement source à un ou plusieurs enregistrements cibles. Par exemple, `PostCardRecipient` peut contenir à la fois une relation `postCard` et une relation `recipient`. Dans le champ `ONE_TO_MANY` de l'objet source, définissez `junctionTargetFieldUniversalIdentifier` sur le champ de relation de l'objet de jonction qui pointe vers la cible :
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` doit identifier une relation `MANY_TO_ONE` ou une relation morph sur l'objet de jonction. Il indique à Twenty quel enregistrement associé est la cible lorsqu'une fonctionnalité telle que la [diffusion des activités de chronologie](/l/fr/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) parcourt la jonction.
## Relation avec des objets standard
Pour créer une relation avec un objet Twenty intégré (Person, Company, etc.), utilisez `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` :
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
## Propriétés des champs de relation
| Propriété | Obligatoire | Description |
| ---------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `type` | Oui | Doit être `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Oui | Le `universalIdentifier` de l'objet cible |
| `relationTargetFieldMetadataUniversalIdentifier` | Oui | Le `universalIdentifier` du champ correspondant sur l'objet cible |
| `universalSettings.relationType` | Oui | `RelationType.MANY_TO_ONE` ou `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE uniquement | Ce qui se passe lorsque l'enregistrement référencé est supprimé : `CASCADE`, `SET_NULL`, `RESTRICT` ou `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE uniquement | Nom de la colonne de base de données pour la clé étrangère (p. ex., `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Jonction `ONE_TO_MANY` uniquement | Champ de relation de l'objet de jonction qui pointe vers la cible de diffusion |
## Champs de relation en ligne
Vous pouvez également déclarer une relation directement à l'intérieur de [`defineObject`](/l/fr/developers/extend/apps/data/objects). Lorsqu'il est en ligne, omettez `objectUniversalIdentifier` — il est hérité de l'objet parent :
```ts
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// … other fields
],
});
```

View File

@@ -0,0 +1,210 @@
---
title: Ciblage des métadonnées système
description: Résolvez les identifiants universels déterministes des métadonnées Twenty provisionnées automatiquement sur chaque objet, afin que votre application puisse s’y référer sans les coder en dur.
icon: gears
---
Chaque objet dans Twenty est fourni avec des **métadonnées système** que vous ne déclarez jamais vous‑même, comme un ensemble de champs et une vue principale en liste avec ses colonnes. Le serveur crée l’ensemble de ces éléments lorsque l’objet est provisionné, et cet ensemble s’agrandit au fur et à mesure que Twenty évolue.
Comme vous ne les déclarez pas, il n’existe pas de constante `universalIdentifier` que vous puissiez importer. À la place, le serveur **dérive** chaque identifiant de manière déterministe, et `twenty-sdk` expose la même dérivation afin que votre manifeste puisse résoudre la valeur exacte utilisée par le serveur.
## Champs système
Les champs scalaires présents sur chaque objet, que vous ne déclarez jamais avec [`defineField()`](/l/fr/developers/extend/apps/data/extending-objects) :
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Alors, comment référencer `createdAt` en tant que colonne dans une [vue](/l/fr/developers/extend/apps/layout/views) ?
### Le problème
Depuis Twenty 2.19, l'identifiant universel d'un champ système est **dérivé de manière déterministe** par le serveur à partir de trois entrées : l'identifiant universel de l'application, l'identifiant universel de l'objet et le nom du champ. Inventer un id et le coder en dur ne fonctionnera pas : il ne correspond à rien sur le serveur, et la synchronisation rejette la référence orpheline :
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### La solution
<Note>
`getFieldUniversalIdentifier` est disponible à partir de `twenty-sdk` 2.21.
</Note>
Utilisez `getFieldUniversalIdentifier` pour résoudre exactement la même valeur que celle utilisée par le serveur. Elle prend les trois entrées et renvoie l'identifiant universel du champ :
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` est l'identifiant de votre application, celui que vous transmettez à [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
* `objectUniversalIdentifier` est l'identifiant de l'objet auquel le champ appartient.
* `name` est le nom du champ système, l'une des valeurs listées ci-dessus.
### Exemple : une colonne createdAt dans une vue
Le cas typique consiste à ajouter une colonne `createdAt` à une vue de l'un de vos objets personnalisés. Résolvez l'id du champ et référencez-le comme n'importe quel autre `fieldMetadataUniversalIdentifier` :
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Le même id résolu fonctionne partout où un `fieldMetadataUniversalIdentifier` est attendu : champs de vue, filtres, tris, regroupements et widgets de mise en page.
<Note>
Résolvez l'id, ne le codez pas en dur. Parce que le serveur dérive la valeur à partir de
l'id de l'application, de l'id de l'objet et du nom du champ, appeler
`getFieldUniversalIdentifier` garde votre référence correcte même si ces
entrées changent, et évite les divergences si la dérivation évolue un jour.
</Note>
### Champs de relation système
<Note>
`getSystemRelationFieldUniversalIdentifier` est disponible dans `twenty-sdk`
à partir de la version 2.23 et nécessite un serveur Twenty en version 2.23 ou ultérieure.
</Note>
Outre les champs système scalaires ci-dessus, le serveur met également à disposition quatre **champs de relation système** sur chaque objet : `timelineActivities`, `attachments`, `noteTargets` et `taskTargets`, chacun pointant vers l’objet de relation standard correspondant.
Ainsi, ces champs ne sont pas résolus avec `getFieldUniversalIdentifier` : leur identifiant est dérivé **indépendamment du nom**, à partir de l’objet qui héberge le champ et de l’objet vers lequel le champ pointe. De cette façon, renommer un objet ne modifie jamais les identifiants de ses champs de relation.
Utilisez `getSystemRelationFieldUniversalIdentifier` pour les résoudre :
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` est l’objet qui **héberge** le champ.
* `relationTargetObjectUniversalIdentifier` est l’objet vers lequel le champ **pointe**.
La direction est encodée par l’ordre des arguments. Pour résoudre le côté inverse (par exemple `attachment.targetRocket`, le champ morph que le serveur crée sur l’objet de relation standard), inversez les deux :
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
Comme pour les champs système scalaires, l’id résolu fonctionne partout où un `fieldMetadataUniversalIdentifier` est attendu.
## Vues système
<Note>
`getSystemViewUniversalIdentifier` et `getSystemViewFieldUniversalIdentifier`
sont disponibles dans `twenty-sdk` à partir de la version 2.26 et nécessitent
un serveur Twenty en version 2.26 ou ultérieure.
</Note>
Le serveur provisionne également une **vue système** sur chaque objet : la vue principale en liste (`All {objectLabelPlural}`, indexée sur `ViewKey.INDEX` (dérivée de `SYSTEM_VIEW_KEYS.INDEX`)), avec une colonne par champ affichable. Comme pour les champs de relation système, leurs identifiants sont dérivés **sans nom**, de sorte que renommer un objet ou un champ ne les modifie jamais.
Utilisez `getSystemViewUniversalIdentifier` pour la résoudre :
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` correspond à l’application propriétaire de l’**objet**, ce qui sert d’espace de noms pour la vue.
* `objectUniversalIdentifier` est l’objet que la vue répertorie.
* `viewKey` est le discriminateur de vue système : `SYSTEM_VIEW_KEYS.INDEX` pour la vue principale en liste, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` pour la vue du widget de champs de la page d’enregistrement. Il sert de clé pour la dérivation ; seul `INDEX` est également stocké sur la ligne de la vue.
L’identifiant résolu fonctionne partout où un `viewUniversalIdentifier` est attendu, par exemple pour une entrée de barre latérale [`NavigationMenuItemType.VIEW`](/l/fr/developers/extend/apps/layout/navigation-menu-items). Pour simplement ouvrir la liste principale d’un objet, privilégiez `NavigationMenuItemType.OBJECT` avec `targetObjectUniversalIdentifier` : cela ne nécessite aucune dérivation.
`getSystemViewFieldUniversalIdentifier` résout une seule **colonne** sur une vue système, à partir de la vue et du champ qu’elle affiche :
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Notez le premier argument : une colonne est placée dans l’espace de noms de l’application propriétaire du **champ qu’elle affiche**, et non de celle propriétaire de la vue. Un champ que votre application ajoute à un objet standard obtient sa colonne dérivée sous votre application, sur une vue appartenant à Twenty.
<Warning>
Les vues système et leurs colonnes sont **la propriété du serveur** : résolvez leurs identifiants pour les référencer, jamais pour les déclarer. `key` sur
[`defineView()`](/l/fr/developers/extend/apps/layout/views) est obsolète et ignoré, de sorte qu’une vue de manifeste ne peut jamais revendiquer la clé `INDEX`, et le serveur provisionne déjà une colonne pour chaque champ que vous ajoutez, de sorte que déclarer votre propre
`defineViewField()` pour ce même champ sur une vue système entre en conflit avec celle‑ci.
</Warning>
## Objets standard de Twenty
Pour un objet Twenty **standard** (Person, Company, Opportunity, …), vous n’avez rien à dériver : les identifiants sont des constantes pré‑calculées que vous pouvez importer directement, à la fois pour les champs et pour les vues.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
Utilisez les fonctions utilitaires ci‑dessus lorsque l’objet est l’un de ceux que **votre application** définit avec [`defineObject()`](/l/fr/developers/extend/apps/data/objects), et pour lesquels il n’existe pas une telle constante.
<Note>
`name` est un champ **par défaut**, pas un champ système. Il conserve son propre identifiant universel codé en dur et n'est pas résolu via
`getFieldUniversalIdentifier`. Sur les objets que vous définissez, référencez le champ
`name` avec l'identifiant que vous lui avez donné dans `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Types d'activités de chronologie
description: Définissez les événements d'audit automatique et les événements explicites de l'application qui s'affichent dans les calendriers d'enregistrement.
icon: clock-rotate-left
---
Un type d'activité de la timeline définit le vocabulaire stable pour un événement affiché dans la timeline d'un enregistrement. Les objets standards et les objets d'application utilisent le même contrat : un type a une étiquette et une icône, peut éventuellement déclarer lorsqu'il est émis, et peut éventuellement être rendu à travers l'un des [composants frontaux] de votre application (/developers/extend/apps/layout/front-components).
<Note>
Les types d'activité de la chronologie sont en bêta et arrivent bientôt dans Vingt 2.34. L'API
peut évoluer pendant que nous apprenons des cas d'utilisation des développeurs d'applications.
</Note>
En créer un avec l'échafaudage :
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
Ou définissez-le directement :
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## Événements automatiques et explicites
Ajoute `emit` quand Vingt devrait créer ce type automatiquement. `emit.on` supporte `created`, `updated`, `deleted`, `restored`, `linked`, et `unlinked`, tandis que `emit.objectUniversalIdentifier` identifie l'objet source. Un seul type efficace peut gérer la même clé d'émission dans un espace de travail : `on`, object et optionnel via une relation.
Sans `emit.through`, l'événement est écrit sur la timeline de l'enregistrement source. Pour l'encourager aux enregistrements liés, définissez `emit.through. elationFieldUniversalIdentifier` à une relation directe de plusieurs à un ou à une relation de jonction [un à plusieurs] (/developers/extend/apps/data/relations#junction-relations) sur l'objet source. Les relations de morphe se répandent vers chaque membre de leur groupe de morphes, donc une seule déclaration peut cibler plusieurs types d'objets.
Pour une relation directe, créer ou restaurer l'enregistrement source produit `linked`, supprimer cela produit `unlinked`, et repointant la relation produit `unlinked` sur la cible précédente plus `linked` sur la nouvelle cible. Les autres mises à jour de sources ne produisent pas d'événements de liens. Il s'agit du contrat utilisé par les pièces jointes, dont la cible est une relation de morphe directe.
Pour une relation de jonction, `universalSettings.junctionTargetFieldUniversalIdentifier` identifie la relation de l'objet de la jonction à la cible. Créer ou supprimer une ligne de jonction produit `linked` ou `unlinked`. Repointer de chaque côté d'une ligne de jonction produit également un événement de lien; les mises à jour des champs de jonction non liés ne le font pas.
Les événements `linked` et `unlinked` nécessitent `emit.through`, car leur déclenchement est un changement de la relation directe ou de la jonction configurée.
Par exemple, il s'agit du même contrat générique utilisé par les notes, les tâches, les messages et les événements du calendrier :
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
Une `mise à jour` à travers un événement est émise pour chaque mise à jour source par défaut. Définissez `emit.through.triggerFieldUniversalIdentifiers` lorsque seuls les changements aux champs source sélectionnés devraient apparaître sur les fils cibles.
Omettre `emit` pour un événement de domaine explicite que votre fonction logique se crée. Cela évite de produire à la fois une ligne d'audit automatique et une ligne explicite pour la même opération.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
Créez des événements explicites avec [`createTimelineActivity()`](/l/fr/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). Le code d'application utilise des identifiants universels stables ; vingt résolvent les identifiants de métadonnées spécifiques à l'installation.
## Rendu personnalisé
Sans composant frontal, Twenty affiche une ligne générique native à partir de l'étiquette de type, de l'icône et des métadonnées de l'objet lié. Cela fonctionne pour les objets standards et personnalisés sans moteur de rendu spécifique à l'objet.
Pour des détails personnalisés, définissez `frontComponentUniversalIdentifier` à un composant frontal appartenant à la même application:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
La ligne native reste la présentation réduite. Vingt montent le composant avant seulement après que l'utilisateur ait agrandi cette rangée, en évitant un bac à sable et un travailleur pour chaque événement visible. À l'intérieur du composant, appelez `useTimelineActivityId()` à partir de `vingt-sdk/front-component` pour lire l'ID de ligne et récupérer toutes les données dont votre présentation a besoin. Il retourne `null` lorsque le composant est rendu en dehors d'une ligne de timeline.
## Remplacer la présentation d'une autre application
Une application possède des contrats automatiques de timeline pour ses propres objets. Pour personnaliser un événement sur un objet appartenant à une autre application, déclarer explicitement le type existant avec `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Une substitution doit inclure `emit`, car elle remplace un emplacement d'émission automatique existant plutôt qu'un type explicite. Le type référencé doit appartenir à l'application de l'objet cible et décrire la même action et la même route. La suppression de l'application écrasante restaure le type de base ; la suppression du type de base désactive la substitution.
## Outrepasser et rendre muet l'espace de travail
Les administrateurs de l'espace de travail peuvent modifier la présentation d'un type ou couper en sourdine ses événements automatiques et explicites sans avoir à creuser l'application. Utilisez la mutation `updateTimelineActivityType` de l'API de métadonnées avec l'ID de type spécifique à l'installation :
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label` et `icon` sont stockés en tant que substituts d'espace de travail, donc les mises à jour ultérieures des applications n'écrasent pas les choix de l'administrateur. Définissez `isActive` à `false` pour arrêter l'émission automatique et rejeter les nouveaux événements explicites de ce type. Les lignes existantes restent visibles.
Restaurer les valeurs par défaut de l'application, y compris l'état actif, avec:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Champs de configuration
| Champ | Obligatoire | Description |
| ------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------ |
| `universalIdentifier` | Oui | UUID stable pour le type d'installations et de mises à jour |
| `name` | Oui | Nom programmatique local à l'application ; doit être unique au sein de l'application |
| `label` | Oui | Texte d'action utilisé par le moteur de rendu natif |
| `icon` | Non | Vingt icônes affichées à côté de l'événement |
| `émettre` | Non | Déclaration d'émission automatique ; omet pour les types explicites |
| `emit.on` | Avec émission | Action d'audit qui permet d'écrire ce type |
| `emit.objectUniversalIdentifier` | Avec émission | Objet dont les enregistrements émettent ce type |
| `emit.through.relationFieldUniversalIdentifier` | Non | Relation directe ou de jonction utilisée pour alimenter les fils connexes |
| `emit.through.triggerFieldUniversalIdentifiers` | Non | Champs source qui peuvent déclencher une `mise à jour` à travers l'événement |
| `frontComponentUniversalIdentifier` | Non | Composant frontal appartenant à une application monté lorsque la ligne est étendue |
| `replacesTimelineActivityTypeUniversalIdentifier` | Non | Type d'émission existant remplacé; nécessite `emit` |
Vingt instantanés instantanés d'identité sémantique et présentation durable se replient quand un événement est créé. Les lignes historiques conservent leur action originale et leur signification d'objet. Pendant que le type est installé, son étiquette traduite en cours, son icône et son composant frontal sont rendus en direct. Après désinstallation, la clé de secours de l'instantané conserve la ligne lisible. La résolution utilise l'identifiant universel, donc la présentation survit également à la réinstallation de l'application.