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: Extender objetos
description: Añade campos a los objetos estándar de Twenty (Person, Company, …) o a objetos de otras aplicaciones usando defineField.
icon: wand-magic-sparkles
---
Usa `defineField()` para añadir un campo a un objeto que no te pertenece — un objeto estándar de Twenty como Person o Company, u otro objeto proporcionado por otra aplicación instalada. A diferencia de los campos en línea declarados dentro de [`defineObject`](/l/es/developers/extend/apps/data/objects), los campos independientes requieren un `objectUniversalIdentifier` para especificar qué objeto extienden.
```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' },
],
});
```
## Puntos clave
* `objectUniversalIdentifier` identifica el objeto de destino. Para los objetos estándar de Twenty, importa la constante desde `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
// …
```
* Al definir campos **en línea dentro de `defineObject()`**, **no** necesitas `objectUniversalIdentifier` — se hereda del objeto padre.
* `defineField()` es la única forma de añadir campos a objetos que no creaste con `defineObject()`.
* La ubicación del archivo depende de ti. La convención es `src/fields/\<name>.field.ts`, pero el SDK detecta campos en cualquier lugar de `src/`.
* La `writability` también se puede configurar por campo: `MetadataWritability.APPLICATION` restringe las escrituras a las funciones lógicas de tu propia app, `MetadataWritability.SYSTEM` está reservado para los datos gestionados por la plataforma. La capacidad de escritura de un campo solo puede ser más estricta que el nivel de su objeto, y las lecturas no se ven afectadas. Consulta [Objetos](/l/es/developers/extend/apps/data/objects) para ver la semántica completa.
* Para agregar una pestaña a un diseño de página estándar (por ejemplo, la página de detalles de la Tarea o de la Empresa), usa [`definePageLayoutTab`](/l/es/developers/extend/apps/layout/page-layouts#definepagelayouttab) con `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` de `twenty-sdk/define`.
## Añadir una relación a un objeto existente
Para añadir un campo de relación (por ejemplo, vinculando tu objeto personalizado a un `Person` estándar), usa `defineField()` con `FieldType.RELATION`. El patrón es el mismo que para las relaciones en línea, pero con `objectUniversalIdentifier` establecido explícitamente. Consulta [Relaciones](/l/es/developers/extend/apps/data/relations) para conocer el patrón bidireccional.

View File

@@ -0,0 +1,143 @@
---
title: Objetos
description: Declara nuevos tipos de registro (tablas personalizadas con sus propios campos) usando defineObject.
icon: table
---
Los **objetos** personalizados son nuevos tipos de registro que tu aplicación añade a un espacio de trabajo — Tarjeta postal, Factura, Suscripción, cualquier cosa específica de tu dominio. Cada objeto declara su esquema (campos, relaciones, valores predeterminados) y un identificador universal estable que se mantiene a través de sincronizaciones e implementaciones.
```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,
},
],
});
```
## Puntos clave
* El `universalIdentifier` debe ser único y estable entre implementaciones.
* Cada campo requiere `name`, `type`, `label` y su propio `universalIdentifier` estable.
* La matriz `fields` es opcional: puedes definir objetos sin campos personalizados.
* `openRecordIn` establece dónde se abren los registros de este objeto al hacer clic: `ObjectOpenRecordIn.USER_CHOICE` (el valor predeterminado, que sigue la preferencia de cada miembro del espacio de trabajo en Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` o `ObjectOpenRecordIn.RECORD_PAGE`. Fíjalo en `RECORD_PAGE` para los registros que necesitan una página completa para ser utilizables, como ocurre con los flujos de trabajo y los paneles, o en `SIDE_PANEL` para los registros que solo tienen sentido como un panel rápido, como los eventos del calendario.
* `writability` controla quién puede escribir registros del objeto en general, antes de que se apliquen los permisos por rol: `MetadataWritability.OPEN` (predeterminado — lo deciden los roles del espacio de trabajo), `MetadataWritability.APPLICATION` (solo las funciones de lógica de tu aplicación pueden crear, actualizar o eliminar registros; usa esto para objetos de configuración cuyos registros definen el comportamiento, de modo que los miembros del espacio de trabajo con amplio acceso a registros no puedan editarlos a través de la API) o `MetadataWritability.SYSTEM` (reservado para datos gestionados por la plataforma). Las lecturas no se ven afectadas: esto se aplica en el lado del servidor, a diferencia de `isUIEditable`, que solo oculta las funcionalidades de la interfaz de usuario. También existe por campo, donde solo puede ser más restrictivo que el nivel del objeto.
* Los campos en línea definidos aquí **no** necesitan un `objectUniversalIdentifier`, ya que se hereda del objeto padre. Usa [`defineField()`](/l/es/developers/extend/apps/data/extending-objects) para añadir campos a objetos que no te pertenecen.
* Puedes generar nuevos objetos con `yarn twenty dev:add object`, que te guía en la asignación de nombres, los campos y las relaciones. Consulta [Arquitectura → Generación de entidades](/l/es/developers/extend/apps/getting-started/scaffolding).
<Note>
**Los campos base se añaden automáticamente.** Cuando defines un objeto personalizado, Twenty crea campos estándar como `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` y `deletedAt` por ti. No necesitas declararlos en tu matriz `fields`, solo tus campos personalizados. Puedes sobrescribir un campo predeterminado declarando uno con el mismo nombre, pero esto rara vez es una buena idea.
</Note>
## Tipos de campo
El conjunto completo de valores de `FieldType`, exportados desde `twenty-sdk/define`:
| Categoría | Tipos |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Texto | `TEXT`, `RICH_TEXT`, `ARRAY` (de cadenas), `RAW_JSON` |
| Numérico | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (precisión arbitraria), `RATING`, `POSITION` |
| Fechas | `DATE`, `DATE_TIME` |
| Selección | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Compuesto | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identificadores y relaciones | `UUID`, `RELATION`, `MORPH_RELATION` (ver [Relations](/l/es/developers/extend/apps/data/relations)) |
| Sistema | `TS_VECTOR` (vector de búsqueda de texto completo, gestionado por el servidor) |
Los tipos compuestos almacenan múltiples subcampos (por ejemplo, `FULL_NAME` = nombre + apellido; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` y `MULTI_SELECT` requieren un arreglo `options` como en el ejemplo anterior.
## Valores predeterminados
Los valores predeterminados de cadenas literales deben ir entre comillas simples **dentro** de la cadena — `defaultValue: "'Draft'"`, no `defaultValue: "Draft"`. Por eso el campo `status` anterior utiliza `` `'${PostCardStatus.DRAFT}'` ``.
Las cadenas sin comillas se reservan para valores predeterminados calculados, evaluados cuando se crea un registro:
* `'uuid'` — genera un UUID (para campos `UUID`)
* `'now'` — la marca de tiempo actual (para campos `DATE_TIME`)
La misma convención se aplica a los subcampos de tipo cadena de los valores predeterminados compuestos (por ejemplo, `{ source: "'MANUAL'" }` en un campo `ACTOR`) y a los valores de `SELECT`/`MULTI_SELECT`. Un valor predeterminado de tipo cadena literal dejado sin comillas genera una advertencia cuando se compila tu aplicación.
## Nulabilidad
`isNullable` controla si un campo acepta `NULL`. Su valor predeterminado es `true` — omítelo para campos opcionales. Configura `isNullable: false` para hacer que un campo sea obligatorio a nivel de base de datos.
Los cambios en `isNullable` se aplican en cada sincronización, incluidas las sincronizaciones que actualizan un campo existente; de este modo, puedes cambiar la nulabilidad de un campo editando el manifiesto y sincronizando de nuevo.
<Note>
**Hacer que un campo existente no acepte valores nulos requiere un valor predeterminado.** Cuando cambies un campo a `isNullable: false`, también debes proporcionar un `defaultValue` no nulo. El valor predeterminado rellena cualquier fila `NULL` existente antes de que se aplique la restricción `NOT NULL`; sin él, la sincronización falla con `Default value cannot be null for non-nullable fields`. Los campos de relación y los campos `TS_VECTOR` siempre aceptan valores nulos, por lo que `isNullable` no tiene ningún efecto en ellos.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## ¿Qué sigue?
* **Conecta este objeto con otros**: consulta [Relaciones](/l/es/developers/extend/apps/data/relations) para el patrón de relación bidireccional.
* **Añade campos a objetos de otras aplicaciones**: consulta [Extender objetos](/l/es/developers/extend/apps/data/extending-objects) para `defineField()`.
* **Mostrar este objeto en la IU** — consulta [Elementos del menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items) para añadir una entrada en la barra lateral; consulta [Vistas](/l/es/developers/extend/apps/layout/views) para añadir configuraciones de lista personalizadas.

View File

@@ -0,0 +1,101 @@
---
title: Resumen
description: "Da forma a los datos que tu aplicación agrega a un espacio de trabajo: objetos, campos y relaciones."
icon: database
---
La **capa de datos** de una aplicación de Twenty es el conjunto de datos que tu aplicación *agrega* a un espacio de trabajo — los nuevos tipos de registros que declara, las columnas que agrega a los objetos existentes y cómo esos registros se conectan entre sí.
```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 │
└──────────────────────────────────────────────────┘
```
## En esta sección
<CardGroup cols={2}>
<Card title="Objetos" icon="table" href="/l/es/developers/extend/apps/data/objects">
`defineObject` — declara nuevos tipos de registros con sus propios campos.
</Card>
<Card title="Extender objetos" icon="wand-magic-sparkles" href="/l/es/developers/extend/apps/data/extending-objects">
`defineField` — agrega campos a objetos estándar o de otras aplicaciones.
</Card>
<Card title="Relaciones" icon="diagram-project" href="/l/es/developers/extend/apps/data/relations">
Conexiones bidireccionales `MANY_TO_ONE` / `ONE_TO_MANY` entre objetos.
</Card>
<Card title="Tipos de actividad de la cronología" icon="clock-rotate-left" href="/l/es/developers/extend/apps/data/timeline-activity-types">
Defina eventos de auditoría automáticos y eventos explícitos de la aplicación para cronologías de registros.
</Card>
</CardGroup>
## Entidades de un vistazo
| Entidad | Propósito | Definido con |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| **Objeto** | Un nuevo tipo de registro personalizado (por ejemplo, PostCard, Invoice) con sus propios campos | `defineObject()` |
| **Campo** | Una columna en un objeto. Los campos independientes pueden ampliar objetos que no creaste (por ejemplo, agregar `loyaltyTier` a Company) | `defineField()` |
| **Relación** | Un vínculo bidireccional entre dos objetos: ambos lados se declaran como campos | `defineField()` con `FieldType.RELATION` |
| **Índice** | Un índice de base de datos para acelerar una consulta recurrente sobre uno de tus objetos | `defineIndex()` |
| **Tipo de actividad de la cronología** | Un evento de auditoría automático o un evento de dominio explícito mostrado en las líneas de tiempo de los registros. | `defineTimelineActivityType()` |
El SDK detecta estos mediante análisis AST en tiempo de compilación, por lo que la organización de archivos depende de ti; la convención es `src/objects/`, `src/fields/`, `src/indexes/` y `src/timeline-activity-types/`. Los UUID `universalIdentifier` estables vinculan todo a través de los despliegues.
## Índices (opcional)
Las aplicaciones pueden incluir índices junto con sus objetos para mantener rápidas las consultas recurrentes. El caso más habitual es una columna de estado o de clave externa que lees con frecuencia.
```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,
},
],
});
```
### Índices únicos
`defineIndex` acepta `isUnique: true` tanto para unicidad de una sola columna como de varias columnas. Este es el elemento primitivo recomendado — `defineField({ isUnique: true })` está obsoleto y se eliminará en una versión futura.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Otras restricciones
* Las cláusulas `WHERE` parciales permanecen bajo el control del administrador: las aplicaciones no pueden declararlas.
* Cada objeto está limitado a 10 índices personalizados (los índices propios del framework no cuentan).
Ordena el arreglo `fields` de la forma en que Postgres debería usarlo: la columna más a la izquierda primero, como en una guía telefónica. Los índices no son gratuitos: cada escritura en la tabla los actualiza. Añade uno solo cuando tengas una consulta que lo necesite.
<Note>
¿Buscas **Application Config** o **Roles & Permissions**? Esos describen la propia aplicación en lugar de los datos que agrega; se encuentran en [Config](/l/es/developers/extend/apps/config/overview). ¿Buscas **Connections** (Linear, GitHub, Slack OAuth)? Estas existen para ser llamadas *desde* las funciones lógicas y se encuentran en [Logic](/l/es/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Relaciones
description: Conecta objetos entre sí con relaciones bidireccionales MANY_TO_ONE / ONE_TO_MANY.
icon: diagram-project
---
Las relaciones conectan dos objetos entre sí. En Twenty, las relaciones siempre son **bidireccionales**: cada relación tiene dos lados, y cada lado se declara como un campo que hace referencia al otro.
| Tipo de relación | Descripción | ¿Tiene clave foránea? |
| ---------------- | --------------------------------------------------------------------------- | --------------------- |
| `MANY_TO_ONE` | Muchos registros de este objeto apuntan a un registro del objeto de destino | Sí (`joinColumnName`) |
| `ONE_TO_MANY` | Un registro de este objeto tiene muchos registros del objeto de destino | No (lado inverso) |
## Cómo funcionan las relaciones
Cada relación requiere **dos campos** que se referencian entre sí:
1. El lado **MANY_TO_ONE** — vive en el objeto que contiene la clave foránea.
2. El lado **ONE_TO_MANY** — vive en el objeto que es propietario de la colección.
Ambos campos usan `FieldType.RELATION` y se hacen referencia cruzada mediante `relationTargetFieldMetadataUniversalIdentifier`.
## Ejemplo: la tarjeta postal tiene muchos destinatarios
Un `PostCard` puede enviarse a muchos registros `PostCardRecipient`. Cada destinatario pertenece exactamente a una tarjeta postal.
**Paso 1: Define el lado ONE_TO_MANY en PostCard** (el lado "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,
},
});
```
**Paso 2: Define el lado MANY_TO_ONE en PostCardRecipient** (el lado "muchos" — contiene la clave foránea):
```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>
**Importaciones circulares:** ambos campos de relación hacen referencia al `universalIdentifier` del otro. Para evitar problemas de importaciones circulares, exporta los ID de tus campos como constantes con nombre desde cada archivo e impórtalos en el otro. El sistema de compilación los resuelve en tiempo de compilación.
</Note>
## Relaciones de unión
Un objeto de unión conecta un registro de origen con uno o más registros de destino. Por ejemplo, `PostCardRecipient` puede contener tanto una relación `postCard` como una relación `recipient`. En el campo `ONE_TO_MANY` del objeto de origen, establece `junctionTargetFieldUniversalIdentifier` en el campo de relación del objeto de unión que apunta al destino:
```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` debe identificar una relación `MANY_TO_ONE` o morph en el objeto de unión. Indica a Twenty qué registro relacionado es el destino cuando una función como la [distribución de actividad de la cronología](/l/es/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) atraviesa la unión.
## Relacionar con objetos estándar
Para crear una relación con un objeto integrado de Twenty (Person, Company, etc.), 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',
},
});
```
## Propiedades del campo de relación
| Propiedad | Obligatorio | Descripción |
| ---------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------ |
| `type` | Sí | Debe ser `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Sí | El `universalIdentifier` del objeto de destino |
| `relationTargetFieldMetadataUniversalIdentifier` | Sí | El `universalIdentifier` del campo correspondiente en el objeto de destino |
| `universalSettings.relationType` | Sí | `RelationType.MANY_TO_ONE` o `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Solo para MANY_TO_ONE | Qué sucede cuando se elimina el registro referenciado: `CASCADE`, `SET_NULL`, `RESTRICT` o `NO_ACTION` |
| `universalSettings.joinColumnName` | Solo para MANY_TO_ONE | Nombre de la columna de base de datos para la clave foránea (p. ej., `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Solo ONE_TO_MANY de unión | Campo de relación del objeto de unión que apunta al destino de la distribución |
## Campos de relación en línea
También puedes declarar una relación directamente dentro de [`defineObject`](/l/es/developers/extend/apps/data/objects). Cuando es en línea, omite `objectUniversalIdentifier` — se hereda del objeto 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,215 @@
---
title: Apuntar a metadatos del sistema
description: Resuelve los identificadores universales deterministas de los metadatos de Twenty que se aprovisionan automáticamente en cada objeto, para que tu aplicación pueda hacer referencia a ellos sin codificarlos de forma rígida.
icon: gears
---
Cada objeto en Twenty incluye **metadatos de sistema** que tú nunca declaras directamente, como un conjunto de campos y una vista de lista principal con sus columnas. El servidor crea todo esto cuando se aprovisiona el objeto, y el conjunto crece a medida que Twenty lo hace.
Como no lo declaras, no hay ninguna constante `universalIdentifier` que puedas importar. En su lugar, el servidor **deriva** cada identificador de forma determinista, y `twenty-sdk` expone la misma derivación para que tu manifiesto pueda resolver el valor exacto que usa el servidor.
## Campos del sistema
Los campos escalares presentes en cada objeto, ninguno de los cuales declaras con [`defineField()`](/l/es/developers/extend/apps/data/extending-objects):
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Entonces, ¿cómo haces referencia a `createdAt` como una columna en una [vista](/l/es/developers/extend/apps/layout/views)?
### Problema
Desde Twenty 2.19, el identificador universal de un campo de sistema se **deriva de forma determinista** en el servidor a partir de tres entradas: el identificador universal de la aplicación, el identificador universal del objeto y el nombre del campo. Inventar un id y ponerlo de forma fija en el código no funcionará: no coincide con nada en el servidor y la sincronización rechaza la referencia colgante:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### La solución
<Note>
`getFieldUniversalIdentifier` está disponible a partir de `twenty-sdk` 2.21.
</Note>
Usa `getFieldUniversalIdentifier` para resolver exactamente el mismo valor que utiliza el servidor. Toma las tres entradas y devuelve el identificador universal del campo:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` es el identificador de tu aplicación, el que pasas a [`defineApplication()`](/l/es/developers/extend/apps/config/application).
* `objectUniversalIdentifier` es el identificador del objeto al que pertenece el campo.
* `name` es el nombre del campo de sistema, uno de los valores enumerados arriba.
### Ejemplo: una columna createdAt en una vista
El caso típico es añadir una columna `createdAt` a una vista de uno de tus objetos personalizados. Resuelve el id del campo y haz referencia a él como a cualquier otro `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,
},
],
});
```
El mismo id resuelto funciona en cualquier lugar donde se espere un `fieldMetadataUniversalIdentifier`: campos de vista, filtros, ordenaciones, agrupaciones y widgets de diseño de página.
<Note>
Resuelve el id, no lo pongas de forma fija en el código. Como el servidor deriva el valor a partir
del id de la aplicación, el id del objeto y el nombre del campo, llamar a
`getFieldUniversalIdentifier` mantiene tu referencia correcta incluso si esas
entradas cambian, y evita desviaciones si la derivación evoluciona en algún momento.
</Note>
### Campos de relación del sistema
<Note>
`getSystemRelationFieldUniversalIdentifier` está disponible desde `twenty-sdk`
2.23 en adelante y requiere un servidor Twenty en la versión 2.23 o posterior.
</Note>
Además de los campos escalares del sistema mencionados arriba, el servidor también aprovisiona cuatro **campos de relación del sistema** en cada objeto: `timelineActivities`, `attachments`, `noteTargets` y `taskTargets`, cada uno apuntando al objeto de relación estándar correspondiente.
Estos campos no se resuelven con `getFieldUniversalIdentifier`: su identificador se deriva **sin nombre**, a partir del objeto que aloja el campo y del objeto al que apunta el campo. De este modo, cambiar el nombre de un objeto nunca modifica los identificadores de sus campos de relación.
Use `getSystemRelationFieldUniversalIdentifier` para resolverlos:
```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` es el objeto que **aloja** el campo.
* `relationTargetObjectUniversalIdentifier` es el objeto al que el campo **apunta**.
La dirección se codifica mediante el orden de los argumentos. Para resolver el lado inverso (por ejemplo, `attachment.targetRocket`, el campo morph que el servidor crea en el objeto de relación estándar), intercambie ambos:
```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,
});
```
Como con los campos de sistema escalares, el id resuelto funciona en cualquier lugar donde se espere un `fieldMetadataUniversalIdentifier`.
## Vistas de sistema
<Note>
`getSystemViewUniversalIdentifier` y `getSystemViewFieldUniversalIdentifier`
están disponibles desde `twenty-sdk` 2.26 en adelante y requieren un servidor
Twenty en la versión 2.26 o posterior.
</Note>
El servidor también aprovisiona una **vista de sistema** en cada objeto: la vista de lista principal (`All {objectLabelPlural}`, con clave `ViewKey.INDEX` (derivada con `SYSTEM_VIEW_KEYS.INDEX`)), con una columna por cada campo visible. Al igual que los campos de relación de sistema, sus identificadores se derivan **sin usar nombres**, por lo que renombrar un objeto o un campo nunca los cambia.
Usa `getSystemViewUniversalIdentifier` para resolver 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` es la aplicación propietaria del **objeto**, que es la que define el espacio de nombres de la vista.
* `objectUniversalIdentifier` es el objeto que la vista enumera.
* `viewKey` es el discriminador de vista de sistema: `SYSTEM_VIEW_KEYS.INDEX` para la vista de lista principal, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` para la vista del widget de campos de la página de registro. Sirve como clave para la derivación; únicamente `INDEX` se almacena además en la fila de la vista.
El id resuelto funciona en cualquier lugar donde se espere un `viewUniversalIdentifier`, como una entrada en la barra lateral de tipo [`NavigationMenuItemType.VIEW`](/l/es/developers/extend/apps/layout/navigation-menu-items). Para simplemente abrir la lista principal de un objeto, es preferible usar `NavigationMenuItemType.OBJECT` con `targetObjectUniversalIdentifier`: no necesita ninguna derivación.
`getSystemViewFieldUniversalIdentifier` resuelve una sola **columna** en una vista de sistema, a partir de la vista y del campo que muestra:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Observa el primer argumento: una columna está en el espacio de nombres de la aplicación propietaria del **campo que muestra**, no de la que es propietaria de la vista. Un campo que tu aplicación añade a un objeto estándar obtiene su columna derivada bajo tu aplicación, en una vista propiedad de Twenty.
<Warning>
Las vistas de sistema y sus columnas son **propiedad del servidor**: resuelve sus identificadores
para hacer referencia a ellas, nunca para declararlas. `key` en
[`defineView()`](/l/es/developers/extend/apps/layout/views) está en desuso y se
ignora, por lo que una vista de manifiesto nunca puede reclamar la clave
`INDEX`, y el servidor ya aprovisiona una columna para cada campo que añades,
de modo que declarar tu propio `defineViewField()` para ese mismo campo en una
vista de sistema entra en conflicto con ella.
</Warning>
## Objetos estándar de Twenty
Para un objeto **estándar** de Twenty (Persona, Compañía, Oportunidad, …), no necesitas derivar nada: los identificadores están precalculados como constantes que puedes importar directamente, tanto para campos como para vistas.
```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
```
Recurre a los ayudantes anteriores cuando el objeto es uno que **tu aplicación** define con [`defineObject()`](/l/es/developers/extend/apps/data/objects), donde no existe tal constante.
<Note>
`name` es un campo **predeterminado**, no un campo de sistema. Conserva su propio identificador universal fijo
y no se resuelve mediante
`getFieldUniversalIdentifier`. En los objetos que defines, haz referencia al campo
`name` mediante el identificador que le diste en `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Tipos de actividad del cronograma
description: Definir eventos de auditoría automática y eventos de aplicación explícitos que renderizan en cronogramas de registro.
icon: clock-rotate-left
---
Un tipo de actividad de línea de tiempo define el vocabulario estable para un evento que se muestra en la línea de tiempo de un registro. Los objetos estándar y los objetos de aplicación utilizan el mismo contrato: un tipo tiene una etiqueta y un icono, puede opcionalmente declarar cuando se emita, y puede renderizar opcionalmente a través de uno de los [componentes frontales] de tu aplicación (/developers/extend/apps/layout/front-components).
<Note>
Los tipos de actividad de la línea de tiempo están en fase beta y pronto estarán disponibles en Veinte 2.34. La
API puede evolucionar mientras aprendemos de los casos de uso de los desarrolladores.
</Note>
Crea uno con el andamiante:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
O definirlo directamente:
```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,
},
});
```
## Eventos explícitos y automáticos
Añade `emit` cuando Veenty debería crear este tipo automáticamente. `emit.on` soporta `created`, `updated`, `deleted`, `restored`, `linked`, y `unlinked`, mientras que `emit.objectUniversalIdentifier` identifica el objeto fuente. Sólo un tipo efectivo puede manejar la misma clave de emisión—`on`, objeto y opcional a través de la relación—en un espacio de trabajo.
Sin `emit.throughh`, el evento está escrito en la línea de tiempo del registro fuente. Para aficionarlo a registros relacionados, establezca `emit.throughh. elationFieldUniversalIdentifier` a una relación directa de muchos-a-uno o a una [relación de unión uno-a-muchos](/l/es/developers/extend/apps/data/relations#junction-relations) en el objeto fuente. Las relaciones morfológicas se dirigen a cada miembro de su grupo morfológico, por lo que una sola declaración puede centrarse en varios tipos de objetos.
Para una relación directa, crear o restaurar el registro fuente produce `linked`, eliminándolo produce `unlinked`, y reapuntando la relación produce `unlinked` en el objetivo anterior más `linked` en el nuevo objetivo. Otras actualizaciones de código fuente no producen eventos de enlace. Este es el contrato utilizado por los archivos adjuntos, cuyo objetivo es una relación directa con la morfa.
Para una relación de juncción, `universalSettings.junctionTargetFieldUniversalIdentifier` identifica la relación del objeto de unión con el objetivo. Crear o eliminar una fila de unión produce `linked` o `unlinked`. Reapuntando a cualquiera de los lados de un registro de junction también produce un evento de enlace; las actualizaciones a campos de junction no relacionados no lo hacen.
Los eventos `linked` y `unlinked` requieren `emit.throughh`, porque su disparador es un cambio en la relación directa o de unión configurada.
Por ejemplo, este es el mismo contrato genérico utilizado por notas, tareas, mensajes y eventos del 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,
},
},
});
```
Por defecto se emite un evento `actualizado` a través de cada actualización de fuente. Establece `emit.through.triggerFieldUniversalIdentifiers` cuando solo los cambios a los campos de origen seleccionados deben aparecer en las líneas de tiempo de destino.
Omita `emit` para un evento de dominio explícito que tu función lógica se crea. Esto evita producir tanto una fila de auditoría automática como una fila explícita para la misma operación.
```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 eventos explícitos con [`createTimelineActivity()`](/l/es/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). El código de la aplicación utiliza identificadores universales estables; Veinte resuelve los identificadores de metadatos específicos de la instalación.
## Procesamiento personalizado
Sin un componente frontal, Veinte renderiza una fila genérica nativa de los metadatos de tipo etiqueta, icono y objeto enlazado. Esto funciona para objetos estándar y personalizados sin un renderizador específico de objetos.
Para detalles personalizados, establezca `frontComponentUniversalIdentifier` en un componente frontal propiedad de la misma aplicación:
```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 fila nativa sigue siendo la presentación colapsada. Veinte monta el componente frontal sólo después de que el usuario amplíe la fila, evitando un sandbox y un trabajador para cada evento visible. Dentro del componente, llame a `useTimelineActivityId()` desde `twenty-sdk/front-component` para leer el ID de la fila y obtener cualquier dato que su presentación necesite. Devuelve `null` cuando el componente es renderizado fuera de una fila de cronología.
## Reemplazar la presentación de otra aplicación
Una aplicación posee contratos de cronología automáticos para sus propios objetos. Para personalizar un evento en un objeto propiedad de otra aplicación, declare explícitamente el tipo existente 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 sobreescritura debe incluir `emit`, porque reemplaza una ranura de emisión automática existente en lugar de un tipo explícito. El tipo referenciado debe pertenecer a la aplicación del objeto objetivo y describir la misma acción y ruta. Eliminar la aplicación de sobre-escritura restaura el tipo base; eliminar el tipo base desactiva la anulación.
## Superposición y silenciamiento del espacio de trabajo
Los administradores del espacio de trabajo pueden cambiar la presentación de un tipo o silenciar sus eventos automáticos y explícitos sin bifurcar la aplicación. Usa la mutación de la API de metadatos `updateTimelineActivityType` con el ID de tipo específico de la instalación:
```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` se almacenan como espacios de trabajo sobrescritos, por lo que las actualizaciones posteriores de la aplicación no sobreescriben las opciones del administrador. Establece `isActive` en `false` para detener la emisión automática y rechazar nuevos eventos explícitos de ese tipo. Las filas existentes siguen siendo visibles.
Restaurar los valores predeterminados de la aplicación, incluyendo el estado activo, con:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Campos de configuración
| Campo | Obligatorio | Descripción |
| ------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------- |
| `universalIdentifier` | Sí | UUID estable para el tipo entre instalaciones y actualizaciones |
| `name` | Sí | Nombre programático local de la aplicación; debe ser único dentro de la aplicación |
| `label` | Sí | Texto de acción de cara al usuario usado por el renderizador nativo |
| `icon` | No | Veinte nombre de icono mostrado al lado del evento |
| `emit` | No | Declaración de emisión automática; omisión de tipos sólo explícitos |
| `emit.on` | Con emisión | Acción de auditoría que hace que este tipo se escriba |
| `emit.objectUniversalIdentifier` | Con emisión | Objeto cuyos registros emiten este tipo |
| `emit.through.relationFieldUniversalIdentifier` | No | Relación directa o de unión usada para reavivar las líneas de tiempo relacionadas |
| `emit.through.triggerFieldUniversalIdentifiers` | No | Campos de origen que pueden desencadenar un evento 'actualizado' |
| `frontComponentUniversalIdentifier` | No | Componente frontal propiedad de la aplicación montado cuando la fila es expandida |
| `replacesTimelineActivityTypeUniversalIdentifier` | No | Tipo de emisión existente reemplazado; requiere `emit` |
Veinte instantáneas capturan identidad semántica y presentaciones duraderas cuando se crea un evento. Las filas históricas mantienen su acción original y su objeto significado. Mientras el tipo está instalado, su actual etiqueta traducida, icono y componente frontal renderizan en vivo; después de desinstalar, la instantánea mantiene el registro legible. La resolución utiliza el identificador universal, así que la presentación también sobrevive a reinstalar la aplicación.