i18n - docs translations (#22715)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22715?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
a0cf4cc9e1
commit
ebee7d71b9
@@ -4,9 +4,9 @@ description: "Ejecuta lógica antes o después de la instalación: introduce dat
|
||||
icon: wrench
|
||||
---
|
||||
|
||||
Los hooks de instalación son funciones de lógica especiales que se ejecutan durante el ciclo de vida de la instalación o actualización. Comparten el mismo tiempo de ejecución del controlador que las [logic functions](/l/es/developers/extend/apps/logic/logic-functions) normales y reciben un `InstallPayload`, pero se declaran con sus propias funciones de definición — `definePostInstallLogicFunction()` y `definePreInstallLogicFunction()` — y están fuera del modelo de desencadenadores normal (HTTP, cron, eventos de base de datos).
|
||||
Los hooks de instalación son funciones de lógica especiales que se ejecutan durante el ciclo de vida de la instalación o actualización. Comparten el mismo tiempo de ejecución del handler que las [logic functions](/l/es/developers/extend/apps/logic/logic-functions) normales y reciben un `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` es `undefined` en una instalación nueva), pero se declaran con sus propias funciones define y viven fuera del modelo de disparadores normal (HTTP, cron, eventos de base de datos).
|
||||
|
||||
Cada aplicación puede definir **como máximo una función de preinstalación** y **como máximo una función de posinstalación**. La compilación del manifiesto generará un error si se detecta más de una de cualquiera de las dos.
|
||||
Cada aplicación puede definir **como máximo una función de preinstalación** y **como máximo una función de posinstalación**. La compilación del manifiesto genera un error si se detecta más de una de cualquiera de las dos.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
@@ -19,111 +19,59 @@ Cada aplicación puede definir **como máximo una función de preinstalación**
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Se ejecuta después de que se aplique la migración de metadatos del espacio de trabajo">
|
||||
## De un vistazo
|
||||
|
||||
Una función de posinstalación se ejecuta automáticamente una vez que tu aplicación ha terminado de instalarse en un espacio de trabajo. El servidor la ejecuta **después** de que se hayan sincronizado los metadatos de la aplicación y se haya generado el cliente del SDK, de modo que el espacio de trabajo esté completamente listo para usarse y el nuevo esquema esté disponible. Los casos de uso típicos incluyen poblar datos predeterminados, crear registros iniciales, configurar los ajustes del espacio de trabajo o aprovisionar recursos en servicios de terceros.
|
||||
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ejecuciones | Antes de la migración de metadatos — el esquema y los datos **anteriores** siguen intactos | Después de la migración y la generación del SDK — el esquema **nuevo** está en su lugar |
|
||||
| Ejecución | Siempre síncrona; bloquea la instalación | Asíncrona de forma predeterminada (en cola, 3 reintentos); modo síncrono opcional mediante `shouldRunSynchronously: true` |
|
||||
| En caso de fallo | La instalación se **aborta** antes de cualquier cambio de esquema | Asíncrono: se vuelve a intentar hasta 3 veces. Síncrono: quien realiza la llamada recibe `POST_INSTALL_ERROR` (los cambios de esquema **no** se revierten) |
|
||||
| Uso típico | Hacer copia de seguridad o corregir datos que una migración perdería; rechazar una actualización arriesgada lanzando una excepción | Sembrar datos predeterminados, configurar el espacio de trabajo, registrar recursos externos |
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
**Regla general:** usa post-install de forma predeterminada. Recurra a la pre-instalación solo cuando la propia migración sea destructiva y necesite interceptar el estado anterior antes de que desaparezca.
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
| Quiere... | Usar |
|
||||
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| Sembrar datos, configurar el espacio de trabajo, registrar recursos externos | `post-install` |
|
||||
| Trabajo de larga duración que no debería bloquear la respuesta de instalación | `post-install` (modo asíncrono predeterminado, con reintentos del worker) |
|
||||
| Configuración rápida de la que el cliente depende inmediatamente después de que finaliza la instalación | `post-install` con `shouldRunSynchronously: true` |
|
||||
| Leer o hacer copia de seguridad de datos que la próxima migración perdería | `pre-install` |
|
||||
| Rechazar una actualización que corrompería datos existentes | `pre-install` (lanzar desde el controlador) |
|
||||
| Reconciliación en cada actualización | Cualquiera de los hooks con `shouldRunOnVersionUpgrade: true` |
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
## Comportamiento compartido por ambos hooks
|
||||
|
||||
También puedes ejecutar manualmente la función de posinstalación en cualquier momento usando la CLI:
|
||||
* La configuración es una configuración de `defineLogicFunction` menos los ajustes de disparador, más `shouldRunOnVersionUpgrade`.
|
||||
* **Cuándo se ejecuta**: solo en instalaciones nuevas, de forma predeterminada. Configura `shouldRunOnVersionUpgrade: true` para que también se ejecute en las actualizaciones. Usa `previousVersion` / `newVersion` para ramificar según la ruta de actualización.
|
||||
* **La idempotencia es importante**: el post-install asíncrono puede reintentarse y cualquiera de los hooks se vuelve a ejecutar en las actualizaciones cuando `shouldRunOnVersionUpgrade` está activado.
|
||||
* El entorno habitual de las logic functions (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) se inyecta, por lo que puedes llamar a la API de Twenty con el token de tu app.
|
||||
* El hook se adjunta automáticamente al manifiesto de la aplicación en tiempo de compilación (`preInstallLogicFunction` / `postInstallLogicFunction`) — no hay nada que referenciar en [`defineApplication()`](/l/es/developers/extend/apps/config/application).
|
||||
* El `timeoutSeconds` predeterminado es 300 para permitir tareas de configuración más largas como la siembra de datos.
|
||||
* **No se ejecuta en modo de desarrollo**: `yarn twenty dev` omite el flujo de instalación y sincroniza los archivos directamente, por lo que los hooks nunca se ejecutan ahí. En su lugar, dispáralos manualmente:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
* Las funciones de posinstalación usan `definePostInstallLogicFunction()` — una variante especializada que omite la configuración de desencadenadores (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
* El controlador recibe un `InstallPayload` con `{ previousVersion?: string; newVersion: string }` — `newVersion` es la versión que se está instalando, y `previousVersion` es la versión que se instaló previamente (o `undefined` en una instalación nueva). Use estos valores para distinguir instalaciones nuevas de actualizaciones y para ejecutar lógica de migración específica de la versión.
|
||||
* **Cuándo se ejecuta el hook**: solo en instalaciones nuevas, de forma predeterminada. Pase `shouldRunOnVersionUpgrade: true` si también quiere que se ejecute cuando la app se actualice desde una versión anterior. Si se omite, el indicador es `false` por defecto y las actualizaciones omiten el hook.
|
||||
* **Modelo de ejecución — asíncrono por defecto, sincronía opcional**: el indicador `shouldRunSynchronously` controla *cómo* se ejecuta la post-instalación.
|
||||
* `shouldRunSynchronously: false` *(predeterminado)* — el hook se **encola en la cola de mensajes** con `retryLimit: 3` y se ejecuta de forma asíncrona en un worker. La respuesta de instalación se devuelve tan pronto como el trabajo se encola, por lo que un controlador lento o con fallos no bloquea al solicitante. El worker reintentará hasta tres veces. **Úselo para trabajos de larga duración** — sembrar conjuntos de datos grandes, llamar a APIs de terceros lentas, aprovisionar recursos externos, cualquier cosa que pueda exceder una ventana de respuesta HTTP razonable.
|
||||
* `shouldRunSynchronously: true` — el hook se ejecuta **en línea durante el flujo de instalación** (el mismo ejecutor que la pre-instalación). La solicitud de instalación se bloquea hasta que el controlador finaliza y, si arroja una excepción, quien realiza la instalación recibe un `POST_INSTALL_ERROR`. Sin reintentos automáticos. **Úselo para trabajo rápido que debe completarse antes de la respuesta** — por ejemplo, emitir un error de validación al usuario, o una configuración rápida de la que el cliente dependerá inmediatamente después de que regrese la llamada de instalación. Tenga en cuenta que la migración de metadatos ya se ha aplicado cuando se ejecuta la post-instalación, por lo que un fallo en modo síncrono **no** revierte los cambios de esquema — solo expone el error.
|
||||
* Asegúrese de que su controlador sea idempotente. En modo asíncrono, la cola puede reintentar hasta tres veces; en cualquier modo, el hook puede ejecutarse de nuevo en las actualizaciones cuando `shouldRunOnVersionUpgrade: true`.
|
||||
* Las variables de entorno `APPLICATION_ID`, `APP_ACCESS_TOKEN` y `API_URL` están disponibles dentro del controlador (igual que en cualquier otra función de lógica), por lo que puede llamar a la API de Twenty con un token de acceso de aplicación con alcance a su app.
|
||||
* Solo se permite una función de posinstalación por aplicación. La compilación del manifiesto generará un error si se detecta más de una.
|
||||
* Los `universalIdentifier`, `shouldRunOnVersionUpgrade` y `shouldRunSynchronously` de la función se adjuntan automáticamente al manifiesto de la aplicación en el campo `postInstallLogicFunction` durante la compilación; no es necesario que los referencies en [`defineApplication()`](/l/es/developers/extend/apps/config/application).
|
||||
* El tiempo de espera predeterminado se establece en 300 segundos (5 minutos) para permitir tareas de configuración más largas como la carga inicial de datos.
|
||||
* **No se ejecuta en modo de desarrollo**: cuando una app se registra localmente (mediante `yarn twenty dev`), el servidor omite por completo el flujo de instalación y sincroniza archivos directamente a través del observador de la CLI — por lo tanto, la post-instalación nunca se ejecuta en modo de desarrollo, independientemente de `shouldRunSynchronously`. Use `yarn twenty dev:function:exec --postInstall` para activarlo manualmente en un espacio de trabajo en ejecución.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Se ejecuta antes de que se aplique la migración de metadatos del espacio de trabajo">
|
||||
|
||||
Una función de preinstalación se ejecuta automáticamente durante la instalación, **antes de que se aplique la migración de metadatos del espacio de trabajo**. Comparte la misma forma de payload que la post-instalación (`InstallPayload`), pero está situada antes en el flujo de instalación para poder preparar el estado del que depende la próxima migración — usos típicos incluyen hacer copias de seguridad de datos, validar la compatibilidad con el nuevo esquema o archivar registros que están a punto de ser reestructurados o eliminados.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
También puedes ejecutar manualmente la función de preinstalación en cualquier momento usando la CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --preInstall
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
* Las funciones de pre-instalación usan `definePreInstallLogicFunction()` — la misma configuración especializada que la post-instalación, solo que adjunta a un punto diferente del ciclo de vida.
|
||||
* Tanto los controladores de pre- como de post-instalación reciben el mismo tipo `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Impórtelo una vez y reutilícelo para ambos hooks.
|
||||
* **Cuándo se ejecuta el hook**: se ubica justo antes de la migración de metadatos del espacio de trabajo (`synchronizeFromManifest`). Antes de ejecutarse, el servidor realiza una "sincronización simplificada" puramente aditiva que registra la función de pre-instalación de la versión **nueva** en los metadatos del espacio de trabajo — no se toca nada más — y luego la ejecuta. Debido a que esta sincronización es solo aditiva, los objetos, campos y datos de la versión anterior siguen intactos cuando se ejecuta su controlador: puede leer y respaldar de forma segura el estado premigración.
|
||||
* **Modelo de ejecución**: la pre-instalación se ejecuta **de forma síncrona** y **bloquea la instalación**. Si el controlador lanza una excepción, la instalación se aborta antes de que se apliquen cambios de esquema — el espacio de trabajo permanece en la versión anterior en un estado consistente. Esto es intencional: la pre-instalación es su última oportunidad para rechazar una actualización arriesgada.
|
||||
* Al igual que con la post-instalación, solo se permite una función de preinstalación por aplicación. Se adjunta automáticamente al manifiesto de la aplicación bajo `preInstallLogicFunction` durante la compilación.
|
||||
* **No se ejecuta en modo de desarrollo**: igual que la post-instalación — el flujo de instalación se omite por completo para las apps registradas localmente, por lo que la pre-instalación nunca se ejecuta con `yarn twenty dev`. Use `yarn twenty dev:function:exec --preInstall` para activarlo manualmente.
|
||||
<AccordionGroup>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Se ejecuta después de que se aplique la migración de metadatos del espacio de trabajo">
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-instalación vs post-instalación: cuándo usar cada una" description="Elegir el hook de instalación adecuado">
|
||||
|
||||
Ambos hooks forman parte del mismo flujo de instalación y reciben el mismo `InstallPayload`. La diferencia es **cuándo** se ejecutan con respecto a la migración de metadatos del espacio de trabajo, y eso cambia qué datos pueden tocar de forma segura.
|
||||
|
||||
La pre-instalación siempre es **síncrona** (bloquea la instalación y puede abortarla). La post-instalación es **asíncrona por defecto** — se pone en cola en un worker con reintentos automáticos — pero puede optar por ejecución síncrona con `shouldRunSynchronously: true`. Consulte el acordeón `definePostInstallLogicFunction` de arriba para saber cuándo usar cada modo.
|
||||
|
||||
**Use `post-install` para cualquier cosa que necesite que exista el nuevo esquema.** Este es el caso más común:
|
||||
|
||||
* Sembrar datos predeterminados (crear registros iniciales, vistas predeterminadas, contenido de demostración) sobre objetos y campos recién añadidos.
|
||||
* Registrar webhooks con servicios de terceros ahora que la app ya tiene sus credenciales.
|
||||
* Llamar a su propia API para finalizar una configuración que depende de los metadatos sincronizados.
|
||||
* Lógica idempotente de "asegurar que esto exista" que debe reconciliar el estado en cada actualización — combínela con `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Ejemplo — sembrar un registro `PostCard` predeterminado después de la instalación:
|
||||
Se ejecuta una vez que tu app ha terminado de instalarse: metadatos sincronizados, cliente SDK generado, nuevo esquema disponible para consulta. Ejemplo — sembrar un registro predeterminado en instalaciones nuevas:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
const client = new CoreApiClient();
|
||||
await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,22 +81,28 @@ export default definePostInstallLogicFunction({
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Use `pre-install` cuando una migración, de otro modo, destruiría o corrompería datos existentes.** Como la pre-instalación se ejecuta contra el esquema *anterior* y su fallo revierte la actualización, es el lugar adecuado para cualquier cosa arriesgada:
|
||||
El flag `shouldRunSynchronously` controla el modelo de ejecución:
|
||||
|
||||
* **Hacer copia de seguridad de datos que están a punto de eliminarse o reestructurarse** — p. ej., está quitando un campo en la v2 y necesita copiar sus valores a otro campo o exportarlos a almacenamiento antes de que se ejecute la migración.
|
||||
* **Archivar registros que una nueva restricción invalidaría** — p. ej., un campo pasará a ser `NOT NULL` y primero necesita eliminar o corregir filas con valores nulos.
|
||||
* **Validar la compatibilidad y rechazar la actualización si los datos actuales no pueden migrarse limpiamente** — lance desde el controlador y la instalación se abortará sin aplicar cambios. Esto es más seguro que descubrir la incompatibilidad a mitad de la migración.
|
||||
* **Renombrar o reasignar claves de datos** antes de un cambio de esquema que perdería la asociación.
|
||||
* `false` *(predeterminado)* — encolado en la cola de mensajes (`retryLimit: 3`) y ejecutado por un worker. La respuesta de instalación se devuelve tan pronto como el trabajo se pone en la cola. **Usar para trabajo de larga duración** — siembra de grandes conjuntos de datos, APIs de terceros lentas.
|
||||
* `true` — se ejecuta en línea durante el flujo de instalación. La solicitud de instalación se bloquea hasta que el handler finaliza; un error lanzado aparece como `POST_INSTALL_ERROR` para quien realiza la llamada (sin reintentos). **Usar para trabajo rápido que debe completarse antes de la respuesta.** La migración ya se ha aplicado en este punto, por lo que un fallo no revierte los cambios de esquema — solo expone el error.
|
||||
|
||||
Ejemplo — archivar registros antes de una migración destructiva:
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Se ejecuta antes de que se aplique la migración de metadatos del espacio de trabajo">
|
||||
|
||||
Se ejecuta antes de la migración de metadatos, contra el esquema **anterior** — el lugar adecuado para hacer una copia de seguridad de los datos que una migración perdería o para rechazar una actualización arriesgada. Antes de ejecutarse, el servidor realiza una "sincronización simplificada" puramente aditiva que registra solo la función de pre-instalación de la versión nueva; todo lo demás — los objetos, campos y datos de la versión anterior — permanece sin cambios cuando se ejecuta tu handler.
|
||||
|
||||
La pre-instalación siempre es **síncrona** y bloquea la instalación. Si el handler lanza una excepción, la instalación se aborta antes de cualquier cambio de esquema — el espacio de trabajo permanece en la versión anterior en un estado consistente. Esto es intencional: la pre-instalación es su última oportunidad para rechazar una actualización arriesgada.
|
||||
|
||||
Ejemplo — copiar los valores de un campo heredado antes de que la migración lo elimine:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
@@ -156,24 +110,24 @@ const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
const client = new CoreApiClient();
|
||||
const { postCards } = await client.query({
|
||||
postCards: {
|
||||
__args: { filter: { notes: { isNot: null } } },
|
||||
edges: { node: { id: true, notes: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
// Copy legacy `notes` into `description` before the migration drops the
|
||||
// column. If this fails, the upgrade aborts and the workspace stays on v1.
|
||||
for (const { node } of postCards.edges) {
|
||||
await client.mutation({
|
||||
updatePostCard: {
|
||||
__args: { id: node.id, data: { description: node.notes } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
@@ -186,21 +140,5 @@ export default definePreInstallLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
**Regla general:**
|
||||
|
||||
| Quiere... | Usar |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| Sembrar datos predeterminados, configurar el espacio de trabajo, registrar recursos externos | `post-install` |
|
||||
| Ejecutar siembras de larga duración o llamadas a terceros que no deberían bloquear la respuesta de instalación | `post-install` (predeterminado — `shouldRunSynchronously: false`, con reintentos del worker) |
|
||||
| Ejecutar una configuración rápida de la que el cliente dependerá inmediatamente después de que regrese la llamada de instalación | `post-install` con `shouldRunSynchronously: true` |
|
||||
| Leer o hacer copia de seguridad de datos que la próxima migración perdería | `pre-install` |
|
||||
| Rechazar una actualización que corrompería datos existentes | `pre-install` (lanzar desde el controlador) |
|
||||
| Ejecutar reconciliación en cada actualización | `post-install` con `shouldRunOnVersionUpgrade: true` |
|
||||
| Realizar una configuración única solo en la primera instalación | `post-install` con `shouldRunOnVersionUpgrade: false` (predeterminado) |
|
||||
|
||||
<Note>
|
||||
En caso de duda, elija **post-install** como predeterminado. Recurra a la pre-instalación solo cuando la propia migración sea destructiva y necesite interceptar el estado anterior antes de que desaparezca.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -86,6 +86,22 @@ export default defineObject({
|
||||
**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` |
|
||||
| Opció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}'` ``.
|
||||
|
||||
+32
-16
@@ -14,26 +14,39 @@ my-twenty-app/
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
front-components/
|
||||
main-page.tsx # Welcome page component
|
||||
navigation-menu-items/
|
||||
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
|
||||
page-layouts/
|
||||
main-page.page-layout.ts # Standalone page hosting the component
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
application-config.test.ts # Unit test
|
||||
global-setup.ts # Integration test setup (sync + uninstall)
|
||||
schema.integration-test.ts # Integration test against a live server
|
||||
.github/workflows/
|
||||
ci.yml # Lint, typecheck, unit + integration tests
|
||||
cd.yml # Deploy + install on push to main
|
||||
public/
|
||||
logo.svg # Static assets
|
||||
vitest.config.ts # Integration test runner config
|
||||
vitest.unit.config.ts # Unit test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
README.md, AGENTS.md, CLAUDE.md
|
||||
```
|
||||
|
||||
## Archivos clave
|
||||
|
||||
| Archivo / Carpeta | Propósito |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `src/application-config.ts` | **Obligatorio.** El archivo de configuración principal de tu app. |
|
||||
| `src/default-role.ts` | Rol predeterminado que controla a qué pueden acceder tus funciones lógicas. |
|
||||
| `src/constants/universal-identifiers.ts` | UUIDs generados automáticamente y metadatos de la app (nombre para mostrar, descripción). |
|
||||
| `src/__tests__/` | Pruebas de integración (configuración + prueba de ejemplo). |
|
||||
| `public/` | Recursos estáticos (imágenes, fuentes) servidos con tu app. |
|
||||
| Archivo / Carpeta | Propósito |
|
||||
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `src/application-config.ts` | **Obligatorio.** El archivo de configuración principal de tu app. |
|
||||
| `src/default-role.ts` | Rol predeterminado que controla a qué pueden acceder tus funciones lógicas. |
|
||||
| `src/constants/universal-identifiers.ts` | UUIDs generados automáticamente y metadatos de la app (nombre para mostrar, descripción). |
|
||||
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | Una página de bienvenida inicial: un front component renderizado por un page layout independiente, accesible desde la barra lateral. |
|
||||
| `src/__tests__/` | Una prueba unitaria más una prueba de integración (con su configuración global) que sincroniza la aplicación contra un servidor real. |
|
||||
| `public/` | Recursos estáticos (imágenes, fuentes) servidos con tu app. |
|
||||
| `AGENTS.md` / `CLAUDE.md` | Guía para agentes de IA de programación que trabajan en la aplicación. |
|
||||
|
||||
<Note>
|
||||
**La organización de archivos depende de ti.** Las carpetas anteriores son convenciones: el SDK detecta entidades mediante análisis AST en llamadas a `export default defineEntity(...)`, sin importar dónde se encuentre el archivo.
|
||||
@@ -47,15 +60,18 @@ Ambos paquetes del SDK de Twenty pertenecen a `devDependencies`, no a `dependenc
|
||||
{
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"twenty-client-sdk": "^2.13.0",
|
||||
"twenty-sdk": "^2.13.0"
|
||||
"twenty-client-sdk": "2.20.0",
|
||||
"twenty-sdk": "2.20.0",
|
||||
"twenty-ui": "1.0.0-alpha.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
El generador fija `twenty-sdk` y `twenty-client-sdk` a su propia versión; mantén ambos sincronizados al actualizar.
|
||||
|
||||
* **`twenty-sdk`** incluye el CLI `twenty` y las herramientas de build/scaffolding. Solo se ejecuta en el desarrollo y durante el build, y nunca lo importa el runtime de la app que publicas.
|
||||
* **`twenty-client-sdk`** *sí* es importado por el código de tu app (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), pero Twenty lo proporciona en tiempo de ejecución: las funciones lógicas lo obtienen de una capa SDK generada y los componentes de front lo resuelven desde módulos servidos por el servidor. Tu copia instalada solo se utiliza para la comprobación de tipos y el build en tiempo de despliegue, por lo que nunca necesita incluirse en el bundle desplegado.
|
||||
|
||||
Mantener cualquiera de los paquetes bajo `dependencies` lo introduce en el bundle de runtime de la app instalada, donde es peso muerto. `twenty build` emite una advertencia cuando cualquiera de ellos sigue listado bajo `dependencies`.
|
||||
Mantener cualquiera de los paquetes bajo `dependencies` lo introduce en el bundle de runtime de la app instalada, donde es peso muerto. `twenty dev:build` emite una advertencia cuando cualquiera de ellos sigue listado bajo `dependencies`.
|
||||
|
||||
Añade las dependencias de runtime propias de tu app (las bibliotecas que tus funciones lógicas realmente importan en tiempo de ejecución) bajo `dependencies` como de costumbre.
|
||||
|
||||
@@ -6,17 +6,17 @@ description: Crea tu primera aplicación de Twenty en minutos.
|
||||
|
||||
## Prerrequisitos
|
||||
|
||||
* **Node.js 24+** — [Descargar](https://nodejs.org/)
|
||||
* **Node.js 24.5+** — [Descargar](https://nodejs.org/)
|
||||
* **Yarn 4** — incluido con Node.js a través de Corepack. Actívalo: `corepack enable`
|
||||
* **Docker** — [Descargar](https://www.docker.com/products/docker-desktop/). Necesario para ejecutar un servidor local de Twenty. Omítelo si ya tienes Twenty ejecutándose en otro lugar.
|
||||
|
||||
La creación de una app de Twenty tiene tres fases. El generador las combina en un único comando de ruta ideal, pero cada fase es un concepto independiente — cuando algo falla, saber en qué fase estás te indica qué debes corregir.
|
||||
|
||||
| Fase | Qué haces | Herramienta | Resultado |
|
||||
| --------------------------- | --------------------------------------------------- | ----------------------------- | ------------------------------------ |
|
||||
| **1. Generar estructura** | Genera el código fuente de la app | `npx create-twenty-app` | Un proyecto de TypeScript en disco |
|
||||
| **2. Ejecutar un servidor** | Inicia un servidor de Twenty con el que sincronizar | Docker + `yarn twenty server` | Una instancia de Twenty en ejecución |
|
||||
| **3. Sincronizar** | Sincroniza en vivo tu código con el servidor | `yarn twenty dev` | Tus cambios aparecen en la UI |
|
||||
| Fase | Qué haces | Herramienta | Resultado |
|
||||
| --------------------------- | --------------------------------------------------- | ----------------------------------- | ------------------------------------ |
|
||||
| **1. Generar estructura** | Genera el código fuente de la app | `npx create-twenty-app` | Un proyecto de TypeScript en disco |
|
||||
| **2. Ejecutar un servidor** | Inicia un servidor de Twenty con el que sincronizar | Docker + `yarn twenty docker:start` | Una instancia de Twenty en ejecución |
|
||||
| **3. Sincronizar** | Sincroniza en vivo tu código con el servidor | `yarn twenty dev` | Tus cambios aparecen en la UI |
|
||||
|
||||
---
|
||||
|
||||
@@ -28,7 +28,7 @@ Crea una app nueva a partir de la plantilla:
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
Se te pedirá un nombre y una descripción — pulsa **Enter** para usar los valores predeterminados. Esto genera un proyecto de TypeScript en `my-twenty-app/` con un `application-config.ts` inicial, un rol predeterminado, un flujo de trabajo de CI y una prueba de integración.
|
||||
El generador no es interactivo: el nombre del directorio se convierte en el nombre de la aplicación. Pasa `--display-name` y `--description` para personalizar los metadatos generados (también puedes editarlos más tarde en `src/constants/universal-identifiers.ts`). Esto genera un proyecto de TypeScript en `my-twenty-app/` con un `application-config.ts` inicial, un rol predeterminado, flujos de trabajo de CI/CD y una prueba de integración.
|
||||
|
||||
**Después de esta fase:** tienes el código fuente de tu app en tu máquina. Aún no se está ejecutando — esa es la Fase 2.
|
||||
|
||||
@@ -38,28 +38,14 @@ Se te pedirá un nombre y una descripción — pulsa **Enter** para usar los val
|
||||
|
||||
Tu app necesita un servidor de Twenty con el que sincronizar. El servidor es una instancia completa de Twenty — UI, API GraphQL, PostgreSQL — ejecutándose localmente en Docker. Tu código local sube sus definiciones a ese servidor, lo que hace que aparezcan en la UI.
|
||||
|
||||
El generador ofrece iniciar uno por ti:
|
||||
El generador inicia uno por ti: con Docker en ejecución, extrae la imagen `twentycrm/twenty-app-dev`, la inicia en el puerto `2020` y autentica la CLI contra el espacio de trabajo de demostración preconfigurado (`tim@apple.dev`), sin necesidad de iniciar sesión.
|
||||
|
||||
> **¿Te gustaría configurar una instancia local de Twenty?**
|
||||
|
||||
* **Sí (recomendado)** — descarga la imagen de Docker `twentycrm/twenty-app-dev` y la inicia en el puerto `2020`. Asegúrate de que Docker esté en ejecución antes.
|
||||
* **No** — elige esto si ya tienes un servidor de Twenty al que te quieres conectar. Puedes conectarlo más tarde con `yarn twenty remote:add`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="¿Debería iniciar una instancia local?" />
|
||||
</div>
|
||||
|
||||
Una vez que el servidor esté en marcha, se abrirá un navegador para iniciar sesión. Inicia sesión con la cuenta de demostración precargada:
|
||||
|
||||
* **Correo electrónico:** `tim@apple.dev`
|
||||
* **Contraseña:** `tim@apple.dev`
|
||||
Para conectarte a un servidor Twenty existente en su lugar, pasa `--url \<your-server-url>`. Los servidores remotos se autentican con OAuth: se abre un navegador para que puedas iniciar sesión y hacer clic en **Authorize**, lo que le da a la CLI acceso a tu espacio de trabajo. (También puedes optar por usar OAuth localmente con `--authentication-method oauth`: inicia sesión con `tim@apple.dev` / `tim@apple.dev`.)
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Pantalla de inicio de sesión de Twenty" />
|
||||
</div>
|
||||
|
||||
Haz clic en **Authorize** en la siguiente pantalla — esto le da a la CLI acceso a tu espacio de trabajo.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Pantalla de autorización de la CLI de Twenty" />
|
||||
</div>
|
||||
@@ -117,27 +103,31 @@ Haz clic en **View installed app** para ver la instalación en el espacio de tra
|
||||
|
||||
### Sincronización de una sola vez para CI y scripts
|
||||
|
||||
Pasa `--once` para ejecutar una sola compilación + sincronización y salir — mismo pipeline, sin watcher:
|
||||
Usa `plan` y `apply` para ejecutar la misma canalización una vez, sin observador:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
yarn twenty plan # preview the metadata changes without applying them
|
||||
yarn twenty apply # show the plan, then apply it
|
||||
```
|
||||
|
||||
| Comando | Comportamiento | Cuándo usarlo |
|
||||
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Supervisa tus archivos fuente y vuelve a sincronizar en cada cambio. Se ejecuta hasta que lo detengas. | Desarrollo local interactivo. |
|
||||
| `yarn twenty dev --once` | Realiza una sola compilación + sincronización y luego sale con el código `0` si tiene éxito o `1` si falla. | CI, hooks de pre-commit, agentes de IA, flujos de trabajo con scripts. |
|
||||
| `yarn twenty dev --once --dry-run` | Genera y muestra los cambios de metadatos **sin aplicarlos**. | Inspeccionar qué cambiaría una sincronización antes de confirmarla. |
|
||||
| Comando | Comportamiento | Cuándo usarlo |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Supervisa tus archivos fuente y vuelve a sincronizar en cada cambio. Se ejecuta hasta que lo detengas. | Desarrollo local interactivo. |
|
||||
| `yarn twenty apply` | Realiza una sola compilación + sincronización y luego sale con el código `0` si tiene éxito o `1` si falla. Pide confirmación para cambios destructivos (pasa `--force` para omitirla). | CI, hooks de pre-commit, agentes de IA, flujos de trabajo con scripts. |
|
||||
| `yarn twenty plan` | Genera y muestra los cambios de metadatos **sin aplicarlos**. | Inspeccionar qué cambiaría una sincronización antes de confirmarla. |
|
||||
|
||||
Ambos modos necesitan un remoto autenticado. Consulta [Sincronización y recuperación](/l/es/developers/extend/apps/operations/sync-and-recovery#previewing-changes-dry-run) para obtener más información sobre `--dry-run`.
|
||||
Todos los modos necesitan un remoto autenticado. Consulta [Sincronización y recuperación](/l/es/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) para obtener más información sobre `plan`.
|
||||
|
||||
<Note>
|
||||
`yarn twenty dev --once` y `yarn twenty dev --once --dry-run` son alias obsoletos de `yarn twenty apply` y `yarn twenty plan`.
|
||||
</Note>
|
||||
|
||||
### Opciones del modo de desarrollo
|
||||
|
||||
| Opción | Descripción |
|
||||
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `--once` | Compila y sincroniza una vez y luego finaliza. |
|
||||
| `--dry-run` | Con `--once`, obtén una vista previa de los cambios de metadatos sin aplicarlos. No escribe nada. |
|
||||
| `--debounceMs \<ms>` | Establece el tiempo de antirrebote para los cambios de archivo en milisegundos (valor predeterminado: `2000`). |
|
||||
| `--force` | Aplica cambios destructivos (eliminaciones) sin confirmación. |
|
||||
| `--debounceMs \<ms>` | Establece el tiempo de antirrebote para los cambios de archivo en milisegundos (valor predeterminado: `1000`). |
|
||||
| `--verbose` / `--debug` | Muestra registros de compilación detallados, solicitudes de sincronización y seguimientos de errores. |
|
||||
|
||||
## Lo que puedes crear
|
||||
|
||||
@@ -34,6 +34,10 @@ yarn twenty dev:add frontComponent
|
||||
| Vista | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
|
||||
| Elemento del menú de navegación | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| Diseño de página | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
| Pestaña Diseño de página | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
|
||||
| Elemento del menú de comandos | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
|
||||
| Campo de vista | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
|
||||
| Proveedor de conexión | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
|
||||
|
||||
## Qué genera el generador
|
||||
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@ icon: wrench
|
||||
---
|
||||
|
||||
* **Errores de Docker** — Asegúrate de que Docker Desktop (o el daemon) esté en ejecución antes de `yarn twenty docker:start`. El mensaje de error mostrará el comando de inicio correcto para tu sistema operativo.
|
||||
* **Versión de Node incorrecta** — Se requiere 24+. Compruébalo con `node -v`.
|
||||
* **Versión de Node incorrecta** — Se necesita la 24.5+ (`engines.node: ^24.5.0`). Compruébalo con `node -v`.
|
||||
* **Falta Yarn 4** — Ejecuta `corepack enable`.
|
||||
* **Dependencias rotas** — `rm -rf node_modules && yarn install`.
|
||||
* **Errores de `twenty-sdk` tras actualizar a la v2.8.0** — Pasó de `dependencies` a `devDependencies` en la v2.8.0. Consulta [Estructura del proyecto → Dependencias](/l/es/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
* **`twenty build` muestra una advertencia sobre `twenty-client-sdk` en `dependencies`** — Twenty lo proporciona en tiempo de ejecución, por lo que debería trasladarse a `devDependencies` junto con `twenty-sdk`. Consulta [Estructura del proyecto → Dependencias](/l/es/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
* **`twenty dev:build` muestra una advertencia sobre `twenty-client-sdk` en `dependencies`** — Twenty lo proporciona en tiempo de ejecución, por lo que debería trasladarse a `devDependencies` junto con `twenty-sdk`. Consulta [Estructura del proyecto → Dependencias](/l/es/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
|
||||
¿Atascado? Pide ayuda en el [Discord de Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
|
||||
@@ -13,7 +13,6 @@ export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
@@ -22,51 +21,23 @@ export default defineCommandMenuItem({
|
||||
|
||||
## Campos de configuración
|
||||
|
||||
| Campo | Obligatorio | Descripción |
|
||||
| --------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sí | ID único estable para el comando |
|
||||
| `label` | Sí | Etiqueta completa mostrada en el menú de comandos (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Sí | El `universalIdentifier` del componente de frontend que abre este comando |
|
||||
| `shortLabel` | No | Etiqueta corta mostrada en el botón de acción rápida anclado |
|
||||
| `icon` | No | Nombre del ícono mostrado junto a la etiqueta (p. ej., 'IconBolt', 'IconSend') |
|
||||
| `isPinned` | No | Cuando es `true`, muestra el comando como un botón de acción rápida en la esquina superior derecha de la página |
|
||||
| `availabilityType` | No | Controla dónde aparece el comando: 'GLOBAL' (siempre disponible), 'RECORD_SELECTION' (solo cuando hay registros seleccionados) o 'FALLBACK' (se muestra cuando ningún otro comando coincide) |
|
||||
| `availabilityObjectUniversalIdentifier` | No | Restringe el comando a páginas de un tipo de objeto específico (p. ej., solo en registros de Company) |
|
||||
| `conditionalAvailabilityExpression` | No | Una expresión booleana que controla dinámicamente la visibilidad (ver abajo) |
|
||||
| Campo | Obligatorio | Descripción |
|
||||
| --------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sí | ID único estable para el comando |
|
||||
| `label` | Sí | Etiqueta completa mostrada en el menú de comandos (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Sí | El `universalIdentifier` del componente de frontend que abre este comando |
|
||||
| `shortLabel` | No | Etiqueta corta mostrada en el botón de acción rápida anclado |
|
||||
| `icon` | No | **Obsoleto**: se ignora en favor del icono de la aplicación; la compilación emite una advertencia si se establece |
|
||||
| `isPinned` | No | Cuando es `true`, muestra el comando como un botón de acción rápida en la esquina superior derecha de la página |
|
||||
| `availabilityType` | No | Controla dónde aparece el comando: `'GLOBAL'` (siempre disponible), `'GLOBAL_OBJECT_CONTEXT'` (solo en páginas con un contexto de objeto: páginas de índice y de registro), `'RECORD_SELECTION'` (solo cuando hay registros seleccionados) o `'FALLBACK'` (se muestra cuando ningún otro comando coincide) |
|
||||
| `availabilityObjectUniversalIdentifier` | No | Restringe el comando a páginas de un tipo de objeto específico (p. ej., solo en registros de Company) |
|
||||
| `conditionalAvailabilityExpression` | No | Una expresión booleana que controla dinámicamente la visibilidad (ver abajo) |
|
||||
|
||||
## Comandos sin interfaz
|
||||
|
||||
Un elemento del menú de comandos emparejado con un [headless front component](/l/es/developers/extend/apps/layout/front-components#headless-vs-non-headless) es la forma idónea de ofrecer una acción de un solo clic: ejecutar código, navegar o confirmar y ejecutar. La página Front Components abarca los [SDK Command components](/l/es/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) que gestionan el patrón de acción y desmontaje.
|
||||
|
||||
Un flujo típico:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
Un flujo típico: un componente sin interfaz gráfica renderiza `<Command execute={...} />` (consulta el [ejemplo completo](/l/es/developers/extend/apps/layout/front-components#sdk-command-components)), y el elemento del menú de comandos lo señala:
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
@@ -74,7 +45,6 @@ import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -49,14 +49,13 @@ export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
Después de sincronizar con `yarn twenty dev` (o ejecutar una sola vez `yarn twenty dev --once`), la acción rápida aparece en la esquina superior derecha de la página:
|
||||
Después de sincronizar con `yarn twenty dev` (o ejecutar una sola vez `yarn twenty apply`), la acción rápida aparece en la esquina superior derecha de la página:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Botón de acción rápida en la esquina superior derecha" />
|
||||
@@ -88,11 +87,11 @@ Los componentes de front vienen en dos modos de renderizado controlados por la o
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
const [recordId] = useSelectedRecordIds();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
@@ -116,7 +115,7 @@ Como el componente devuelve `null`, Twenty omite renderizar un contenedor para
|
||||
|
||||
El paquete `twenty-sdk` proporciona cuatro componentes auxiliares Command diseñados para componentes de front headless. Cada componente ejecuta una acción al montarse, gestiona los errores mostrando una notificación tipo snackbar y desmonta automáticamente el componente de front al finalizar.
|
||||
|
||||
Impórtalos desde `twenty-sdk/command`:
|
||||
Impórtalos desde `twenty-sdk/front-component`:
|
||||
|
||||
* **`Command`** — Ejecuta un callback asíncrono mediante la prop `execute`.
|
||||
* **`CommandLink`** — Navega a una ruta de la aplicación. Props: `to`, `params`, `queryParams`, `options`.
|
||||
@@ -127,8 +126,8 @@ Aquí tienes un ejemplo completo de un componente de front headless que usa `Com
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { Command } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
@@ -160,7 +159,6 @@ import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
@@ -169,7 +167,7 @@ Y un ejemplo que usa `CommandModal` para pedir confirmación antes de ejecutar:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
import { CommandModal } from 'twenty-sdk/front-component';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
@@ -202,7 +200,7 @@ Los componentes de front se ejecutan en el navegador dentro de un Web Worker ais
|
||||
|
||||
Una función de lógica declarada con `httpRouteTriggerSettings` es accesible por HTTP en su ruta. Twenty inyecta en el worker la URL base desde la que se sirven tus funciones como `TWENTY_FUNCTIONS_URL`, junto con el `TWENTY_APP_ACCESS_TOKEN` que autentica la llamada. Todavía no hay un cliente SDK dedicado para invocar tus propias funciones, así que llámalas con un simple `fetch`:
|
||||
|
||||
> **En Twenty Cloud, las funciones de lógica activadas por HTTP se sirven en un dominio dedicado por espacio de trabajo** en `https://\<your-workspace-subdomain>.twenty.com\<path>` — esto es exactamente a lo que se resuelve `TWENTY_FUNCTIONS_URL`. Para clientes externos, copia la URL exacta desde la configuración de **HTTP trigger** de la función o desde la pestaña **Settings** de la aplicación.
|
||||
> **En Twenty Cloud, las funciones de lógica activadas por HTTP se sirven en un dominio dedicado por espacio de trabajo** en `https://\<your-workspace-subdomain>.withtwenty.com\<path>` — esto es exactamente a lo que se resuelve `TWENTY_FUNCTIONS_URL`. Para clientes externos, copia la URL exacta desde la configuración de **HTTP trigger** de la función o desde la pestaña **Settings** de la aplicación.
|
||||
|
||||
<Warning>
|
||||
La ruta heredada de la función `/s/` está **obsoleta** y será **desactivada el 2026-07-24**. En su lugar, utiliza `TWENTY_FUNCTIONS_URL` (arriba) y migra cualquier URL de `/s/` codificada de forma fija antes de esa fecha. La ruta `/s/` sigue disponible para autoalojamiento.
|
||||
@@ -212,7 +210,7 @@ Un componente de front sin interfaz (headless) puede ejecutar la llamada al mont
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { Command } from 'twenty-sdk/front-component';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
@@ -316,13 +314,13 @@ Dentro de tu componente, usa hooks del SDK para acceder al usuario actual, el re
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useSelectedRecordIds,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const [recordId] = useSelectedRecordIds();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
@@ -405,12 +403,11 @@ Aquí tienes un ejemplo que usa la API del host para mostrar un snackbar y cerra
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
const [recordId] = useSelectedRecordIds();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
@@ -451,10 +448,10 @@ export default defineFrontComponent({
|
||||
Usa `useSelectedRecordIds()` para manejar varios registros seleccionados. Esto es útil para operaciones por lotes:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
@@ -492,12 +489,19 @@ export default defineFrontComponent({
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Muéstralo con un [elemento de menú de comando](/l/es/developers/extend/apps/layout/command-menu-items) restringido a selecciones de registros:
|
||||
|
||||
```ts src/command-menu-items/bulk-export.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ export default defineNavigationMenuItem({
|
||||
|
||||
* `position` controla el orden en la barra lateral.
|
||||
|
||||
* El enum también contiene `NavigationMenuItemType.RECORD`, que se usa internamente para los favoritos de registros creados por el usuario; no se puede usar desde un manifiesto de aplicación (no hay ningún campo para hacer referencia a un registro).
|
||||
|
||||
* `icon` y `color` son opcionales y personalizan el aspecto de la entrada.
|
||||
|
||||
* `folderUniversalIdentifier` también está disponible en cualquier elemento para anidarlo dentro de un elemento padre de tipo `FOLDER`.
|
||||
|
||||
@@ -33,17 +33,32 @@ export default defineView({
|
||||
## Puntos clave
|
||||
|
||||
* `objectUniversalIdentifier` especifica a qué objeto se aplica esta vista. Puede ser un objeto personalizado que hayas definido o un objeto estándar de Twenty.
|
||||
* `key` determina el tipo de vista — `ViewKey.INDEX` es la vista de lista principal para el objeto.
|
||||
* `key: ViewKey.INDEX` marca la vista como la vista de lista principal del objeto (la que abre un elemento de navegación `OBJECT`).
|
||||
* `fields` controla qué columnas aparecen y en qué orden. Cada campo referencia un `fieldMetadataUniversalIdentifier`.
|
||||
* También puedes definir `filters`, `filterGroups`, `groups` y `fieldGroups` para configuraciones avanzadas.
|
||||
* También puedes declarar `filters`, `filterGroups`, `sorts`, `groups` y `fieldGroups` para configuraciones avanzadas.
|
||||
* `position` controla el orden cuando existen múltiples vistas para el mismo objeto.
|
||||
|
||||
## Propiedades opcionales
|
||||
|
||||
| Propiedad | Valores | Descripción |
|
||||
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `type` | `ViewType.TABLE` (predeterminado), `ViewType.KANBAN`, `ViewType.CALENDAR` | Cómo se presentan los registros. (`FIELDS_WIDGET` / `TABLE_WIDGET` también existen, pero son usados internamente por los widgets de diseño de página). |
|
||||
| `visibility` | `ViewVisibility.WORKSPACE` (predeterminado), `ViewVisibility.UNLISTED` | Si la vista se muestra para todo el espacio de trabajo o se oculta en los selectores. |
|
||||
| `openRecordIn` | `ViewOpenRecordIn.SIDE_PANEL` (predeterminado), `ViewOpenRecordIn.RECORD_PAGE` | Dónde se abre un registro al hacer clic en él. |
|
||||
| `criterios de ordenación` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Orden de clasificación predeterminado. |
|
||||
| `isCompact` | `boolean` | Visualización compacta de filas. |
|
||||
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Agrupar registros (por ejemplo, columnas de kanban) por un campo. |
|
||||
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Agregados y tamaño de columnas kanban. |
|
||||
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Vistas de calendario: diseño y el campo de fecha que posiciona los registros. |
|
||||
|
||||
Todos los enums anteriores se exportan desde `twenty-sdk/define`.
|
||||
|
||||
## Filtros
|
||||
|
||||
Una vista puede incluir filtros preaplicados. Cada filtro tiene tres coordenadas: el **campo** que se está filtrando, el **operando** (cómo comparar) y el **valor** (contra qué comparar). Las tres deben alinearse: usar un operando que no aplique a un tipo de campo será rechazado en el momento de la sincronización.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { ViewFilterOperand } from 'twenty-sdk/define';
|
||||
|
||||
filters: [
|
||||
{
|
||||
|
||||
@@ -51,8 +51,12 @@ export default defineLogicFunction({
|
||||
```
|
||||
|
||||
Tipos de desencadenadores disponibles:
|
||||
* **httpRoute**: Expone tu función en una ruta y método HTTP **bajo el endpoint `/s/`**:
|
||||
> p. ej., `path: '/post-card/create'` se puede invocar en `https://your-twenty-server.com/s/post-card/create`
|
||||
* **httpRoute**: Expone tu función en una ruta HTTP y método en la **URL base de las funciones de tu espacio de trabajo** — el valor Veinte inyectos como `TWENTY_FUNCTIONS_URL` (en la nube veinte, un dominio dedicado por área de trabajo):
|
||||
> p. ej., `path: '/post-card/create'` se puede invocar en `https://your-workspace.withtwenty.com/post-card/create`
|
||||
|
||||
<Warning>
|
||||
El prefijo heredado `/s/` (`https://your-twenty-server.com/s/post-card/create`) está \*\*obsoleto en 20 nubes y será desactivado en **2026-07-24**. Sigue disponible para instancias locales y autosuficientes que no configuran un dominio de funciones aisladas — use `TWENTY_FUNCTIONS_URL` cuando está definido. y vuelve a `\<server-url>/s/\<path>` de lo contrario.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Para invocar una función de lógica activada por una ruta desde un componente de frontend (headless), consulta [Llamar a una función de lógica](/l/es/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
|
||||
@@ -42,7 +42,7 @@ Una función de lógica selecciona uno o más disparadores: cada entrada a conti
|
||||
|
||||
| Disparador | Cuándo se ejecuta | Configuración |
|
||||
| ------------------------------- | --------------------------------------------------------------- | ------------------------------- |
|
||||
| **Ruta HTTP** | Una solicitud llega a tu endpoint `/s/\<path>` | `httpRouteTriggerSettings` |
|
||||
| **Ruta HTTP** | Una solicitud llega a la URL pública de tu función | `httpRouteTriggerSettings` |
|
||||
| **Cron** | Coincide una expresión CRON | `cronTriggerSettings` |
|
||||
| **Evento de base de datos** | Se crea, actualiza o elimina un registro del espacio de trabajo | `databaseEventTriggerSettings` |
|
||||
| **Herramienta de IA** | Una funcionalidad de IA de Twenty decide llamar a tu función | `toolTriggerSettings` |
|
||||
|
||||
@@ -4,7 +4,25 @@ description: Comandos de `yarn twenty` para ejecutar funciones, transmitir regis
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
Más allá de `dev`, `dev:build`, `dev:add` y `dev:typecheck`, la CLI de `yarn twenty` proporciona comandos para ejecutar funciones, ver registros y gestionar instalaciones de aplicaciones.
|
||||
La CLI de `yarn twenty` es tu interfaz para todo lo relacionado con la aplicación. Lista completa de comandos:
|
||||
|
||||
| Comando | Qué hace | Documentado en |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `dev` | Supervisar archivos fuente y sincronizar en vivo los cambios | [Inicio rápido](/l/es/developers/extend/apps/getting-started/quick-start) |
|
||||
| `plan` | Previsualizar cambios de metadatos sin aplicarlos | [Sincronización y recuperación](/l/es/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
|
||||
| `apply` | Aplicar cambios de metadatos después de mostrar el plan | [Sincronización y recuperación](/l/es/developers/extend/apps/operations/sync-and-recovery) |
|
||||
| `dev:build` | Compilar la aplicación y generar el cliente de la API (`--tarball` para empaquetar un `.tgz`) | [Publicación](/l/es/developers/extend/apps/operations/publishing) |
|
||||
| `dev:typecheck` | Ejecutar la comprobación de tipos de TypeScript | [Pruebas](/l/es/developers/extend/apps/operations/testing) |
|
||||
| `dev:add` | Crear una nueva entidad con scaffolding | [Scaffolding](/l/es/developers/extend/apps/getting-started/scaffolding) |
|
||||
| `dev:generate-client` | Regenerar el cliente de API tipado | esta página |
|
||||
| `dev:function:exec` / `dev:function:logs` | Ejecutar funciones y transmitir sus registros | esta página |
|
||||
| `dev:translations-extract` | Extraer cadenas traducibles en catálogos de `locales/` | [Traducciones](/l/es/developers/extend/apps/translations/overview) |
|
||||
| `dev:catalog-sync` | Activar la sincronización del catálogo del marketplace | [Publicación](/l/es/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
|
||||
| `app:publish` / `app:install` / `app:uninstall` | Ciclo de vida de la publicación | [Publicación](/l/es/developers/extend/apps/operations/publishing) y esta página |
|
||||
| `docker:*` | Administrar el contenedor del servidor local de Twenty | [Servidor local](/l/es/developers/extend/apps/getting-started/local-server) |
|
||||
| `remote:*` | Administrar conexiones de servidor | esta página |
|
||||
|
||||
Todos los comandos aceptan `-r, --remote \<name>` para dirigirse a un remoto específico en lugar del predeterminado.
|
||||
|
||||
## Ejecutar funciones (`yarn twenty dev:function:exec`)
|
||||
|
||||
@@ -20,8 +38,9 @@ yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
# Pass a JSON payload
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
# Execute the install hooks
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
yarn twenty dev:function:exec --preInstall
|
||||
```
|
||||
|
||||
## Ver registros de funciones (`yarn twenty dev:function:logs`)
|
||||
@@ -100,6 +119,12 @@ yarn twenty remote:list
|
||||
|
||||
# Set the active remote
|
||||
yarn twenty remote:use <name>
|
||||
|
||||
# Check that the active remote's authentication is still valid
|
||||
yarn twenty remote:status
|
||||
|
||||
# Remove a remote
|
||||
yarn twenty remote:remove <name>
|
||||
```
|
||||
|
||||
Tus credenciales se almacenan en `~/.twenty/config.json`.
|
||||
|
||||
@@ -229,7 +229,7 @@ yarn twenty dev:catalog-sync
|
||||
# yarn twenty dev:catalog-sync --remote production
|
||||
```
|
||||
|
||||
Los metadatos que se muestran en el marketplace provienen de tu configuración de `defineApplication()` — campos como `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` y `termsUrl`.
|
||||
Los metadatos que se muestran en el marketplace provienen de tu configuración de `defineApplication()`; consulta [Metadatos del marketplace](#marketplace-metadata) arriba.
|
||||
|
||||
<Note>
|
||||
Si tu aplicación no define un `aboutDescription` en `defineApplication()`, el marketplace usará automáticamente el `README.md` de tu paquete en npm como el contenido de la página Acerca de. Esto significa que puedes mantener un único README tanto para npm como para el marketplace de Twenty. Si quieres una descripción diferente en el marketplace, establece explícitamente `aboutDescription`.
|
||||
|
||||
@@ -15,33 +15,44 @@ Para la iteración local del día a día casi siempre quieres `yarn twenty dev`.
|
||||
| Quieres… | Comando | Notas |
|
||||
| --------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Iterar localmente con sincronización en tiempo real | `yarn twenty dev` | Supervisa tus archivos y sincroniza en cada cambio. |
|
||||
| Sincronizar una vez y salir (CI, scripts, hooks) | `yarn twenty dev --once` | Una compilación + sincronización, luego sale. |
|
||||
| Previsualizar cambios **sin aplicarlos** | `yarn twenty dev --once --dry-run` | Calcula e imprime el diff; no escribe nada. |
|
||||
| Sincronizar una vez y salir (CI, scripts, hooks) | `yarn twenty apply` | Una compilación + sincronización, luego sale. Añade `--force` para omitir la confirmación de cambio destructivo. |
|
||||
| Previsualizar cambios **sin aplicarlos** | `yarn twenty plan` | Calcula e imprime el diff; no escribe nada. |
|
||||
| Eliminar la aplicación del espacio de trabajo | `yarn twenty app:uninstall` | Agrega `--yes` para omitir la confirmación. |
|
||||
| Enviar un tarball a un servidor | `yarn twenty app:publish --private` | Requiere una versión de `package.json` **estrictamente superior**; consulta [Publicación](/l/es/developers/extend/apps/operations/publishing). |
|
||||
| Publicar en el marketplace (npm) | `yarn twenty app:publish` | — |
|
||||
| Instalar / actualizar una versión implementada | `yarn twenty app:install` | Instala la versión actualmente implementada. |
|
||||
| Borrar el servidor local y empezar desde cero | `yarn twenty docker:reset` | Elimina **todos** los datos locales: último recurso. |
|
||||
|
||||
<Note>
|
||||
`yarn twenty dev --once` y `yarn twenty dev --once --dry-run` siguen funcionando como alias obsoletos de `yarn twenty apply` y `yarn twenty plan`.
|
||||
</Note>
|
||||
|
||||
### La sincronización local no necesita un aumento de versión
|
||||
|
||||
La regla de `version` estrictamente creciente (`VERSION_ALREADY_EXISTS` al implementar, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` al instalar) se aplica a **`app:publish` / `app:install`**: la ruta de publicación. `yarn twenty dev` sincroniza tu manifiesto en su lugar y nunca requiere un cambio de versión, por lo que no necesitas tocar `package.json` para iterar. Si te encuentras aumentando la versión para probar un cambio local, estás usando la ruta de publicación cuando lo que quieres es el ciclo de desarrollo.
|
||||
|
||||
## Leer la salida de la sincronización
|
||||
|
||||
Cada sincronización muestra los cambios de metadatos que aplicó (o aplicaría, con `--dry-run`):
|
||||
Cada sincronización imprime los cambios de metadatos que aplicó (o aplicaría, con `plan`), al estilo de Terraform: un bloque por entidad con sus atributos y luego una línea de resumen:
|
||||
|
||||
```text filename="Terminal"
|
||||
Metadata changes: 2 created, 1 updated, 1 deleted
|
||||
created objectMetadata rocket
|
||||
created fieldMetadata timelineActivities
|
||||
updated fieldMetadata launchedAt
|
||||
deleted pageLayout legacyTab
|
||||
✓ Synced
|
||||
# objectMetadata "rocket" will be created
|
||||
+ icon = "IconRocket"
|
||||
+ labelSingular = "Rocket"
|
||||
+ ...
|
||||
|
||||
# fieldMetadata "launchedAt" will be updated
|
||||
~ isNullable = false -> true
|
||||
|
||||
Plan: 2 to add, 1 to change, 1 to destroy.
|
||||
|
||||
✓ Synced My App (4 files)
|
||||
```
|
||||
|
||||
Este es tu primer diagnóstico: te indica exactamente qué objetos, campos y diseños cambiaron, para que puedas confirmar que una sincronización hizo lo que esperabas antes de revisar la interfaz de usuario.
|
||||
|
||||
Los cambios destructivos (`to destroy`) se enumeran con lo que eliminan (p. ej., `objectMetadata "auditNote" — drops the table and all its rows`) y requieren confirmación interactiva, o `--force` en scripts.
|
||||
|
||||
Cuando una sincronización falla en una sola entidad, el error nombra la entidad implicada y su `universalIdentifier`, por ejemplo:
|
||||
|
||||
```text
|
||||
@@ -50,39 +61,42 @@ Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337)
|
||||
|
||||
Usa ese identificador para encontrar la entidad en tu manifiesto (y, si es necesario, en el espacio de trabajo) en lugar de adivinar cuál entra en conflicto.
|
||||
|
||||
## Previsualizar cambios (simulación)
|
||||
## Previsualizar cambios (plan)
|
||||
|
||||
`yarn twenty dev --once --dry-run` compila tu manifiesto, le pide al servidor el plan de migración y lo imprime, **sin aplicar nada**. Es la forma segura de responder "¿qué cambiaría esta sincronización?" antes de comprometerte a ella.
|
||||
`yarn twenty plan` compila tu manifiesto, le pide al servidor el plan de migración y lo imprime, **sin aplicar nada**. Es la forma segura de responder "¿qué cambiaría esta sincronización?" antes de comprometerte a ella.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once --dry-run
|
||||
yarn twenty plan
|
||||
```
|
||||
|
||||
```text filename="Terminal"
|
||||
Building manifest...
|
||||
Computing metadata diff (dry run, nothing will be applied)...
|
||||
Metadata changes: 1 created, 1 updated
|
||||
created fieldMetadata timelineActivities
|
||||
updated objectMetadata rocket
|
||||
✓ Dry run complete for My App — no changes were applied
|
||||
Computing metadata plan (read-only, nothing will be applied)...
|
||||
|
||||
# fieldMetadata "timelineActivities" will be created
|
||||
+ ...
|
||||
|
||||
Plan: 1 to add, 1 to change, 0 to destroy.
|
||||
|
||||
✓ Plan complete for My App — no changes were applied
|
||||
```
|
||||
|
||||
Una simulación:
|
||||
Un plan:
|
||||
|
||||
* **No escribe nada**: sin migración de metadatos, sin actualización del registro de la aplicación, sin cambios de roles/pestañas predeterminados y sin generación del cliente de la API.
|
||||
* Devuelve el **mismo diff** que aplicaría una sincronización real, para que puedas revisar por adelantado las entidades creadas/actualizadas/eliminadas.
|
||||
* Es útil antes de un cambio arriesgado, al revisar un cambio generado por IA o en un script que deba fallar si está a punto de producirse un cambio inesperado.
|
||||
|
||||
<Note>
|
||||
Una simulación solo previsualiza cambios de **metadatos** y requiere que la aplicación se haya sincronizado al menos una vez (para que el espacio de trabajo la conozca). Si la ejecutas con una aplicación que nunca se sincronizó, el servidor indicará que la aplicación no está instalada; ejecuta `yarn twenty dev` una vez primero.
|
||||
Un plan solo previsualiza cambios de **metadatos** y requiere que la aplicación se haya sincronizado al menos una vez (para que el espacio de trabajo la conozca). Si la ejecutas con una aplicación que nunca se sincronizó, el servidor indicará que la aplicación no está instalada; ejecuta `yarn twenty dev` una vez primero.
|
||||
</Note>
|
||||
|
||||
## Escalera de recuperación
|
||||
|
||||
Cuando los metadatos locales parezcan incorrectos, ve escalando en este orden y detente en cuanto te hayas desbloqueado. Cada paso es más disruptivo que el anterior.
|
||||
|
||||
1. **Volver a sincronizar.** Ejecuta `yarn twenty dev --once` de nuevo. Las sincronizaciones son idempotentes: volver a ejecutar un manifiesto limpio es seguro y suele resolver un problema transitorio.
|
||||
2. **Previsualizar el plan.** Ejecuta `yarn twenty dev --once --dry-run` para ver exactamente qué pretende cambiar la siguiente sincronización, sin aplicarlo.
|
||||
1. **Volver a sincronizar.** Ejecuta `yarn twenty apply` de nuevo. Las sincronizaciones son idempotentes: volver a ejecutar un manifiesto limpio es seguro y suele resolver un problema transitorio.
|
||||
2. **Previsualizar el plan.** Ejecuta `yarn twenty plan` para ver exactamente qué pretende cambiar la siguiente sincronización, sin aplicarlo.
|
||||
3. Lee el error identificado. Un conflicto suele señalar un identificador duplicado o reutilizado.
|
||||
4. **Desinstalar y volver a instalar.** `yarn twenty app:uninstall`, luego vuelve a sincronizar (`yarn twenty dev`). Esto reconstruye los metadatos de la aplicación desde cero manteniendo intacto el resto de tu espacio de trabajo.
|
||||
5. **Restablecimiento completo (último recurso).** `yarn twenty docker:reset`, luego vuelve a sembrar los datos y a sincronizar.
|
||||
|
||||
@@ -78,6 +78,13 @@ Crea un `vitest.config.ts` en la raíz de tu aplicación:
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
|
||||
|
||||
// Make env vars available to globalSetup (test.env only applies to workers)
|
||||
process.env.TWENTY_API_URL = TWENTY_API_URL;
|
||||
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
@@ -88,66 +95,74 @@ export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
fileParallelism: false,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
globalSetup: ['src/__tests__/global-setup.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
TWENTY_API_URL,
|
||||
TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Crea un archivo de configuración que verifique que el servidor es accesible antes de ejecutar las pruebas:
|
||||
Crea un archivo de configuración global que verifique que el servidor es accesible, escriba una configuración de prueba para el SDK (`~/.twenty/config.test.json`) y sincronice la aplicación antes de que se ejecuten las pruebas:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
```ts src/__tests__/global-setup.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
|
||||
export async function setup() {
|
||||
const apiUrl = process.env.TWENTY_API_URL!;
|
||||
const apiKey = process.env.TWENTY_API_KEY!;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
const response = await fetch(`${apiUrl}/healthz`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
path.join(CONFIG_DIR, 'config.test.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
remotes: { local: { apiUrl, apiKey } },
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
|
||||
// Start from a clean slate, then sync the app
|
||||
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
||||
|
||||
const result = await appDevOnce({ appPath: APP_PATH });
|
||||
if (!result.success) {
|
||||
throw new Error(`Dev sync failed: ${result.error?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
}
|
||||
```
|
||||
|
||||
## APIs programáticas del SDK
|
||||
|
||||
La subruta `twenty-sdk/cli` exporta funciones que puedes invocar directamente desde el código de pruebas:
|
||||
|
||||
| Función | Descripción |
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| `appBuild` | Compilar la aplicación y opcionalmente empaquetar un tarball |
|
||||
| `appDeploy` | Subir un tarball al servidor |
|
||||
| `appInstall` | Instalar la aplicación en el espacio de trabajo activo |
|
||||
| `appUninstall` | Desinstalar la aplicación del espacio de trabajo activo |
|
||||
| Función | Descripción |
|
||||
| -------------- | -------------------------------------------------------------------------- |
|
||||
| `appBuild` | Compilar la aplicación y opcionalmente empaquetar un tarball |
|
||||
| `appDeploy` | Subir un tarball al servidor |
|
||||
| `appDevOnce` | Compila y sincroniza la aplicación una vez (igual que `yarn twenty apply`) |
|
||||
| `appInstall` | Instalar la aplicación en el espacio de trabajo activo |
|
||||
| `appUninstall` | Desinstalar la aplicación del espacio de trabajo activo |
|
||||
|
||||
Cada función devuelve un objeto de resultado con `success: boolean` y `data` o `error`.
|
||||
|
||||
@@ -238,64 +253,10 @@ También puedes ejecutar la comprobación de tipos en tu aplicación sin ejecuta
|
||||
yarn twenty dev:typecheck
|
||||
```
|
||||
|
||||
Esto ejecuta `tsc --noEmit` e informa cualquier error de tipo.
|
||||
Esto ejecuta `tsc --noEmit` contra el `tsconfig.json` de tu aplicación e informa cualquier error de tipo. Las aplicaciones generadas también incluyen un script `yarn typecheck` que también cubre los archivos de prueba (`tsconfig.spec.json`).
|
||||
|
||||
## CI con GitHub Actions
|
||||
|
||||
El generador crea un flujo de trabajo de GitHub Actions listo para usar en `.github/workflows/ci.yml`. Ejecuta tus pruebas de integración automáticamente en cada push a `main` y en los pull requests.
|
||||
El generador crea un flujo de trabajo listo para usar en `.github/workflows/ci.yml`. En cada push a `main` y en cada pull request, inicia un servidor efímero de Twenty en el runner (mediante la acción `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`), luego ejecuta `yarn lint`, `yarn typecheck`, `yarn test:unit` y `yarn test` con `TWENTY_API_URL` / `TWENTY_API_KEY` apuntando a ese servidor. No se requieren secretos y puedes fijar la versión del servidor mediante la variable de entorno `TWENTY_VERSION` en la parte superior del flujo de trabajo.
|
||||
|
||||
El flujo de trabajo:
|
||||
|
||||
1. Obtiene tu código
|
||||
2. Inicia un servidor temporal de Twenty usando la acción `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Instala las dependencias con `yarn install --immutable`
|
||||
4. Ejecuta `yarn test` con `TWENTY_API_URL` y `TWENTY_API_KEY` inyectados a partir de las salidas de la acción
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
No necesitas configurar secretos: la acción `spawn-twenty-docker-image` inicia un servidor efímero de Twenty directamente en el runner y devuelve los detalles de conexión. El secreto `GITHUB_TOKEN` lo proporciona GitHub automáticamente.
|
||||
|
||||
Para fijar una versión específica de Twenty en lugar de `latest`, cambia la variable de entorno `TWENTY_VERSION` al inicio del flujo de trabajo.
|
||||
Consulta [Publicación → CI/CD automatizado](/l/es/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) para ver una guía completa de ambos flujos de trabajo generados (`ci.yml` y la canalización de despliegue `cd.yml`).
|
||||
|
||||
+7
-3
@@ -91,9 +91,11 @@ const GenerateDocumentForm = () => {
|
||||
}, []);
|
||||
|
||||
const generate = async () => {
|
||||
const apiBaseUrl = process.env.TWENTY_API_URL;
|
||||
// Prefer the injected functions URL; fall back to the legacy /s prefix (self-hosted/local)
|
||||
const functionsBaseUrl =
|
||||
process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`;
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
|
||||
const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
|
||||
const res = await fetch(`${functionsBaseUrl}/documents/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ templateId, recordId }),
|
||||
@@ -185,7 +187,9 @@ const DocumentViewer = () => {
|
||||
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
|
||||
// ...load { content, file } for recordId, then derive the links:
|
||||
const pdfUrl = document.file?.[0]?.url;
|
||||
const webUrl = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?id=${recordId}`;
|
||||
const functionsBaseUrl =
|
||||
process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL ?? ''}/s`;
|
||||
const webUrl = `${functionsBaseUrl}/documents/view?id=${recordId}`;
|
||||
|
||||
// Render the template body, plus quick links to the web page and the PDF.
|
||||
// Links open in a new tab so they don't navigate the embedded component.
|
||||
|
||||
+9
-2
@@ -9,8 +9,15 @@ El mismo manejador también puede responder a peticiones HTTP. Añadiremos dos r
|
||||
* un endpoint **POST** para generar un documento, y
|
||||
* un endpoint público **GET** que renderiza un documento como una página web imprimible.
|
||||
|
||||
Ambos usan `httpRouteTriggerSettings`. Las rutas de la aplicación se sirven bajo `/s` en tu servidor
|
||||
Veenty (por ejemplo, `http://localhost:2020/s/documents/generate`).
|
||||
Ambos usan `httpRouteTriggerSettings`. En el servidor dev local, las rutas de la aplicación son
|
||||
servidas bajo el prefijo `/s` (por ejemplo, `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
<Note>
|
||||
En 20 nubes las rutas se sirven en el dominio
|
||||
de funciones dedicadas del espacio de trabajo — la URL Veinte inyectos como `TWENTY_FUNCTIONS_URL`, sin prefijo `/s`. El prefijo
|
||||
`/s` está obsoleto allí y sólo permanece para instancias locales y autoalojadas.
|
||||
Ver [Llamar a una función lógica](/l/es/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
|
||||
## Ruta POST — generar bajo demanda
|
||||
|
||||
|
||||
+3
-3
@@ -77,11 +77,11 @@ Ejecuta las mismas puertas que CI hace:
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
yarn twenty plan # preview the metadata diff
|
||||
```
|
||||
|
||||
La ejecución seca imprime exactamente lo que cambiaría en el servidor sin aplicarlo —
|
||||
una buena comprobación final de sanidad. Ver
|
||||
El plan muestra exactamente qué cambiaría en el servidor sin aplicarlos —
|
||||
una buena comprobación final. Ver
|
||||
[Testing](/l/es/developers/extend/apps/operations/testing) y
|
||||
[Sincronizando y recuperando](/l/es/developers/extend/apps/operations/sync-and-recovery).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user