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: Estendere gli oggetti
description: Aggiungi campi agli oggetti standard di Twenty (Person, Company, …) o agli oggetti di altre app usando defineField.
icon: wand-magic-sparkles
---
Usa `defineField()` per aggiungere un campo a un oggetto che non possiedi — un oggetto standard di Twenty come Person o Company, oppure un oggetto fornito da un'altra app installata. A differenza dei campi inline dichiarati all'interno di [`defineObject`](/l/it/developers/extend/apps/data/objects), i campi autonomi richiedono un `objectUniversalIdentifier` per specificare quale oggetto estendono.
```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' },
],
});
```
## Punti chiave
* `objectUniversalIdentifier` identifica l'oggetto di destinazione. Per gli oggetti standard di Twenty, importa la costante da `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
// …
```
* Quando definisci campi **inline dentro `defineObject()`**, **non** hai bisogno di `objectUniversalIdentifier` — viene ereditato dall'oggetto padre.
* `defineField()` è l'unico modo per aggiungere campi a oggetti che non hai creato con `defineObject()`.
* Il percorso del file dipende da te. La convenzione è `src/fields/\<name>.field.ts`, ma l'SDK rileva i campi ovunque in `src/`.
* La `writability` può anche essere impostata per singolo campo: `MetadataWritability.APPLICATION` limita le scritture alle sole funzioni di logica della tua app, mentre `MetadataWritability.SYSTEM` è riservata ai dati gestiti dalla piattaforma. La writability di un campo può essere solo più restrittiva rispetto al livello del relativo oggetto e le letture non sono influenzate. Consulta [Objects](/l/it/developers/extend/apps/data/objects) per i dettagli completi sulla semantica.
* Per aggiungere una scheda a un layout di pagina standard (ad es. la pagina di dettaglio di Task o Company), utilizza [`definePageLayoutTab`](/l/it/developers/extend/apps/layout/page-layouts#definepagelayouttab) con `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` da `twenty-sdk/define`.
## Aggiungere una relazione a un oggetto esistente
Per aggiungere un campo di relazione (ad esempio collegare il tuo oggetto personalizzato a un `Person` standard), usa `defineField()` con `FieldType.RELATION`. Lo schema è lo stesso delle relazioni inline, ma con `objectUniversalIdentifier` impostato esplicitamente. Vedi [Relazioni](/l/it/developers/extend/apps/data/relations) per lo schema bidirezionale.

View File

@@ -0,0 +1,143 @@
---
title: Oggetti
description: Dichiara nuovi tipi di record — tabelle personalizzate con i propri campi — usando defineObject.
icon: table
---
Gli **oggetti** personalizzati sono nuovi tipi di record che la tua app aggiunge a uno spazio di lavoro — Cartolina, Fattura, Abbonamento, qualsiasi cosa specifica per il tuo dominio. Ogni oggetto dichiara il proprio schema (campi, relazioni, valori predefiniti) e un identificatore universale stabile che persiste tra le sincronizzazioni e le distribuzioni.
```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,
},
],
});
```
## Punti chiave
* Il `universalIdentifier` deve essere univoco e stabile tra i deployment.
* Ogni campo richiede un `name`, `type`, `label` e il proprio `universalIdentifier` stabile.
* L'array `fields` è facoltativo: puoi definire oggetti senza campi personalizzati.
* `openRecordIn` imposta dove si aprono i record di questo oggetto quando vengono cliccati: `ObjectOpenRecordIn.USER_CHOICE` (l'impostazione predefinita, che segue la preferenza di ciascun membro dell'area di lavoro da Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` oppure `ObjectOpenRecordIn.RECORD_PAGE`. Fissalo su `RECORD_PAGE` per i record che devono occupare un'intera pagina per essere usabili, come avviene per i workflow e le dashboard, oppure su `SIDE_PANEL` per i record che hanno senso solo come pannello rapido, come avviene per gli eventi di calendario.
* `writability` controlla chi può scrivere i record dell'oggetto in generale, prima che si applichino le autorizzazioni basate sui ruoli: `MetadataWritability.OPEN` (impostazione predefinita — i ruoli dello spazio di lavoro decidono), `MetadataWritability.APPLICATION` (solo le funzioni logiche della tua app possono creare, aggiornare o eliminare record; usa questa impostazione per oggetti simili a configurazioni i cui record determinano il comportamento, in modo che i membri dello spazio di lavoro con un ampio accesso ai record non possano modificarli tramite l'API) oppure `MetadataWritability.SYSTEM` (riservato ai dati gestiti dalla piattaforma). Le letture non sono influenzate — questo è applicato lato server, diversamente da `isUIEditable`, che si limita a nascondere gli elementi dell'interfaccia utente. Esiste anche per singolo campo, dove può essere soltanto più restrittivo rispetto al livello dell'oggetto.
* I campi inline definiti qui **non** hanno bisogno di un `objectUniversalIdentifier` — viene ereditato dall'oggetto padre. Usa [`defineField()`](/l/it/developers/extend/apps/data/extending-objects) per aggiungere campi a oggetti che non possiedi.
* Puoi generare nuovi oggetti con `yarn twenty dev:add object`, che ti guida nella denominazione, nei campi e nelle relazioni. Vedi [Architettura → Scaffolding delle entità](/l/it/developers/extend/apps/getting-started/scaffolding).
<Note>
**I campi base vengono aggiunti automaticamente.** Quando definisci un oggetto personalizzato, Twenty crea per te campi standard come `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt`. Non è necessario dichiararli nel tuo array `fields` — solo i tuoi campi personalizzati. Puoi sovrascrivere un campo predefinito dichiarandone uno con lo stesso nome, ma è raramente una buona idea.
</Note>
## Tipi di campo
L’insieme completo dei valori di `FieldType`, esportati da `twenty-sdk/define`:
| Categoria | Tipi |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Testo | `TEXT`, `RICH_TEXT`, `ARRAY` (di stringhe), `RAW_JSON` |
| Numerico | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (precisione arbitraria), `RATING`, `POSITION` |
| Date | `DATE`, `DATE_TIME` |
| Scelta | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Compositi | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identificatori e relazioni | `UUID`, `RELATION`, `MORPH_RELATION` (vedi [Relazioni](/l/it/developers/extend/apps/data/relations)) |
| Sistema | `TS_VECTOR` (vettore per la ricerca full-text, gestito dal server) |
I tipi compositi memorizzano più sotto-campi (ad es. `FULL_NAME` = nome + cognome; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` e `MULTI_SELECT` richiedono un array `options` come nell’esempio sopra.
## Valori predefiniti
I valori predefiniti letterali devono essere racchiusi tra apici singoli **all'interno** della stringa — `defaultValue: "'Draft'"`, non `defaultValue: "Draft"`. Ecco perché il campo `status` sopra utilizza `` `'${PostCardStatus.DRAFT}'` ``.
Le stringhe senza virgolette sono riservate ai valori predefiniti calcolati, valutati quando viene creato un record:
* `'uuid'` — genera un UUID (per i campi `UUID`)
* `'now'` — il timestamp corrente (per i campi `DATE_TIME`)
La stessa convenzione si applica ai sotto-campi stringa dei valori compositi predefiniti (ad es. `{ source: "'MANUAL'" }` su un campo `ACTOR`) e ai valori `SELECT`/`MULTI_SELECT`. Una stringa letterale predefinita lasciata senza virgolette genera un avviso quando la tua app viene compilata.
## Nullabilità
`isNullable` controlla se un campo accetta `NULL`. Il valore predefinito è `true` — omettilo per i campi facoltativi. Imposta `isNullable: false` per rendere un campo obbligatorio a livello di database.
Le modifiche a `isNullable` vengono applicate a ogni sincronizzazione, incluse quelle che aggiornano un campo esistente — quindi puoi cambiare la nullabilità di un campo modificando il manifest e rieseguendo la sincronizzazione.
<Note>
**Rendere un campo esistente che non ammette `NULL` richiede un valore predefinito.** Quando imposti `isNullable: false` per un campo, devi anche fornire un `defaultValue` non nullo. Il valore predefinito esegue il backfill di tutte le righe `NULL` esistenti prima che venga applicato il vincolo `NOT NULL`; senza di esso la sincronizzazione non riesce con l'errore `Default value cannot be null for non-nullable fields`. I campi di relazione e i campi `TS_VECTOR` ammettono sempre `NULL`, quindi `isNullable` non ha alcun effetto su di essi.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## Cosa c'è dopo
* **Collega questo oggetto ad altri** — vedi [Relazioni](/l/it/developers/extend/apps/data/relations) per il pattern di relazione bidirezionale.
* **Aggiungi campi a oggetti di altre app** — vedi [Estendere gli oggetti](/l/it/developers/extend/apps/data/extending-objects) per `defineField()`.
* **Visualizza questo oggetto nell'interfaccia utente (UI)** — vedi [Voci del menu di navigazione](/l/it/developers/extend/apps/layout/navigation-menu-items) per aggiungere una voce nella barra laterale; vedi [Viste](/l/it/developers/extend/apps/layout/views) per aggiungere configurazioni di elenco personalizzate.

View File

@@ -0,0 +1,101 @@
---
title: Panoramica
description: Definisci i dati che la tua app aggiunge a uno spazio di lavoro — oggetti, campi e relazioni.
icon: database
---
Il **data layer** di un'app Twenty è l'insieme dei dati che la tua app *aggiunge* a uno spazio di lavoro — i nuovi tipi di record che dichiara, le colonne che aggiunge agli oggetti esistenti e come questi record si collegano tra loro.
```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 │
└──────────────────────────────────────────────────┘
```
## In questa sezione
<CardGroup cols={2}>
<Card title="Oggetti" icon="table" href="/l/it/developers/extend/apps/data/objects">
`defineObject` — dichiara nuovi tipi di record con i propri campi.
</Card>
<Card title="Estendere gli oggetti" icon="wand-magic-sparkles" href="/l/it/developers/extend/apps/data/extending-objects">
`defineField` — aggiunge campi agli oggetti standard o di altre app.
</Card>
<Card title="Relazioni" icon="diagram-project" href="/l/it/developers/extend/apps/data/relations">
Connessioni bidirezionali `MANY_TO_ONE` / `ONE_TO_MANY` tra oggetti.
</Card>
<Card title="Tipi di attività della timeline" icon="clock-rotate-left" href="/l/it/developers/extend/apps/data/timeline-activity-types">
Definisci eventi di audit automatici ed eventi espliciti dell'app per le timeline dei record.
</Card>
</CardGroup>
## Entità in sintesi
| Entità | Scopo | Definito con |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| **Oggetto** | Un nuovo tipo di record personalizzato (ad es. PostCard, Invoice) con i propri campi | `defineObject()` |
| **Campo** | Una colonna su un oggetto. I campi autonomi possono estendere oggetti che non hai creato tu (ad es. aggiungere `loyaltyTier` a Company) | `defineField()` |
| **Relazione** | Un collegamento bidirezionale tra due oggetti — entrambi i lati sono dichiarati come campi | `defineField()` con `FieldType.RELATION` |
| **Indice** | Un indice di database per velocizzare una query ricorrente su uno dei tuoi oggetti | `defineIndex()` |
| **Tipo di attività della timeline** | Un evento di audit automatico o un evento di dominio esplicito visualizzato nelle timeline dei record | `defineTimelineActivityType()` |
L'SDK rileva questi elementi tramite analisi AST in fase di build, quindi l'organizzazione dei file dipende da te — la convenzione è `src/objects/`, `src/fields/`, `src/indexes/` e `src/timeline-activity-types/`. Gli UUID `universalIdentifier` stabili collegano tutto tra le varie distribuzioni.
## Indici (facoltativi)
Le app possono distribuire indici insieme ai loro oggetti per mantenere veloci le query ricorrenti. Il caso più comune è una colonna di stato o di chiave esterna che leggi spesso.
```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,
},
],
});
```
### Indici univoci
`defineIndex` accetta `isUnique: true` sia per l'univocità a singola colonna sia per quella multi-colonna. Questa è la primitiva consigliata — `defineField({ isUnique: true })` è deprecata e verrà rimossa in una futura release.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Altri vincoli
* Le clausole `WHERE` parziali restano sotto il controllo dell'amministratore — le app non possono dichiararle.
* Ogni oggetto è limitato a 10 indici personalizzati (gli indici propri del framework non contano).
Ordina l'array `fields` nel modo in cui Postgres dovrebbe usarlo — la colonna più a sinistra per prima, come in un elenco telefonico. Gli indici non sono gratuiti: ogni scrittura sulla tabella li aggiorna. Aggiungine uno solo quando hai una query che ne ha bisogno.
<Note>
Cerchi **Application Config** o **Roles & Permissions**? Quelli descrivono l'app in sé piuttosto che i dati che aggiunge — si trovano sotto [Config](/l/it/developers/extend/apps/config/overview). Cerchi **Connections** (Linear, GitHub, Slack OAuth)? Quelle esistono per essere chiamate *dalle* funzioni di logica e si trovano sotto [Logic](/l/it/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Relazioni
description: Collega gli oggetti tra loro con relazioni bidirezionali MANY_TO_ONE / ONE_TO_MANY.
icon: diagram-project
---
Le relazioni collegano due oggetti tra loro. In Twenty, le relazioni sono sempre **bidirezionali** — ogni relazione ha due lati e ciascun lato è dichiarato come un campo che fa riferimento all'altro.
| Tipo di relazione | Descrizione | Ha una chiave esterna? |
| ----------------- | --------------------------------------------------------------------- | ---------------------- |
| `MANY_TO_ONE` | Molti record di questo oggetto puntano a un record della destinazione | Sì (`joinColumnName`) |
| `ONE_TO_MANY` | Un record di questo oggetto ha molti record della destinazione | No (lato inverso) |
## Come funzionano le relazioni
Ogni relazione richiede **due campi** che fanno riferimento l'uno all'altro:
1. Il lato **MANY_TO_ONE** — risiede sull'oggetto che detiene la chiave esterna.
2. Il lato **ONE_TO_MANY** — risiede sull'oggetto che possiede la collezione.
Entrambi i campi usano `FieldType.RELATION` e si riferiscono reciprocamente tramite `relationTargetFieldMetadataUniversalIdentifier`.
## Esempio: Post Card ha molti destinatari
Un `PostCard` può essere inviato a molti record `PostCardRecipient`. Ogni destinatario appartiene esattamente a una sola cartolina.
**Passaggio 1: definisci il lato ONE_TO_MANY su PostCard** (il lato "uno"):
```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,
},
});
```
**Passaggio 2: definisci il lato MANY_TO_ONE su PostCardRecipient** (il lato "molti" — contiene la chiave esterna):
```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>
**Importazioni circolari:** entrambi i campi di relazione fanno riferimento all'`universalIdentifier` dell'altro. Per evitare problemi di importazioni circolari, esporta gli ID dei campi come costanti denominate da ciascun file e importale nell'altro. Il sistema di build le risolve in fase di compilazione.
</Note>
## Relazioni di giunzione
Un oggetto di giunzione collega un record di origine a uno o più record di destinazione. Ad esempio, `PostCardRecipient` può contenere sia una relazione `postCard` sia una relazione `recipient`. Nel campo `ONE_TO_MANY` dell'oggetto di origine, imposta `junctionTargetFieldUniversalIdentifier` sul campo di relazione dell'oggetto di giunzione che punta alla destinazione:
```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` deve identificare una relazione `MANY_TO_ONE` o morph nell'oggetto di giunzione. Indica a Twenty quale record correlato è la destinazione quando una funzionalità come la [distribuzione delle attività della timeline](/l/it/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) attraversa la giunzione.
## Relazioni con gli oggetti standard
Per creare una relazione con un oggetto Twenty integrato (Person, Company, ecc.), usa `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',
},
});
```
## Proprietà dei campi di relazione
| Proprietà | Obbligatorio | Descrizione |
| ---------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `type` | Sì | Deve essere `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` dell'oggetto di destinazione |
| `relationTargetFieldMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` del campo corrispondente sull'oggetto di destinazione |
| `universalSettings.relationType` | Sì | `RelationType.MANY_TO_ONE` o `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Solo MANY_TO_ONE | Cosa accade quando il record referenziato viene eliminato: `CASCADE`, `SET_NULL`, `RESTRICT` o `NO_ACTION` |
| `universalSettings.joinColumnName` | Solo MANY_TO_ONE | Nome della colonna del database per la chiave esterna (ad es., `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Solo ONE_TO_MANY di giunzione | Campo di relazione nell'oggetto di giunzione che punta alla destinazione della distribuzione |
## Campi di relazione inline
Puoi anche dichiarare una relazione direttamente all'interno di [`defineObject`](/l/it/developers/extend/apps/data/objects). Quando è inline, ometti `objectUniversalIdentifier` — viene ereditato dall'oggetto padre:
```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,211 @@
---
title: Fare riferimento ai metadati di sistema
description: Risolvi gli identificatori universali deterministici dei metadati che Twenty predispone automaticamente su ogni oggetto, così la tua app può farvi riferimento senza doverli codificare in modo statico.
icon: gears
---
Ogni oggetto in Twenty include **metadati di sistema** che non dichiari mai direttamente, come ad esempio un insieme di campi e una vista elenco principale con le sue colonne. Il server crea tutto questo quando l'oggetto viene predisposto e l'insieme cresce man mano che Twenty cresce.
Poiché non lo dichiari, non esiste alcuna costante `universalIdentifier` da importare. Invece, il server **deriva** ogni identificatore in modo deterministico e `twenty-sdk` espone la stessa derivazione affinché il tuo manifest possa risolvere il valore esatto utilizzato dal server.
## Campi di sistema
I campi scalari presenti su ogni oggetto, nessuno dei quali dichiari con [`defineField()`](/l/it/developers/extend/apps/data/extending-objects):
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Quindi, come fai a fare riferimento a `createdAt` come colonna in una [vista](/l/it/developers/extend/apps/layout/views)?
### Il problema
A partire da Twenty 2.19, l'identificatore universale di un campo di sistema viene derivato in modo deterministico dal server sulla base di tre input: l'identificatore universale dell'applicazione, l'identificatore universale dell'oggetto e il nome del campo. Inventare un id e hardcodarlo non funziona: non corrisponde a nulla sul server e la sincronizzazione rifiuta il riferimento orfano:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### La soluzione
<Note>
`getFieldUniversalIdentifier` è disponibile da `twenty-sdk` 2.21 in poi.
</Note>
Usa `getFieldUniversalIdentifier` per ottenere esattamente lo stesso valore utilizzato dal server. Accetta i tre input e restituisce l'identificatore universale del campo:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` è l'identificatore della tua app, quello che passi a [`defineApplication()`](/l/it/developers/extend/apps/config/application).
* `objectUniversalIdentifier` è l'identificatore dell'oggetto a cui il campo appartiene.
* `name` è il nome del campo di sistema, uno dei valori elencati sopra.
### Esempio: una colonna createdAt in una vista
Il caso tipico è aggiungere una colonna `createdAt` a una vista di uno dei tuoi oggetti personalizzati. Risolvi l'id del campo e usalo come qualsiasi altro `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,
},
],
});
```
Lo stesso id risolto funziona ovunque ci si aspetti un `fieldMetadataUniversalIdentifier`: campi di vista, filtri, ordinamenti, raggruppamenti e widget di layout di pagina.
<Note>
Risolvi l'id, non hardcodarlo. Poiché il server deriva il valore
dall'id dell'applicazione, dall'id dell'oggetto e dal nome del campo, chiamare
`getFieldUniversalIdentifier` mantiene corretto il riferimento anche se questi
input cambiano ed evita discrepanze se la modalità di derivazione dovesse mai evolvere.
</Note>
### Campi di relazione di sistema
<Note>
`getSystemRelationFieldUniversalIdentifier` è disponibile da `twenty-sdk`
2.23 in poi e richiede un server Twenty dalla versione 2.23 o successiva.
</Note>
Oltre ai campi scalari di sistema sopra indicati, il server fornisce anche quattro **campi di relazione di sistema** su ogni oggetto: `timelineActivities`, `attachments`, `noteTargets` e `taskTargets`, ciascuno dei quali punta al corrispondente oggetto di relazione standard.
Questi campi non vengono risolti con `getFieldUniversalIdentifier`: il loro identificatore è derivato **in modo indipendente dal nome**, dall'oggetto che ospita il campo e dall'oggetto a cui il campo punta. In questo modo, rinominare un oggetto non modifica mai gli identificatori dei suoi campi di relazione.
Usa `getSystemRelationFieldUniversalIdentifier` per risolverli:
```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` è l'oggetto che **ospita** il campo.
* `relationTargetObjectUniversalIdentifier` è l'oggetto a cui il campo **punta**.
La direzione è codificata dall'ordine degli argomenti. Per risolvere il lato inverso (ad esempio `attachment.targetRocket`, il campo morph che il server crea sull'oggetto di relazione standard), scambia i due:
```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,
});
```
Come per i campi di sistema scalari, l'id risolto funziona ovunque ci si aspetti un `fieldMetadataUniversalIdentifier`.
## Viste di sistema
<Note>
`getSystemViewUniversalIdentifier` e `getSystemViewFieldUniversalIdentifier`
sono disponibili da `twenty-sdk` 2.26 in poi e richiedono un server Twenty
dalla versione 2.26 o successiva.
</Note>
Il server predispone anche una **vista di sistema** su ogni oggetto: la vista elenco principale (`All {objectLabelPlural}`, con chiave `ViewKey.INDEX` (derivata con `SYSTEM_VIEW_KEYS.INDEX`)), con una colonna per ogni campo visualizzabile. Come per i campi di relazione di sistema, i loro identificatori sono derivati senza dipendere dai nomi, quindi rinominare un oggetto o un campo non li modifica mai.
Usa `getSystemViewUniversalIdentifier` per risolvere la vista:
```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` è l’applicazione proprietaria dell’**oggetto**, che è ciò in base a cui viene definito lo spazio dei nomi della vista.
* `objectUniversalIdentifier` è l’oggetto che la vista elenca.
* `viewKey` è il discriminatore della vista di sistema: `SYSTEM_VIEW_KEYS.INDEX` per la vista elenco principale, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` per la vista del widget dei campi della pagina del record. Determina la derivazione; solo `INDEX` è anche memorizzato sulla riga della vista.
L’id risolto funziona ovunque ci si aspetti un `viewUniversalIdentifier`, ad esempio in una voce della sidebar [`NavigationMenuItemType.VIEW`](/l/it/developers/extend/apps/layout/navigation-menu-items). Per aprire semplicemente la lista principale di un oggetto, è preferibile usare `NavigationMenuItemType.OBJECT` con `targetObjectUniversalIdentifier`: non richiede alcuna derivazione.
`getSystemViewFieldUniversalIdentifier` risolve una singola **colonna** su una vista di sistema, a partire dalla vista e dal campo che visualizza:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Nota il primo argomento: una colonna appartiene allo spazio dei nomi dell’applicazione proprietaria del **campo che visualizza**, non di quella proprietaria della vista. Un campo che la tua app aggiunge a un oggetto standard ottiene la propria colonna derivata sotto la tua applicazione, su una vista di proprietà di Twenty.
<Warning>
Le viste di sistema e le loro colonne sono **di proprietà del server**: risolvi i loro identificatori per fare riferimento ad esse, mai per dichiararle. `key` su
[`defineView()`](/l/it/developers/extend/apps/layout/views) è deprecato e
ignorato, quindi una vista del manifest non può mai rivendicare la chiave `INDEX`, e il server predispone già una colonna per ogni campo che aggiungi, pertanto dichiarare un tuo
`defineViewField()` per quello stesso campo su una vista di sistema entra in conflitto con essa.
</Warning>
## Oggetti standard di Twenty
Per un oggetto **standard** di Twenty (Person, Company, Opportunity, …), non hai bisogno di derivare nulla: gli identificatori sono costanti pre-calcolate che puoi importare direttamente, sia per i campi che per le viste.
```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
```
Usa gli helper sopra quando l’oggetto è uno che **la tua app** definisce con [`defineObject()`](/l/it/developers/extend/apps/data/objects), per cui non esiste alcuna costante di questo tipo.
<Note>
`name` è un campo **predefinito**, non un campo di sistema. Mantiene un proprio identificatore universale hardcoded e non viene risolto tramite
`getFieldUniversalIdentifier`. Sugli oggetti che definisci tu, fai riferimento al campo
`name` tramite l'identificatore che gli hai assegnato in `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Tipi Di Attività Timeline
description: Definire gli eventi di audit automatico e gli eventi espliciti delle app che vengono visualizzati in tempi record.
icon: clock-rotate-left
---
Un tipo di attività della timeline definisce il vocabolario stabile per un evento mostrato nella timeline di un record. Oggetti standard e oggetti app utilizzano lo stesso contratto: un tipo ha un'etichetta e un'icona, può opzionalmente dichiarare quando viene emesso, e può opzionalmente renderizzare attraverso uno dei [componenti anteriori](/l/it/developers/extend/apps/layout/front-components).
<Note>
I tipi di attività temporali sono in beta e stanno arrivando presto a Twenty 2.34. L'API
può evolvere mentre impariamo dai casi d'uso degli sviluppatori di app.
</Note>
Creane uno con il ponteggio:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
O definirlo direttamente:
```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,
},
});
```
## Eventi automatici ed espliciti
Aggiungi `emit` quando Twenty dovrebbe creare questo tipo automaticamente. `emit.on` supporta `created`, `updated`, `deleted`, `restored`, `linked`, e `unlinked`, mentre `emit.objectUniversalIdentifier` identifica l'oggetto sorgente. Solo un tipo efficace può gestire la stessa chiave di emissione — `on`, oggetto e opzionale attraverso la relazione — in uno spazio di lavoro.
Senza `emit.through`, l'evento è scritto sulla timeline del record di origine. Per adattarlo ai record correlati, imposta `emit.through. elationFieldUniversalIdentifier` o a una relazione diretta tra molti o a una [relazione di giunzione uno-a-molto](/l/it/developers/extend/apps/data/relations#junction-relations) sull'oggetto sorgente. Amante delle relazioni morph ad ogni membro del loro gruppo morph, in modo da una singola dichiarazione può colpire diversi tipi di oggetti.
Per una relazione diretta, la creazione o il ripristino del record di origine produce `linked`, eliminando produce `unlinked`, e ripuntando la relazione produce `unlinked` sul target precedente più `linked` sul nuovo obiettivo. Altri aggiornamenti di origine non producono eventi di collegamento. Questo è il contratto utilizzato dagli allegati, il cui obiettivo è una relazione diretta morfologica.
Per una relazione di giunzione, `universalSettings.junctionTargetFieldUniversalIdentifier` identifica la relazione dall'oggetto di giunzione al bersaglio. La creazione o l'eliminazione di una riga di giunzione produce `linked` o `unlinked`. Il riorientamento di entrambi i lati di una riga di giunzione produce anche un evento di collegamento; gli aggiornamenti di campi di giunzione non correlati non.
`linked` e `unlinked` gli eventi richiedono `emit.through`, perché il loro trigger è una modifica al rapporto diretto o di giunzione configurato.
Ad esempio, questo è lo stesso contratto generico utilizzato da note, compiti, messaggi e eventi di calendario:
```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,
},
},
});
```
Per impostazione predefinita, viene emesso un evento `updated` through per ogni aggiornamento della sorgente. Imposta `emit.through.triggerFieldUniversalIdentifiers` quando solo le modifiche ai campi sorgente selezionati devono apparire nelle timeline di destinazione.
Ometti `emit` per un evento di dominio esplicito creato dalla tua funzione logica. In questo modo si evita di produrre sia una riga di audit automatica sia una riga esplicita per la stessa operazione.
```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',
});
```
Crea eventi espliciti con [`createTimelineActivity()`](/l/it/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). Il codice dell'app usa identificatori universali stabili; Twenty risolve gli ID dei metadati specifici dell'installazione.
## Rendering personalizzato
Senza un componente front, Twenty esegue il rendering di una riga generica nativa a partire dall'etichetta del tipo, dall'icona e dai metadati dell'oggetto collegato. Questo funziona per oggetti standard e personalizzati senza un renderer specifico dell'oggetto.
Per dettagli personalizzati, imposta `frontComponentUniversalIdentifier` su un componente front appartenente alla stessa app:
```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 riga nativa rimane la presentazione compressa. Twenty monta il componente front solo dopo che l'utente espande quella riga, evitando una sandbox e un worker per ogni evento visibile. All'interno del componente, chiama `useTimelineActivityId()` da `twenty-sdk/front-component` per leggere l'ID della riga e recuperare tutti i dati necessari alla presentazione. Restituisce `null` quando il componente viene sottoposto a rendering al di fuori di una riga della timeline.
## Sovrascrivere la presentazione di un'altra applicazione
Un'app possiede contratti automatici della timeline per i propri oggetti. Per personalizzare un evento su un oggetto posseduto da un'altra app, dichiara esplicitamente il tipo esistente con `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',
});
```
Una sostituzione deve includere `emit`, perché sostituisce uno slot di emissione automatica esistente anziché un tipo solo esplicito. Il tipo a cui viene fatto riferimento deve appartenere all'applicazione dell'oggetto di destinazione e descrivere la stessa azione e lo stesso percorso. La rimozione dell'app che effettua la sostituzione ripristina il tipo di base; la rimozione del tipo di base disabilita la sostituzione.
## Sostituzioni e disattivazione dell'area di lavoro
Gli amministratori dell'area di lavoro possono modificare la presentazione di un tipo o disattivare i relativi eventi automatici ed espliciti senza effettuare un fork dell'applicazione. Usa la mutazione `updateTimelineActivityType` dell'API dei metadati con l'ID del tipo specifico dell'installazione:
```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` e `icon` vengono archiviati come sostituzioni dell'area di lavoro, pertanto gli aggiornamenti successivi dell'applicazione non sovrascrivono le scelte dell'amministratore. Imposta `isActive` su `false` per interrompere l'emissione automatica e rifiutare nuovi eventi espliciti di quel tipo. Le righe esistenti rimangono visibili.
Ripristina le impostazioni predefinite dell'applicazione, incluso lo stato attivo, con:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Campi di configurazione
| Campo | Obbligatorio | Descrizione |
| ------------------------------------------------- | ------------ | ------------------------------------------------------------------------------- |
| `universalIdentifier` | Sì | UUID stabile per il tipo tra installazioni e aggiornamenti |
| `name` | Sì | Nome programmatico locale dell'app; deve essere univoco all'interno dell'app |
| `label` | Sì | Testo dell'azione rivolto all'utente usato dal renderer nativo |
| `icon` | No | Nome dell'icona Twenty visualizzata accanto all'evento |
| `emit` | No | Dichiarazione di emissione automatica; ometti per i tipi solo espliciti |
| `emit.on` | Con emit | Azione di audit che causa la scrittura di questo tipo |
| `emit.objectUniversalIdentifier` | Con emit | Oggetto i cui record emettono questo tipo |
| `emit.through.relationFieldUniversalIdentifier` | No | Relazione diretta o di giunzione utilizzata per ventilare le timeline correlate |
| `emit.through.triggerFieldUniversalIdentifiers` | No | Campi sorgente che possono attivare un evento `updated` through |
| `frontComponentUniversalIdentifier` | No | Componente anteriore di proprietà dell'app montato quando la riga è espansa |
| `replacesTimelineActivityTypeUniversalIdentifier` | No | Tipo di emissione esistente sostituito; richiede `emit` |
Venti istantanee identità semantica e fallback durevoli della presentazione quando viene creato un evento. Le righe storiche mantengono la loro azione originale e il significato dell'oggetto. Mentre il tipo è installato, la sua attuale etichetta, icona e componente frontale rendono live; dopo la disinstallazione, lo snapshot fallback mantiene la riga leggibile. La risoluzione utilizza l'identificatore universale, quindi la presentazione sopravvive anche reinstallando l'app.