i18n - docs translations (#22335)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22335?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
ba8e1bf5a3
commit
10e0f1d29f
@@ -1,104 +0,0 @@
|
||||
---
|
||||
title: Arquitetura
|
||||
description: Como as aplicações Twenty funcionam — sandboxing, ciclo de vida e os blocos de construção.
|
||||
icon: sitemap
|
||||
---
|
||||
|
||||
As aplicações Twenty são pacotes TypeScript que estendem seu espaço de trabalho com objetos personalizados, lógica, componentes de UI e recursos de IA. Elas são executadas na plataforma Twenty com sandboxing completo e controles de permissão.
|
||||
|
||||
## Como as aplicações funcionam
|
||||
|
||||
Uma aplicação é uma coleção de **entidades** declaradas usando funções `defineEntity()` do pacote `twenty-sdk`. O SDK detecta essas declarações via análise de AST no momento da compilação e produz um **manifesto** — uma descrição completa do que seu aplicativo adiciona a um espaço de trabalho.
|
||||
|
||||
```
|
||||
your-app/
|
||||
├── src/
|
||||
│ ├── application-config.ts ← defineApplication (required, one per app)
|
||||
│ ├── roles/ ← defineRole
|
||||
│ ├── objects/ ← defineObject
|
||||
│ ├── fields/ ← defineField
|
||||
│ ├── logic-functions/ ← defineLogicFunction
|
||||
│ ├── front-components/ ← defineFrontComponent
|
||||
│ ├── skills/ ← defineSkill
|
||||
│ ├── agents/ ← defineAgent
|
||||
│ ├── views/ ← defineView
|
||||
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
|
||||
│ └── page-layouts/ ← definePageLayout
|
||||
├── public/ ← Static assets (images, icons)
|
||||
└── package.json
|
||||
```
|
||||
|
||||
<Note>
|
||||
**A organização de arquivos fica a seu critério.** A detecção de entidades é baseada em AST — o SDK encontra chamadas a `export default defineEntity(...)` independentemente de onde o arquivo esteja. A estrutura de pastas acima é uma convenção, não um requisito.
|
||||
</Note>
|
||||
|
||||
## Tipos de entidade
|
||||
|
||||
| Entidade | Finalidade | Documentação |
|
||||
| ----------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| **Aplicação** | Identidade da aplicação, permissões, variáveis | [Modelo de Dados](/l/pt/developers/extend/apps/data-model) |
|
||||
| **Papel** | Conjuntos de permissões para objetos e campos | [Modelo de Dados](/l/pt/developers/extend/apps/data-model) |
|
||||
| **Objeto** | Tabelas de dados personalizadas com campos | [Modelo de Dados](/l/pt/developers/extend/apps/data-model) |
|
||||
| **Campo** | Estender objetos existentes, definir relações | [Modelo de Dados](/l/pt/developers/extend/apps/data-model) |
|
||||
| **Função lógica** | TypeScript no lado do servidor com gatilhos | [Funções lógicas](/l/pt/developers/extend/apps/logic-functions) |
|
||||
| **Componente de front-end** | UI React em sandbox na página do Twenty | [Componentes de front-end](/l/pt/developers/extend/apps/front-components) |
|
||||
| **Habilidade** | Instruções reutilizáveis para agentes de IA | [Habilidades e Agentes](/l/pt/developers/extend/apps/skills-and-agents) |
|
||||
| **Agente** | Assistentes de IA com prompts personalizados | [Habilidades e Agentes](/l/pt/developers/extend/apps/skills-and-agents) |
|
||||
| **Vista** | Vistas de lista de registros pré-configuradas | [Layout](/l/pt/developers/extend/apps/layout) |
|
||||
| **Item do menu de navegação** | Entradas personalizadas na barra lateral | [Layout](/l/pt/developers/extend/apps/layout) |
|
||||
| **Layout da Página** | Abas e widgets personalizados nas páginas de registro | [Layout](/l/pt/developers/extend/apps/layout) |
|
||||
|
||||
## Sandboxing
|
||||
|
||||
* **Funções lógicas** são executadas em processos Node.js isolados no servidor. Elas acessam dados apenas por meio do cliente de API tipado, restrito às permissões do papel do aplicativo.
|
||||
* **Componentes de front-end** executam em Web Workers usando Remote DOM — isolados da página principal, mas renderizando elementos DOM nativos (não iframes). Eles se comunicam com o Twenty por meio de uma API de host com passagem de mensagens.
|
||||
* **Permissões** são aplicadas no nível da API. O token de tempo de execução (`TWENTY_APP_ACCESS_TOKEN`) é derivado do papel definido em `defineApplication()`.
|
||||
|
||||
## Ciclo de vida do aplicativo
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Development │
|
||||
│ npx create-twenty-app → yarn twenty dev (live sync) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Build & Deploy │
|
||||
│ yarn twenty build → yarn twenty deploy │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Install flow │
|
||||
│ upload → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Publish │
|
||||
│ npm publish → appears in Twenty marketplace │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
* **`yarn twenty dev`** — observa seus arquivos-fonte e sincroniza ao vivo as alterações com um servidor Twenty conectado. O cliente de API tipado é regenerado automaticamente quando o esquema muda.
|
||||
* **`yarn twenty build`** — compila TypeScript, empacota funções de lógica e componentes de front-end com o esbuild e produz um manifesto.
|
||||
* **Hooks de pré/pós-instalação** — funções de lógica opcionais que são executadas durante a instalação. Veja [Funções de Lógica](/l/pt/developers/extend/apps/logic-functions) para detalhes.
|
||||
|
||||
## Próximos passos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Modelo de dados" icon="database" href="/l/pt/developers/extend/apps/data-model">
|
||||
Defina objetos, campos, papéis e relações.
|
||||
</Card>
|
||||
<Card title="Funções lógicas" icon="bolt" href="/l/pt/developers/extend/apps/logic-functions">
|
||||
Funções no lado do servidor com gatilhos HTTP, cron e de eventos.
|
||||
</Card>
|
||||
<Card title="Componentes de front-end" icon="window-maximize" href="/l/pt/developers/extend/apps/front-components">
|
||||
Componentes React em sandbox dentro da UI do Twenty.
|
||||
</Card>
|
||||
<Card title="Layout" icon="table-columns" href="/l/pt/developers/extend/apps/layout">
|
||||
Vistas, itens de navegação e layouts de página de registro.
|
||||
</Card>
|
||||
<Card title="Habilidades e agentes" icon="robot" href="/l/pt/developers/extend/apps/skills-and-agents">
|
||||
Habilidades e agentes de IA com prompts personalizados.
|
||||
</Card>
|
||||
<Card title="CLI e Testes" icon="terminal" href="/l/pt/developers/extend/apps/cli-and-testing">
|
||||
Comandos de CLI, testes, assets, remotes e CI.
|
||||
</Card>
|
||||
<Card title="Publicação" icon="rocket" href="/l/pt/developers/extend/apps/publishing">
|
||||
Implante em um servidor ou publique no marketplace.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,434 +0,0 @@
|
||||
---
|
||||
title: CLI e Testes
|
||||
description: Comandos de CLI, configuração de testes, assets públicos, pacotes do npm, remotes e configuração de CI.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## Recursos públicos (pasta `public/`)
|
||||
|
||||
A pasta `public/` na raiz do seu app contém arquivos estáticos — imagens, ícones, fontes ou quaisquer outros recursos de que seu app precisa em tempo de execução. Esses arquivos são incluídos automaticamente nas compilações, sincronizados durante o modo de desenvolvimento e enviados para o servidor.
|
||||
|
||||
Arquivos colocados em `public/` são:
|
||||
|
||||
* **Publicamente acessíveis** — depois de sincronizados com o servidor, os recursos são servidos em uma URL pública. Não é necessária autenticação para acessá-los.
|
||||
* **Disponíveis em componentes de front-end** — use URLs de recursos para exibir imagens, ícones ou qualquer mídia dentro de seus componentes React.
|
||||
* **Disponíveis em funções lógicas** — referencie URLs de recursos em e-mails, respostas de API ou qualquer lógica no lado do servidor.
|
||||
* **Usados para metadados do marketplace** — os campos `logoUrl` e `screenshots` em `defineApplication()` referenciam arquivos desta pasta (por exemplo, `public/logo.png`). Eles são exibidos no marketplace quando seu app é publicado.
|
||||
* **Sincronizados automaticamente no modo de desenvolvimento** — quando você adiciona, atualiza ou exclui um arquivo em `public/`, ele é sincronizado automaticamente com o servidor. Não é necessário reiniciar.
|
||||
* **Incluídos nas compilações** — `yarn twenty build` agrupa todos os recursos públicos na saída de distribuição.
|
||||
|
||||
### Acessando recursos públicos com `getPublicAssetUrl`
|
||||
|
||||
Use o helper `getPublicAssetUrl` de `twenty-sdk` para obter a URL completa de um arquivo no seu diretório `public/`. Funciona tanto em **funções lógicas** quanto em **componentes de front-end**.
|
||||
|
||||
**Em uma função lógica:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
|
||||
|
||||
// Fetch the file content (no auth required — public endpoint)
|
||||
const response = await fetch(invoiceUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return { logoUrl, size: buffer.byteLength };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-...',
|
||||
name: 'send-invoice',
|
||||
description: 'Sends an invoice with the app logo',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Em um componente de front-end:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
export default defineFrontComponent(() => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
});
|
||||
```
|
||||
|
||||
O argumento `path` é relativo à pasta `public/` do seu app. Tanto `getPublicAssetUrl('logo.png')` quanto `getPublicAssetUrl('public/logo.png')` resolvem para a mesma URL — o prefixo `public/` é removido automaticamente, se presente.
|
||||
|
||||
## Usando pacotes npm
|
||||
|
||||
Você pode instalar e usar qualquer pacote npm no seu app. Tanto funções lógicas quanto componentes de front-end são empacotados com [esbuild](https://esbuild.github.io/), que incorpora todas as dependências na saída — nenhum `node_modules` é necessário em tempo de execução.
|
||||
|
||||
### Instalando um pacote
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add axios
|
||||
```
|
||||
|
||||
Em seguida, importe-o no seu código:
|
||||
|
||||
```ts src/logic-functions/fetch-data.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import axios from 'axios';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const { data } = await axios.get('https://api.example.com/data');
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-data',
|
||||
description: 'Fetches data from an external API',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
O mesmo vale para componentes de front-end:
|
||||
|
||||
```tsx src/front-components/chart.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DateWidget = () => {
|
||||
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'date-widget',
|
||||
component: DateWidget,
|
||||
});
|
||||
```
|
||||
|
||||
### Como o empacotamento funciona
|
||||
|
||||
A etapa de build usa o esbuild para produzir um único arquivo independente por função lógica e por componente de front-end. Todos os pacotes importados são incorporados ao bundle.
|
||||
|
||||
**Funções lógicas** são executadas em um ambiente Node.js. Módulos nativos do Node (`fs`, `path`, `crypto`, `http`, etc.) estão disponíveis e não precisam ser instalados.
|
||||
|
||||
**Componentes de front-end** são executados em um Web Worker. Módulos nativos do Node **não** estão disponíveis — apenas APIs do navegador e pacotes npm que funcionam em um ambiente de navegador.
|
||||
|
||||
Ambos os ambientes têm `twenty-client-sdk/core` e `twenty-client-sdk/metadata` disponíveis como módulos pré-fornecidos — eles não são empacotados, mas resolvidos em tempo de execução pelo servidor.
|
||||
|
||||
## Testando seu aplicativo
|
||||
|
||||
O SDK fornece APIs programáticas que permitem compilar, implantar, instalar e desinstalar seu aplicativo a partir de código de teste. Em conjunto com [Vitest](https://vitest.dev/) e os clientes de API tipados, você pode escrever testes de integração que verificam que seu aplicativo funciona de ponta a ponta em um servidor Twenty real.
|
||||
|
||||
### Configuração
|
||||
|
||||
O aplicativo gerado pelo scaffolder já inclui o Vitest. Se você configurá-lo manualmente, instale as dependências:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D vitest vite-tsconfig-paths
|
||||
```
|
||||
|
||||
Crie um `vitest.config.ts` na raiz do seu aplicativo:
|
||||
|
||||
```ts vitest.config.ts
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Crie um arquivo de configuração que verifique se o servidor está acessível antes da execução dos testes:
|
||||
|
||||
```ts src/__tests__/setup-test.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');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### APIs programáticas do SDK
|
||||
|
||||
O subcaminho `twenty-sdk/cli` exporta funções que você pode chamar diretamente a partir do código de teste:
|
||||
|
||||
| Função | Descrição |
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| `appBuild` | Compilar o aplicativo e, opcionalmente, empacotar um tarball |
|
||||
| `appDeploy` | Enviar um tarball para o servidor |
|
||||
| `appInstall` | Instalar o aplicativo no espaço de trabalho ativo |
|
||||
| `appUninstall` | Desinstalar o aplicativo do espaço de trabalho ativo |
|
||||
|
||||
Cada função retorna um objeto de resultado com `success: boolean` e `data` ou `error`.
|
||||
|
||||
### Escrevendo um teste de integração
|
||||
|
||||
Aqui está um exemplo completo que compila, implanta e instala o aplicativo e, em seguida, verifica se ele aparece no espaço de trabalho:
|
||||
|
||||
```ts src/__tests__/app-install.integration-test.ts
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(`Install failed: ${installResult.error?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
});
|
||||
|
||||
it('should find the installed app in the workspace', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(app: { universalIdentifier: string }) =>
|
||||
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Executando testes
|
||||
|
||||
Certifique-se de que seu servidor Twenty local esteja em execução e, em seguida:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test
|
||||
```
|
||||
|
||||
Ou no modo watch durante o desenvolvimento:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
### Verificação de tipos
|
||||
|
||||
Você também pode executar a verificação de tipos no seu aplicativo sem executar os testes:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty typecheck
|
||||
```
|
||||
|
||||
Isso executa `tsc --noEmit` e informa quaisquer erros de tipo.
|
||||
|
||||
## Referência da CLI
|
||||
|
||||
Além de `dev`, `build`, `add` e `typecheck`, a CLI fornece comandos para executar funções, visualizar logs e gerenciar instalações de aplicativos.
|
||||
|
||||
### Executando funções (`yarn twenty exec`)
|
||||
|
||||
Execute manualmente uma função de lógica sem acioná-la via HTTP, cron ou evento de banco de dados:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Execute by function name
|
||||
yarn twenty exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
### Visualizando logs de funções (`yarn twenty logs`)
|
||||
|
||||
Transmita os logs de execução das funções de lógica do seu aplicativo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Stream all function logs
|
||||
yarn twenty logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
Isso é diferente de `yarn twenty server logs`, que mostra os logs do contêiner Docker. `yarn twenty logs` mostra os logs de execução de funções do seu aplicativo a partir do servidor Twenty.
|
||||
</Note>
|
||||
|
||||
### Desinstalando um aplicativo (`yarn twenty uninstall`)
|
||||
|
||||
Remova seu aplicativo do espaço de trabalho ativo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty uninstall --yes
|
||||
```
|
||||
|
||||
## Gerenciando remotos
|
||||
|
||||
Um **remoto** é um servidor Twenty ao qual seu aplicativo se conecta. Durante a configuração, o gerador de scaffold cria um para você automaticamente. Você pode adicionar mais remotos ou alternar entre eles a qualquer momento.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
Suas credenciais são armazenadas em `~/.twenty/config.json`.
|
||||
|
||||
## CI com GitHub Actions
|
||||
|
||||
O gerador de scaffold cria um workflow do GitHub Actions pronto para uso em `.github/workflows/ci.yml`. Ele executa seus testes de integração automaticamente a cada push para `main` e em pull requests.
|
||||
|
||||
O workflow:
|
||||
|
||||
1. Faz checkout do seu código
|
||||
2. Inicializa um servidor Twenty temporário usando a ação `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Instala as dependências com `yarn install --immutable`
|
||||
4. Executa `yarn test` com `TWENTY_API_URL` e `TWENTY_API_KEY` injetados a partir das saídas da ação
|
||||
|
||||
```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 }}
|
||||
```
|
||||
|
||||
Você não precisa configurar nenhum segredo — a ação `spawn-twenty-docker-image` inicia um servidor Twenty efêmero diretamente no runner e fornece os detalhes de conexão. O segredo `GITHUB_TOKEN` é fornecido automaticamente pelo GitHub.
|
||||
|
||||
Para fixar uma versão específica do Twenty em vez de `latest`, altere a variável de ambiente `TWENTY_VERSION` no topo do workflow.
|
||||
@@ -1,193 +0,0 @@
|
||||
---
|
||||
title: Conexões
|
||||
description: Permita que seu aplicativo aja em nome de um usuário em serviços de terceiros via OAuth.
|
||||
icon: plug
|
||||
---
|
||||
|
||||
Conexões são credenciais que um usuário mantém para um serviço externo (Linear, GitHub, Slack, ...). Seu app declara **como** essas credenciais são obtidas — um **provedor de conexão** — e as consome em tempo de execução para fazer chamadas autenticadas à API de terceiros.
|
||||
|
||||
Atualmente, apenas o OAuth 2.0 tem suporte. Tipos de credenciais futuros (tokens de acesso pessoal, chaves de API, autenticação básica) serão conectados à mesma interface — apps que já usam `defineConnectionProvider({ type: 'oauth', ... })` não precisarão migrar.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="Declare como as conexões do seu app são obtidas">
|
||||
|
||||
Um provedor de conexão descreve o handshake OAuth de que seu app precisa. O usuário clica em "Adicionar conexão" nas configurações do seu app, conclui a tela de consentimento do provedor e uma linha `ConnectedAccount` é criada no seu workspace.
|
||||
|
||||
Uma configuração funcional precisa de **dois arquivos** — o provedor de conexão e uma declaração correspondente de `serverVariables` em `defineApplication` que contém as credenciais do cliente OAuth.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
defaultRoleUniversalIdentifier: '...',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* `name` é a string de identificador exclusivo usada em `listConnections({ providerName })` (kebab-case, deve corresponder a `^[a-z][a-z0-9-]*$`).
|
||||
* `displayName` aparece na aba de configurações do app e na lista de ferramentas de IA.
|
||||
* `clientIdVariable` / `clientSecretVariable` são **nomes**, não valores — devem corresponder às chaves declaradas em `defineApplication.serverVariables`. Os `client_id` e `client_secret` reais são inseridos pelo administrador do servidor por meio da interface de registro do app e nunca são versionados no seu repositório.
|
||||
* Use `serverVariables` (não `applicationVariables`) — as credenciais OAuth são do servidor como um todo e há um app OAuth por servidor do Twenty.
|
||||
* Até que ambos os `serverVariables` sejam preenchidos, a aba de configurações do app mostra uma dica "precisa de administrador do servidor" e o botão "Adicionar conexão" fica desativado.
|
||||
* `type: 'oauth'` é o único valor compatível atualmente. O discriminador é compatível com versões futuras: tipos futuros (`'pat'`, `'api-key'`, ...) adicionarão novos blocos de subconfiguração ao lado de `oauth`.
|
||||
|
||||
O URL de callback do OAuth que seu provedor precisa adicionar à lista de permissões é:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/apps/oauth/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="Use conexões a partir de uma função de lógica">
|
||||
|
||||
Dentro de um handler de função de lógica, `listConnections({ providerName })` retorna as linhas `ConnectedAccount` deste app para o provedor fornecido, com tokens de acesso atualizados.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
Cada conexão tem:
|
||||
|
||||
| Campo | Descrição |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | ID de linha exclusivo; passe para `getConnection(id)` para buscar novamente um único registro |
|
||||
| `visibilidade` | `'user'` (privada para um membro do workspace) ou `'workspace'` (compartilhada com todos os membros) |
|
||||
| `escopos` | Permissões OAuth concedidas pelo provedor de origem (distintas de `visibility` — não têm relação) |
|
||||
| `userWorkspaceId` | O id de userWorkspace do proprietário — útil para selecionar "a conexão do usuário da requisição" em gatilhos de rota HTTP |
|
||||
| `accessToken` | Token de acesso OAuth recente (atualizado automaticamente se estiver expirado) |
|
||||
| `name` / `handle` | O nome de exibição da conexão (derivado automaticamente no callback do OAuth, renomeável pelo usuário) |
|
||||
| `authFailedAt` | Definido quando a atualização mais recente falhou; o usuário deve reconectar |
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* Passe `{ providerName }` para filtrar por provedor; omita para obter todas as conexões que este app possui em todos os provedores.
|
||||
* O servidor atualiza transparentemente o token de acesso antes de retornar. Seu handler sempre vê um token utilizável (ou `authFailedAt` definido).
|
||||
* `getConnection(id)` é o equivalente de uma única linha.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Visibilidade por usuário vs. compartilhada no workspace" description="Como os usuários escolhem entre credenciais privadas e compartilhadas">
|
||||
|
||||
Quando um usuário clica em "Adicionar conexão", é solicitado que escolha uma visibilidade:
|
||||
|
||||
* **Apenas para mim** — a credencial é privada para o usuário que a conectou. Qualquer função de lógica chamada em seu nome (gatilho de rota HTTP com `isAuthRequired: true`) a vê; gatilhos cron e eventos de banco de dados não.
|
||||
* **Compartilhada no workspace** — qualquer membro do workspace pode usar a credencial. Gatilhos de cron / banco de dados também a veem, pois não há um usuário da requisição.
|
||||
|
||||
Use a adequada para cada handler:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
Várias conexões por (usuário, provedor) são permitidas, então o mesmo usuário pode manter "Linear pessoal" e "Linear de trabalho" lado a lado.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configuração única do provedor" description="Registre seu app OAuth no serviço de terceiros">
|
||||
|
||||
Para cada provedor de conexão, o administrador do servidor precisa primeiro registrar um app OAuth no serviço de terceiros.
|
||||
|
||||
1. Acesse as configurações de desenvolvedor do provedor (por exemplo, https://linear.app/settings/api/applications/new).
|
||||
2. Defina a **URI de redirecionamento** como `\<SERVER_URL>/apps/oauth/callback`.
|
||||
3. Copie o **ID do cliente** e o **Segredo do cliente** gerados.
|
||||
4. Abra o app instalado no Twenty como administrador do servidor → defina os valores nos `serverVariables` correspondentes.
|
||||
5. Os membros do workspace podem então adicionar conexões na seção **Conexões** de cada app.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -1,493 +0,0 @@
|
||||
---
|
||||
title: Modelo de dados
|
||||
description: Defina objetos, campos, papéis e metadados da aplicação com o Twenty SDK.
|
||||
icon: database
|
||||
---
|
||||
|
||||
O pacote `twenty-sdk` fornece funções `defineEntity` para declarar o modelo de dados da sua aplicação. Você deve usar `export default defineEntity({...})` para que o SDK detecte suas entidades. Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
|
||||
|
||||
<Note>
|
||||
**A organização de arquivos fica a seu critério.**
|
||||
A detecção de entidades é baseada em AST — o SDK encontra chamadas a `export default defineEntity(...)` independentemente de onde o arquivo esteja. Agrupar arquivos por tipo (por exemplo, `logic-functions/`, `roles/`) é apenas uma convenção, não um requisito.
|
||||
</Note>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineRole" description="Configurar permissões de papéis e acesso a objetos">
|
||||
|
||||
Papéis encapsulam permissões sobre os objetos e ações do seu espaço de trabalho.
|
||||
|
||||
```ts restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
PermissionFlag,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
||||
label: 'My new role',
|
||||
description: 'A role that can be used in your workspace',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineApplication" description="Configurar metadados do aplicativo (obrigatório, um por app)">
|
||||
|
||||
Todo app deve ter exatamente uma chamada a `defineApplication` que descreve:
|
||||
|
||||
* **Identidade**: identificadores, nome de exibição e descrição.
|
||||
* **Permissões**: qual papel é usado por suas funções e componentes de front-end.
|
||||
* **Variáveis (opcional)**: pares chave–valor expostos às suas funções como variáveis de ambiente.
|
||||
* **(Opcional) Funções de pré-instalação/pós-instalação**: funções de lógica que são executadas antes ou depois da instalação.
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
value: 'Jane Doe',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
Notas:
|
||||
* Os campos `universalIdentifier` são IDs determinísticos que você controla. Gere-os uma vez e mantenha-os estáveis entre sincronizações.
|
||||
* `applicationVariables` tornam-se variáveis de ambiente para suas funções e componentes de front-end (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` deve fazer referência a um papel definido com `defineRole()` (veja acima).
|
||||
* As funções de pré-instalação e pós-instalação são detectadas automaticamente durante a construção do manifesto — você não precisa referenciá-las em `defineApplication()`.
|
||||
|
||||
#### Metadados do Marketplace
|
||||
|
||||
Se você planeja [publicar seu app](/l/pt/developers/extend/apps/publishing), estes campos opcionais controlam como seu app aparece no marketplace:
|
||||
|
||||
| Campo | Descrição |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `author` | Nome do autor ou da empresa |
|
||||
| `category` | Categoria do app para filtragem no marketplace |
|
||||
| `logoUrl` | Caminho para o logo do seu app (por exemplo, `public/logo.png`) |
|
||||
| `screenshots` | Array de caminhos de capturas de tela (por exemplo, `public/screenshot-1.png`) |
|
||||
| `aboutDescription` | Descrição em markdown mais longa para a aba "Sobre". Se omitido, o marketplace usa o `README.md` do pacote no npm |
|
||||
| `websiteUrl` | Link para seu site |
|
||||
| `termsUrl` | Link para os Termos de Serviço |
|
||||
| `emailSupport` | Endereço de e-mail de suporte |
|
||||
| `issueReportUrl` | Link para o rastreador de problemas |
|
||||
|
||||
#### Papéis e permissões
|
||||
|
||||
O campo `defaultRoleUniversalIdentifier` em `application-config.ts` designa o papel padrão usado pelas funções de lógica e pelos componentes de front-end do seu app. Veja `defineRole` acima para detalhes.
|
||||
|
||||
* O token em tempo de execução injetado como `TWENTY_APP_ACCESS_TOKEN` é derivado desse papel.
|
||||
* O cliente tipado é restrito às permissões concedidas a esse papel.
|
||||
* Siga o princípio do menor privilégio: crie um papel dedicado com apenas as permissões de que suas funções precisam.
|
||||
|
||||
##### Papel de função padrão
|
||||
|
||||
Ao criar um novo app com o scaffold, a CLI cria um arquivo de papel padrão:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
});
|
||||
```
|
||||
|
||||
O `universalIdentifier` desse papel é referenciado em `application-config.ts` como `defaultRoleUniversalIdentifier`:
|
||||
|
||||
* **\*.role.ts** define o que o papel pode fazer.
|
||||
* **application-config.ts** aponta para esse papel para que suas funções herdem suas permissões.
|
||||
|
||||
Notas:
|
||||
* Comece pelo papel gerado pelo scaffold e depois restrinja-o progressivamente seguindo o princípio do menor privilégio.
|
||||
* Substitua `objectPermissions` e `fieldPermissions` pelos objetos e campos de que suas funções realmente precisam.
|
||||
* `permissionFlags` controlam o acesso a recursos em nível de plataforma. Mantenha-os no mínimo necessário.
|
||||
* Veja um exemplo funcional: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineObject" description="Define objetos personalizados com campos">
|
||||
|
||||
Objetos personalizados descrevem tanto o esquema quanto o comportamento de registros no seu espaço de trabalho. Use `defineObject()` para definir objetos com validação integrada:
|
||||
|
||||
```ts postCard.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,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* Use `defineObject()` para validação integrada e melhor suporte na IDE.
|
||||
* O `universalIdentifier` deve ser exclusivo e estável entre implantações.
|
||||
* Cada campo requer `name`, `type`, `label` e seu próprio `universalIdentifier` estável.
|
||||
* O array `fields` é opcional — você pode definir objetos sem campos personalizados.
|
||||
* Você pode criar novos objetos usando `yarn twenty add`, que orienta você sobre nomeação, campos e relacionamentos.
|
||||
|
||||
<Note>
|
||||
**Os campos base são criados automaticamente.** Quando você define um objeto personalizado, o Twenty adiciona automaticamente campos padrão
|
||||
como `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt`.
|
||||
Você não precisa definir esses no seu array `fields` — adicione apenas seus campos personalizados.
|
||||
Você pode substituir os campos padrão definindo um campo com o mesmo nome no seu array `fields`,
|
||||
mas isso não é recomendado.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Campos padrão" description="Estender objetos existentes com campos adicionais">
|
||||
|
||||
Use `defineField()` para adicionar campos a objetos que não são seus — como objetos padrão do Twenty (Person, Company, etc.). ou a objetos de outros apps. Ao contrário dos campos inline em `defineObject()`, os campos independentes exigem um `objectUniversalIdentifier` para especificar qual objeto eles estendem:
|
||||
|
||||
```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' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* `objectUniversalIdentifier` identifica o objeto de destino. Para objetos padrão, use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exportado de `twenty-sdk`.
|
||||
* Ao definir campos inline em `defineObject()`, você não precisa de `objectUniversalIdentifier` — ele é herdado do objeto pai.
|
||||
* `defineField()` é a única forma de adicionar campos a objetos que você não criou com `defineObject()`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Campos de relação" description="Conecte objetos com relações bidirecionais">
|
||||
|
||||
As relações conectam objetos entre si. No Twenty, as relações são sempre **bidirecionais** — você define ambos os lados, e cada lado faz referência ao outro.
|
||||
|
||||
Existem dois tipos de relação:
|
||||
|
||||
| Tipo de relação | Descrição | Tem chave estrangeira? |
|
||||
| --------------- | ----------------------------------------------------------------- | ---------------------- |
|
||||
| `MANY_TO_ONE` | Muitos registros deste objeto apontam para um registro do destino | Sim (`joinColumnName`) |
|
||||
| `ONE_TO_MANY` | Um registro deste objeto possui muitos registros do destino | Não (lado inverso) |
|
||||
|
||||
#### Como as relações funcionam
|
||||
|
||||
Toda relação requer **dois campos** que façam referência um ao outro:
|
||||
|
||||
1. O lado **MANY_TO_ONE** — fica no objeto que contém a chave estrangeira
|
||||
2. O lado **ONE_TO_MANY** — fica no objeto que possui a coleção
|
||||
|
||||
Ambos os campos usam `FieldType.RELATION` e fazem referência cruzada um ao outro via `relationTargetFieldMetadataUniversalIdentifier`.
|
||||
|
||||
#### Exemplo: Um cartão postal tem muitos destinatários
|
||||
|
||||
Suponha que um `PostCard` possa ser enviado para muitos registros `PostCardRecipient`. Cada destinatário pertence a exatamente um cartão postal.
|
||||
|
||||
**Etapa 1: Defina o lado ONE_TO_MANY em PostCard** (o lado "um"):
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Etapa 2: Defina o lado MANY_TO_ONE em PostCardRecipient** (o lado "muitos" — contém a chave estrangeira):
|
||||
|
||||
```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>
|
||||
**Importações circulares:** Ambos os campos de relação referenciam o `universalIdentifier` um do outro. Para evitar problemas de importação circular, exporte os IDs dos seus campos como constantes nomeadas de cada arquivo e importe-os no outro arquivo. O sistema de build resolve isso em tempo de compilação.
|
||||
</Note>
|
||||
|
||||
#### Relacionando a objetos padrão
|
||||
|
||||
Para criar uma relação com um objeto integrado do Twenty (Person, Company, etc.), use `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',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Propriedades de campos de relação
|
||||
|
||||
| Propriedade | Obrigatório | Descrição |
|
||||
| ------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Sim | Deve ser `FieldType.RELATION` |
|
||||
| `relationTargetObjectMetadataUniversalIdentifier` | Sim | O `universalIdentifier` do objeto de destino |
|
||||
| `relationTargetFieldMetadataUniversalIdentifier` | Sim | O `universalIdentifier` do campo correspondente no objeto de destino |
|
||||
| `universalSettings.relationType` | Sim | `RelationType.MANY_TO_ONE` ou `RelationType.ONE_TO_MANY` |
|
||||
| `universalSettings.onDelete` | Apenas para MANY_TO_ONE | O que acontece quando o registro referenciado é excluído: `CASCADE`, `SET_NULL`, `RESTRICT` ou `NO_ACTION` |
|
||||
| `universalSettings.joinColumnName` | Apenas para MANY_TO_ONE | Nome da coluna no banco de dados para a chave estrangeira (por exemplo, `postCardId`) |
|
||||
|
||||
#### Campos de relação inline em defineObject
|
||||
|
||||
Você também pode definir campos de relação diretamente dentro de `defineObject()`. Nesse caso, omita `objectUniversalIdentifier` — ele é herdado do objeto pai:
|
||||
|
||||
```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
|
||||
],
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Gerando entidades com `yarn twenty add`
|
||||
|
||||
Em vez de criar arquivos de entidade manualmente, você pode usar o scaffolder interativo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add
|
||||
```
|
||||
|
||||
Isso solicita que você escolha um tipo de entidade e orienta você pelos campos obrigatórios. Ele gera um arquivo pronto para uso com um `universalIdentifier` estável e a chamada correta de `defineEntity()`.
|
||||
|
||||
Você também pode passar o tipo de entidade diretamente para pular o primeiro prompt:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add object
|
||||
yarn twenty add logicFunction
|
||||
yarn twenty add frontComponent
|
||||
```
|
||||
|
||||
### Tipos de entidade disponíveis
|
||||
|
||||
| Tipo de entidade | Comando | Arquivo gerado |
|
||||
| ------------------------- | ------------------------------------ | ------------------------------------------------------- |
|
||||
| Objeto | `yarn twenty add object` | `src/objects/\<name>.ts` |
|
||||
| Campo | `yarn twenty add field` | `src/fields/\<name>.ts` |
|
||||
| Função lógica | `yarn twenty add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| Componente de front-end | `yarn twenty add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| Papel | `yarn twenty add role` | `src/roles/\<name>.ts` |
|
||||
| Habilidade | `yarn twenty add skill` | `src/skills/\<name>.ts` |
|
||||
| Agente | `yarn twenty add agent` | `src/agents/\<name>.ts` |
|
||||
| Vista | `yarn twenty add view` | `src/views/\<name>.ts` |
|
||||
| Item do menu de navegação | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| Layout da página | `yarn twenty add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
|
||||
### O que o scaffolder gera
|
||||
|
||||
Cada tipo de entidade tem seu próprio modelo. Por exemplo, `yarn twenty add object` solicita:
|
||||
|
||||
1. **Nome (singular)** — por exemplo, `invoice`
|
||||
2. **Nome (plural)** — por exemplo, `invoices`
|
||||
3. **Rótulo (singular)** — preenchido automaticamente a partir do nome (por exemplo, `Invoice`)
|
||||
4. **Rótulo (plural)** — preenchido automaticamente (por exemplo, `Invoices`)
|
||||
5. **Criar uma view e um item de navegação?** — se você responder sim, o scaffolder também gera uma view correspondente e um link na barra lateral para o novo objeto.
|
||||
|
||||
Outros tipos de entidade têm prompts mais simples — a maioria pede apenas um nome.
|
||||
|
||||
O tipo de entidade `field` é mais detalhado: ele solicita o nome do campo, rótulo, tipo (a partir de uma lista de todos os tipos de campo disponíveis como `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.) e o `universalIdentifier` do objeto de destino.
|
||||
|
||||
### Caminho de saída personalizado
|
||||
|
||||
Use a opção `--path` para colocar o arquivo gerado em um local personalizado:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add logicFunction --path src/custom-folder
|
||||
```
|
||||
@@ -1,489 +0,0 @@
|
||||
---
|
||||
title: Componentes de front-end
|
||||
description: Crie componentes React que renderizam dentro da UI do Twenty com isolamento em sandbox.
|
||||
icon: window-maximize
|
||||
---
|
||||
|
||||
Componentes de front-end são componentes React que renderizam diretamente dentro da UI do Twenty. Eles são executados em um Web Worker isolado usando Remote DOM — seu código é sandboxed, mas renderiza nativamente na página, não em um iframe.
|
||||
|
||||
## Onde os componentes de front-end podem ser usados
|
||||
|
||||
Os componentes de front-end podem ser renderizados em dois locais dentro do Twenty:
|
||||
|
||||
* **Painel lateral** — Componentes de front-end não headless abrem no painel lateral direito. Este é o comportamento padrão quando um componente de front-end é acionado pelo menu de comandos.
|
||||
* **Widgets (painéis e páginas de registro)** — Componentes de front-end podem ser incorporados como widgets nos layouts de página. Ao configurar um painel ou o layout de uma página de registro, os usuários podem adicionar um widget de componente de front-end.
|
||||
|
||||
## Exemplo básico
|
||||
|
||||
A maneira mais rápida de ver um componente de front-end em ação é registrá-lo como um **item do menu de comando**. Use `defineCommandMenuItem` em um arquivo separado para fazer o componente aparecer como um botão de ação rápida no canto superior direito da página:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello from my app!</h1>
|
||||
<p>This component renders inside Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
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',
|
||||
});
|
||||
```
|
||||
|
||||
Após sincronizar com `yarn twenty dev` (ou executando uma única vez o `yarn twenty dev --once`), a ação rápida aparece no canto superior direito da página:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Botão de ação rápida no canto superior direito" />
|
||||
</div>
|
||||
|
||||
Clique nele para renderizar o componente inline.
|
||||
|
||||
## Campos de configuração
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
| --------------------- | ----------- | ---------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sim | ID único e estável para este componente |
|
||||
| `component` | Sim | Uma função de componente React |
|
||||
| `name` | Não | Nome de Exibição |
|
||||
| `description` | Não | Descrição do que o componente faz |
|
||||
| `isHeadless` | Não | Defina como `true` se o componente não tiver interface visível (veja abaixo) |
|
||||
|
||||
## Colocando um componente de front-end em uma página
|
||||
|
||||
Além de comandos, você pode incorporar um componente de front-end diretamente em uma página de registro adicionando-o como um widget em um **layout de página**. Veja a seção [definePageLayout](/l/pt/developers/extend/apps/skills-and-agents#definepagelayout) para obter detalhes.
|
||||
|
||||
## Headless vs não headless
|
||||
|
||||
Os componentes de front-end têm dois modos de renderização controlados pela opção `isHeadless`:
|
||||
|
||||
**Não headless (padrão)** — O componente renderiza uma interface visível. Quando acionado pelo menu de comandos, ele é aberto no painel lateral. Este é o comportamento padrão quando `isHeadless` é `false` ou omitido.
|
||||
|
||||
**Headless (`isHeadless: true`)** — O componente é montado de forma invisível em segundo plano. Ele não abre o painel lateral. Componentes headless são projetados para ações que executam lógica e, em seguida, se desmontam — por exemplo, executar uma tarefa assíncrona, navegar para uma página ou exibir um modal de confirmação. Eles se combinam naturalmente com os componentes Command do SDK descritos abaixo.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
}, [recordId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-tracker',
|
||||
description: 'Tracks record views silently',
|
||||
isHeadless: true,
|
||||
component: SyncTracker,
|
||||
});
|
||||
```
|
||||
|
||||
Como o componente retorna `null`, o Twenty ignora renderizar um contêiner para ele — nenhum espaço vazio aparece no layout. O componente ainda tem acesso a todos os hooks e à API de comunicação do host.
|
||||
|
||||
## Componentes Command do SDK
|
||||
|
||||
O pacote `twenty-sdk` fornece quatro componentes auxiliares Command projetados para componentes de front-end headless. Cada componente executa uma ação ao montar, trata erros exibindo uma notificação de snackbar e desmonta automaticamente o componente de front-end ao concluir.
|
||||
|
||||
Importe-os de `twenty-sdk/command`:
|
||||
|
||||
* **`Command`** — Executa um callback assíncrono via a prop `execute`.
|
||||
* **`CommandLink`** — Navega para um caminho do app. Props: `to`, `params`, `queryParams`, `options`.
|
||||
* **`CommandModal`** — Abre um modal de confirmação. Se o usuário confirmar, executa o callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
|
||||
* **`CommandOpenSidePanelPage`** — Abre uma página específica do painel lateral. Props: `page`, `pageTitle`, `pageIcon`.
|
||||
|
||||
Aqui está um exemplo completo de um componente de front-end headless usando `Command` para executar uma ação a partir do menu de comandos:
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
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',
|
||||
});
|
||||
```
|
||||
|
||||
E um exemplo usando `CommandModal` para solicitar confirmação antes de executar:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Acessando o contexto de execução
|
||||
|
||||
Dentro do seu componente, use hooks do SDK para acessar o usuário atual, o registro e a instância do componente:
|
||||
|
||||
```tsx src/front-components/record-info.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User: {userId}</p>
|
||||
<p>Record: {recordId ?? 'No record context'}</p>
|
||||
<p>Component: {componentId}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
||||
name: 'record-info',
|
||||
component: RecordInfo,
|
||||
});
|
||||
```
|
||||
|
||||
Hooks disponíveis:
|
||||
|
||||
| Hook | Retorna | Descrição |
|
||||
| --------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `useUserId()` | `string` ou `null` | O ID do usuário atual |
|
||||
| `useSelectedRecordIds()` | `string[]` | Todos os IDs dos registros selecionados (array vazio se nenhum estiver selecionado) |
|
||||
| `useRecordId()` | `string` ou `null` | **Obsoleto.** Use `useSelectedRecordIds()` em vez disso |
|
||||
| `useFrontComponentId()` | `string` | O ID desta instância do componente |
|
||||
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
|
||||
|
||||
## API de comunicação do host
|
||||
|
||||
Componentes de front-end podem acionar navegação, modais e notificações usando funções de `twenty-sdk`:
|
||||
|
||||
| Função | Descrição |
|
||||
| ----------------------------------------------- | ------------------------------------- |
|
||||
| `navigate(to, params?, queryParams?, options?)` | Navegar para uma página no app |
|
||||
| `openSidePanelPage(params)` | Abrir um painel lateral |
|
||||
| `closeSidePanel()` | Fechar o painel lateral |
|
||||
| `openCommandConfirmationModal(params)` | Mostrar um diálogo de confirmação |
|
||||
| `enqueueSnackbar(params)` | Mostrar uma notificação do tipo toast |
|
||||
| `unmountFrontComponent()` | Desmontar o componente |
|
||||
| `updateProgress(progress)` | Atualizar um indicador de progresso |
|
||||
|
||||
Aqui está um exemplo que usa a API do host para exibir um snackbar e fechar o painel lateral após a conclusão de uma ação:
|
||||
|
||||
```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';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
### Trabalhando com vários registros
|
||||
|
||||
Use `useSelectedRecordIds()` para lidar com vários registros selecionados. Isso é útil para operações em lote:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent, numberOfSelectedRecords } 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';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
|
||||
const handleExport = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
for (const recordId of selectedRecordIds) {
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { exported: true } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Exported ${selectedRecordIds.length} records`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||||
<button onClick={handleExport}>Export</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## defineCommandMenuItem
|
||||
|
||||
Use `defineCommandMenuItem` para registrar um componente de front-end no menu de comando (Cmd+K). Se `isPinned` for `true`, ele também aparece como um botão de ação rápida no canto superior direito da página.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
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',
|
||||
});
|
||||
```
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
| --------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sim | ID exclusivo e estável para o comando |
|
||||
| `label` | Sim | Rótulo completo exibido no menu de comandos (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Sim | O `universalIdentifier` do componente de front-end que este comando abre |
|
||||
| `shortLabel` | Não | Rótulo mais curto exibido no botão fixado de ação rápida |
|
||||
| `icon` | Não | Nome do ícone exibido ao lado do rótulo (por exemplo, `'IconBolt'`, `'IconSend'`) |
|
||||
| `isPinned` | Não | Quando `true`, mostra o comando como um botão de ação rápida no canto superior direito da página |
|
||||
| `availabilityType` | Não | Controla onde o comando aparece: `'GLOBAL'` (sempre disponível), `'RECORD_SELECTION'` (apenas quando registros estão selecionados) ou `'FALLBACK'` (exibido quando nenhum outro comando corresponde) |
|
||||
| `availabilityObjectUniversalIdentifier` | Não | Restringe o comando a páginas de um tipo específico de objeto (por exemplo, somente em registros de Company) |
|
||||
| `conditionalAvailabilityExpression` | Não | Uma expressão booleana para controlar dinamicamente se o comando é visível (veja abaixo) |
|
||||
|
||||
## Expressões de disponibilidade condicional
|
||||
|
||||
O campo `conditionalAvailabilityExpression` permite controlar quando um comando é visível com base no contexto da página atual. Importe variáveis tipadas e operadores de `twenty-sdk` para construir expressões:
|
||||
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import {
|
||||
defineCommandMenuItem,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
**Variáveis de contexto** — representam o estado atual da página:
|
||||
|
||||
| Variável | Tipo | Descrição |
|
||||
| ------------------------------ | --------- | --------------------------------------------------------------------------- |
|
||||
| `pageType` | `string` | Tipo de página atual (por exemplo, `'RecordIndexPage'`, `'RecordShowPage'`) |
|
||||
| `isInSidePanel` | `boolean` | Se o componente é renderizado em um painel lateral |
|
||||
| `numberOfSelectedRecords` | `number` | Número de registros atualmente selecionados |
|
||||
| `isSelectAll` | `boolean` | Se "selecionar tudo" está ativo |
|
||||
| `selectedRecords` | `array` | Os objetos de registro selecionados |
|
||||
| `favoriteRecordIds` | `array` | IDs dos registros marcados como favoritos |
|
||||
| `objectPermissions` | `object` | Permissões para o tipo de objeto atual |
|
||||
| `targetObjectReadPermissions` | `object` | Permissões de leitura para o objeto alvo |
|
||||
| `targetObjectWritePermissions` | `object` | Permissões de escrita para o objeto alvo |
|
||||
| `featureFlags` | `object` | Flags de recurso ativas |
|
||||
| `objectMetadataItem` | `object` | Metadados do tipo de objeto atual |
|
||||
| `hasAnySoftDeleteFilterOnView` | `boolean` | Se a visualização atual tem um filtro de soft-delete |
|
||||
|
||||
**Operadores** — combine variáveis em expressões booleanas:
|
||||
|
||||
| Operador | Descrição |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `isDefined(value)` | `true` se o valor não for null/undefined |
|
||||
| `isNonEmptyString(value)` | `true` se o valor for uma string não vazia |
|
||||
| `includes(array, value)` | `true` se o array contiver o valor |
|
||||
| `includesEvery(array, prop, value)` | `true` se a propriedade de cada item incluir o valor |
|
||||
| `every(array, prop)` | `true` se a propriedade for truthy em cada item |
|
||||
| `everyDefined(array, prop)` | `true` se a propriedade estiver definida em cada item |
|
||||
| `everyEquals(array, prop, value)` | `true` se a propriedade for igual ao valor em cada item |
|
||||
| `some(array, prop)` | `true` se a propriedade for truthy em pelo menos um item |
|
||||
| `someDefined(array, prop)` | `true` se a propriedade estiver definida em pelo menos um item |
|
||||
| `someEquals(array, prop, value)` | `true` se a propriedade for igual ao valor em pelo menos um item |
|
||||
| `someNonEmptyString(array, prop)` | `true` se a propriedade for uma string não vazia em pelo menos um item |
|
||||
| `none(array, prop)` | `true` se a propriedade for falsy em cada item |
|
||||
| `noneDefined(array, prop)` | `true` se a propriedade for undefined em cada item |
|
||||
| `noneEquals(array, prop, value)` | `true` se a propriedade não for igual ao valor em nenhum item |
|
||||
|
||||
## Recursos públicos
|
||||
|
||||
Componentes de front-end podem acessar arquivos do diretório `public/` do app usando `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'logo',
|
||||
component: Logo,
|
||||
});
|
||||
```
|
||||
|
||||
Veja a [seção de recursos públicos](/l/pt/developers/extend/apps/cli-and-testing#public-assets-public-folder) para obter detalhes.
|
||||
|
||||
## Estilização
|
||||
|
||||
Componentes de front-end suportam várias abordagens de estilização. Você pode usar:
|
||||
|
||||
* **Estilos inline** — `style={{ color: 'red' }}`
|
||||
* **Componentes de UI do Twenty** — importe de `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar e mais)
|
||||
* **Emotion** — CSS-in-JS com `@emotion/react`
|
||||
* **Styled-components** — padrões `styled.div`
|
||||
* **Tailwind CSS** — classes utilitárias
|
||||
* **Qualquer biblioteca CSS-in-JS** compatível com React
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||||
|
||||
const StyledWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
||||
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
||||
<Tag text="Active" color="green" />
|
||||
<Status color="green" text="Online" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
||||
name: 'styled-widget',
|
||||
component: StyledWidget,
|
||||
});
|
||||
```
|
||||
@@ -1,273 +0,0 @@
|
||||
---
|
||||
title: Primeiros passos
|
||||
icon: rocket
|
||||
description: Crie seu primeiro app do Twenty em minutos.
|
||||
---
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
* **Node.js 24+** — [Baixar](https://nodejs.org/)
|
||||
* **Yarn 4** — Vem com o Node.js via Corepack. Ative-o: `corepack enable`
|
||||
* **Docker** — [Baixar](https://www.docker.com/products/docker-desktop/). Necessário para executar um servidor Twenty local. Ignore se você já tiver o Twenty em execução em outro lugar.
|
||||
|
||||
A criação de um aplicativo Twenty tem três fases. A ferramenta de scaffolding as reúne em um único comando do fluxo ideal, mas cada fase é um conceito separado — quando algo falha, saber em que fase você está indica o que corrigir.
|
||||
|
||||
| Fase | O que você faz | Ferramenta | Resultado |
|
||||
| --------------------------- | -------------------------------------------------- | ----------------------------- | ------------------------------------- |
|
||||
| **1. Criar scaffolding** | Gerar o código-fonte do aplicativo | `npx create-twenty-app` | Um projeto TypeScript em disco |
|
||||
| **2. Executar um servidor** | Iniciar um servidor Twenty para o qual sincronizar | Docker + `yarn twenty server` | Uma instância Twenty em execução |
|
||||
| **3. Sincronizar** | Sincronize seu código em tempo real com o servidor | `yarn twenty dev` | Suas alterações aparecem na interface |
|
||||
|
||||
---
|
||||
|
||||
## Fase 1 — Fazer scaffolding do seu projeto
|
||||
|
||||
Crie um novo aplicativo a partir do modelo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
Você será solicitado a informar um nome e uma descrição — pressione **Enter** para aceitar os valores padrão. Isso gera um projeto TypeScript em `my-twenty-app/` com um `application-config.ts` inicial, um papel padrão, um fluxo de trabalho de CI e um teste de integração.
|
||||
|
||||
**Após esta fase:** você tem o código-fonte de um aplicativo na sua máquina. Ele ainda não está em execução — isso é a Fase 2.
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — Executar um servidor Twenty local
|
||||
|
||||
Seu aplicativo precisa de um servidor Twenty para o qual sincronizar. O servidor é uma instância completa do Twenty — interface, API GraphQL, PostgreSQL — executando localmente no Docker. Seu código local envia suas definições para esse servidor, o que faz com que elas apareçam na interface.
|
||||
|
||||
A ferramenta de scaffolding oferece iniciar um para você:
|
||||
|
||||
> **Você gostaria de configurar uma instância local do Twenty?**
|
||||
|
||||
* **Sim (recomendado)** — baixa a imagem Docker `twentycrm/twenty-app-dev` e a inicia na porta `2020`. Certifique-se de que o Docker esteja em execução primeiro.
|
||||
* **Não** — escolha isto se você já tiver um servidor Twenty ao qual deseja se conectar. Você pode conectá-lo depois com `yarn twenty remote add`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Deve iniciar instância local?" />
|
||||
</div>
|
||||
|
||||
Quando o servidor estiver ativo, um navegador será aberto para login. Use a conta de demonstração pré-configurada:
|
||||
|
||||
* **E-mail:** `tim@apple.dev`
|
||||
* **Senha:** `tim@apple.dev`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Tela de login do Twenty" />
|
||||
</div>
|
||||
|
||||
Clique em **Authorize** na próxima tela — isso dá à CLI acesso ao seu espaço de trabalho.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Tela de autorização da CLI do Twenty" />
|
||||
</div>
|
||||
|
||||
Seu terminal confirmará que tudo está configurado.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Scaffold do aplicativo criado com sucesso" />
|
||||
</div>
|
||||
|
||||
**Após esta fase:** você tem um servidor Twenty em execução em [http://localhost:2020](http://localhost:2020) com sua CLI autorizada a sincronizar com ele.
|
||||
|
||||
<Note>
|
||||
Se o Docker não estiver instalado ou em execução, a ferramenta de scaffolding informará o comando de inicialização correto para o seu sistema operacional. Quando o Docker estiver ativo, você pode retomar com `yarn twenty server start` — sem necessidade de recriar o scaffolding.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — Sincronizar suas alterações
|
||||
|
||||
Este é o ciclo interno no qual você passará a maior parte do tempo.
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Isso monitora `src/`, recompila a cada alteração e sincroniza o resultado com o servidor. Edite um arquivo, salve e, em um segundo, o servidor refletirá a alteração. Você verá um painel de status em tempo real no seu terminal.
|
||||
|
||||
Para uma saída mais detalhada (logs de build, solicitações de sincronização, rastros de erro), adicione `--verbose`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Saída do terminal no modo de desenvolvimento" />
|
||||
</div>
|
||||
|
||||
Abra [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Você deverá ver seu aplicativo em **Your Apps**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps exibindo My twenty app" />
|
||||
</div>
|
||||
|
||||
Clique em **My twenty app** para ver seu **registro do aplicativo** — um registro em nível de servidor que descreve seu aplicativo (nome, identificador, credenciais OAuth, origem). Um registro pode ser instalado em vários espaços de trabalho no mesmo servidor.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalhes do registro do aplicativo" />
|
||||
</div>
|
||||
|
||||
Clique em **View installed app** para ver a instalação no espaço de trabalho. A aba **About** mostra a versão e as opções de gerenciamento.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicação instalada" />
|
||||
</div>
|
||||
|
||||
**Após esta fase:** você tem um ciclo de desenvolvimento em tempo real. Edite qualquer arquivo em `src/` e ele aparecerá na interface.
|
||||
|
||||
### Sincronização única para CI e scripts
|
||||
|
||||
Passe `--once` para executar uma única compilação + sincronização e sair — mesmo pipeline, sem watcher:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| Comando | Comportamento | Quando usar |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Monitora e ressincroniza a cada alteração. Fica em execução até você interrompê-lo. | Desenvolvimento local interativo. |
|
||||
| `yarn twenty dev --once` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. | Scripts, CI, hooks de pre-commit, agentes de IA e fluxos de trabalho com script. |
|
||||
|
||||
Ambos os modos precisam de um servidor em modo de desenvolvimento e de um remoto autenticado.
|
||||
|
||||
<Warning>
|
||||
O modo de desenvolvimento só está disponível em instâncias do Twenty em modo de desenvolvimento (`NODE_ENV=development`). Instâncias de produção rejeitam solicitações de sincronização de desenvolvimento — use `yarn twenty deploy` para implantar em servidores de produção. Veja [Publicação de aplicativos](/l/pt/developers/extend/apps/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
## O que você pode criar
|
||||
|
||||
Os aplicativos são compostos por **entidades** — cada uma definida como um arquivo TypeScript com um único `export default`:
|
||||
|
||||
| Entidade | O que faz |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Objetos e campos** | Modelos de dados personalizados (Cartão postal, Fatura etc.) com campos tipados |
|
||||
| **Funções lógicas** | Funções TypeScript do lado do servidor acionadas por rotas HTTP, agendamentos do cron ou eventos de banco de dados |
|
||||
| **Componentes de front-end** | Componentes React que são renderizados na UI do Twenty (painel lateral, widgets, menu de comandos) |
|
||||
| **Habilidades e agentes** | Recursos de IA — instruções reutilizáveis e assistentes autônomos |
|
||||
| **Exibições e navegação** | Exibições de lista pré-configuradas e itens de menu da barra lateral |
|
||||
| **Layouts de página** | Páginas de detalhes de registros personalizadas com abas e widgets |
|
||||
|
||||
Referência completa: [Criando aplicativos](/l/pt/developers/extend/apps/building).
|
||||
|
||||
## Estrutura do projeto
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
package.json
|
||||
src/
|
||||
application-config.ts # Required — your app's entry point
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
```
|
||||
|
||||
| Arquivo / Pasta | Finalidade |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `src/application-config.ts` | **Obrigatório.** O principal arquivo de configuração do seu aplicativo. |
|
||||
| `src/default-role.ts` | Papel padrão que controla o que suas funções de lógica podem acessar. |
|
||||
| `src/constants/universal-identifiers.ts` | UUIDs gerados automaticamente e metadados (nome de exibição, descrição). |
|
||||
| `src/__tests__/` | Testes de integração (configuração + teste de exemplo). |
|
||||
| `public/` | Recursos estáticos (imagens, fontes) servidos com seu aplicativo. |
|
||||
|
||||
### Começando a partir de um exemplo
|
||||
|
||||
Use `--example` para começar com um projeto mais completo (objetos personalizados, campos, funções de lógica, componentes de front-end):
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
Os exemplos estão em [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). Você também pode criar o scaffolding de entidades individuais em um projeto existente com `yarn twenty add` — veja [Criando aplicativos](/l/pt/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
|
||||
|
||||
---
|
||||
|
||||
## Gerenciando o servidor local
|
||||
|
||||
Use `yarn twenty server` para controlar o contêiner Twenty local:
|
||||
|
||||
| Comando | O que faz |
|
||||
| -------------------------------------- | ------------------------------------------------ |
|
||||
| `yarn twenty server start` | Inicia o servidor (baixa a imagem se necessário) |
|
||||
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
|
||||
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
|
||||
| `yarn twenty server status` | Mostra a URL, a versão e as credenciais de login |
|
||||
| `yarn twenty server logs` | Transmite os logs do servidor |
|
||||
| `yarn twenty server reset` | Apaga os dados e começa do zero |
|
||||
| `yarn twenty server upgrade` | Baixa a imagem mais recente `twenty-app-dev` |
|
||||
| `yarn twenty server upgrade 2.2.0` | Atualizar para uma versão específica |
|
||||
|
||||
Os dados são persistidos entre reinicializações em dois volumes do Docker (`twenty-app-dev-data` para PostgreSQL, `twenty-app-dev-storage` para arquivos). Use `reset` para apagar tudo.
|
||||
|
||||
### Atualizando a imagem do servidor
|
||||
|
||||
`yarn twenty server upgrade` baixa a imagem mais recente, compara os digests e só recria o contêiner se algo realmente tiver mudado. Os volumes são preservados — apenas o contêiner é substituído. Se uma nova imagem foi baixada e o contêiner estava em execução, a atualização inicia automaticamente um novo contêiner; execute `yarn twenty server start` depois para aguardar até que ele fique saudável.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server upgrade # Latest
|
||||
yarn twenty server upgrade 2.2.0 # Specific version
|
||||
```
|
||||
|
||||
Verifique a versão em execução com `yarn twenty server status` (ele mostra o `APP_VERSION` incorporado ao contêiner).
|
||||
|
||||
### Executando uma instância de teste paralela
|
||||
|
||||
Passe `--test` para qualquer comando de `server` para gerenciar uma segunda instância totalmente isolada — útil para testes de integração ou para experimentar sem tocar nos seus dados principais de desenvolvimento:
|
||||
|
||||
| Comando | O que faz |
|
||||
| ----------------------------------- | ------------------------------------------------ |
|
||||
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
|
||||
| `yarn twenty server stop --test` | Parar |
|
||||
| `yarn twenty server status --test` | Mostrar seu status |
|
||||
| `yarn twenty server logs --test` | Transmitir seus logs |
|
||||
| `yarn twenty server reset --test` | Apagar seus dados |
|
||||
| `yarn twenty server upgrade --test` | Atualizar sua imagem |
|
||||
|
||||
A instância de teste tem seu próprio contêiner (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e configuração — ela é executada junto com sua instância principal sem conflitos. Combine `--test` com `--port` para substituir 2021.
|
||||
|
||||
---
|
||||
|
||||
## Configuração manual (sem o gerador)
|
||||
|
||||
Ignore a ferramenta de scaffolding se você estiver adicionando o SDK a um projeto existente:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
Adicione o script ao `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agora você pode executar `yarn twenty dev`, `yarn twenty server start` e o restante.
|
||||
|
||||
<Note>
|
||||
Não instale `twenty-sdk` globalmente — fixe-o por projeto, para que cada aplicativo use sua própria versão.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolução de Problemas
|
||||
|
||||
* **Erros do Docker** — Certifique-se de que o Docker Desktop (ou o daemon) esteja em execução antes de `yarn twenty server start`. A mensagem de erro mostrará o comando de inicialização correto para o seu sistema operacional.
|
||||
* **Versão errada do Node** — É necessário 24 ou superior. Verifique com `node -v`.
|
||||
* **Falta o Yarn 4** — Execute `corepack enable`.
|
||||
* **Dependências com problemas** — `rm -rf node_modules && yarn install`.
|
||||
|
||||
Travou? Peça ajuda no [Discord da Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
@@ -1,178 +0,0 @@
|
||||
---
|
||||
title: Layout
|
||||
description: Defina vistas, itens do menu de navegação e layouts de página para moldar como seu app aparece no Twenty.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
As entidades de layout controlam como seu app aparece na UI do Twenty — o que fica na barra lateral, quais vistas salvas acompanham o app e como uma página de detalhes de registro é organizada.
|
||||
|
||||
## Conceitos de layout
|
||||
|
||||
| Conceito | O que controla | Entidade |
|
||||
| ----------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||
| **Vista** | Uma configuração de lista salva para um objeto — campos visíveis, ordem, filtros, grupos | `defineView` |
|
||||
| **Item do menu de navegação** | Uma entrada na barra lateral esquerda que aponta para uma vista ou uma URL externa | `defineNavigationMenuItem` |
|
||||
| **Layout da Página** | As abas e widgets que compõem a página de detalhes de um registro | `definePageLayout` |
|
||||
| **Aba Layout da Página** | Uma aba independente vinculada a um layout de página existente (padrão ou do seu próprio aplicativo) | `definePageLayoutTab` |
|
||||
|
||||
Vistas, itens de navegação e layouts de página referenciam-se mutuamente por `universalIdentifier`:
|
||||
|
||||
* Um **item do menu de navegação** do tipo `VIEW` aponta para um identificador `defineView`, assim o link da barra lateral abre essa vista salva.
|
||||
* Um **layout de página** do tipo `RECORD_PAGE` destina-se a um objeto e pode incorporar [front components](/l/pt/developers/extend/apps/front-components) em suas abas como widgets.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineView" description="Defina visualizações salvas para objetos">
|
||||
|
||||
As visualizações são configurações salvas de como os registros de um objeto são exibidos — incluindo quais campos são visíveis, sua ordem e quaisquer filtros ou grupos aplicados. Use `defineView()` para enviar visualizações pré-configuradas com seu app:
|
||||
|
||||
```ts src/views/example-view.ts
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* `objectUniversalIdentifier` especifica a qual objeto esta visualização se aplica.
|
||||
* `key` determina o tipo de visualização (por exemplo, `ViewKey.INDEX` para a visualização de lista principal).
|
||||
* `fields` controla quais colunas aparecem e sua ordem. Cada campo referencia um `fieldMetadataUniversalIdentifier`.
|
||||
* Você também pode definir `filters`, `filterGroups`, `groups` e `fieldGroups` para configurações mais avançadas.
|
||||
* `position` controla a ordenação quando existem várias visualizações para o mesmo objeto.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineNavigationMenuItem" description="Defina links de navegação da barra lateral">
|
||||
|
||||
Os itens do menu de navegação adicionam entradas personalizadas à barra lateral do espaço de trabalho. Use `defineNavigationMenuItem()` para vincular a visualizações, URLs externas ou objetos:
|
||||
|
||||
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* `type` determina para o que o item de menu aponta: `NavigationMenuItemType.VIEW` para uma visualização salva ou `NavigationMenuItemType.LINK` para uma URL externa.
|
||||
* Para links de visualização, defina `viewUniversalIdentifier`. Para links externos, defina `link`.
|
||||
* `position` controla a ordenação na barra lateral.
|
||||
* `icon` e `color` (opcionais) personalizam a aparência.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePageLayout" description="Defina layouts de página personalizados para visualizações de registro">
|
||||
|
||||
Layouts de página permitem personalizar como uma página de detalhes do registro se parece — quais abas aparecem, quais widgets estão dentro de cada aba e como eles são organizados. Use `definePageLayout()` para enviar layouts personalizados com seu app:
|
||||
|
||||
```ts src/page-layouts/example-record-page-layout.ts
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* `type` geralmente é `'RECORD_PAGE'` para personalizar a visualização de detalhes de um objeto específico.
|
||||
* `objectUniversalIdentifier` especifica a qual objeto este layout se aplica.
|
||||
* Cada `tab` define uma seção da página com um `title`, `position` e `layoutMode` (`CANVAS` para layout livre).
|
||||
* Cada `widget` dentro de uma aba pode renderizar um componente de front-end, uma lista de relações ou outros tipos de widget incorporados.
|
||||
* `position` nas abas controla sua ordem. Use valores mais altos (por exemplo, 50) para colocar abas personalizadas após as nativas.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePageLayoutTab" description="Adicionar uma aba a um layout de página existente">
|
||||
|
||||
`definePageLayoutTab` permite que seu app adicione uma única aba — com widgets opcionais — a um layout de página **existente**. O caso de uso mais comum é adicionar uma aba personalizada (por exemplo, uma aba de análises ou de resumo por IA) a uma das páginas de registro nativas do Twenty, ou a um layout de página que o seu próprio app já fornece.
|
||||
|
||||
O layout de página de destino deve ser um layout de página **padrão** do Twenty ou um definido pelo **seu próprio app**; referências entre apps a layouts de página pertencentes a outro app instalado não são compatíveis no momento.
|
||||
|
||||
```ts src/page-layouts/example-extra-tab.ts
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* `pageLayoutUniversalIdentifier` é **obrigatório** ao usar `definePageLayoutTab` e deve apontar para um layout de página que já exista no momento da instalação (padrão ou do seu app). Quando o layout de página pai está ausente, a instalação falha com um erro de validação claro.
|
||||
* `widgets` têm escopo apenas para esta aba — eles referenciam componentes de interface, visualizações etc., exatamente como widgets definidos inline em `definePageLayout`.
|
||||
* `position` controla a ordenação em relação às abas existentes no layout de destino. Escolha um valor que posicione sua aba onde você deseja em relação às abas nativas.
|
||||
* Use isto em vez de `definePageLayout` quando você quiser apenas **adicionar** a um layout existente. Use `definePageLayout` quando você for o proprietário de todo o layout (normalmente uma `RECORD_PAGE` para um objeto que você fornece no seu app, ou uma `STANDALONE_PAGE`).
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -1,565 +0,0 @@
|
||||
---
|
||||
title: Funções lógicas
|
||||
description: Defina funções TypeScript no lado do servidor com gatilhos HTTP, cron e de eventos de banco de dados.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
As funções de lógica são funções TypeScript no lado do servidor que são executadas na plataforma Twenty. Elas podem ser acionadas por solicitações HTTP, agendamentos cron ou eventos de banco de dados — e também podem ser expostas como ferramentas para agentes de IA.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineLogicFunction" description="Defina funções de lógica e seus gatilhos">
|
||||
|
||||
Cada arquivo de função usa `defineLogicFunction()` para exportar uma configuração com um manipulador e gatilhos opcionais.
|
||||
|
||||
```ts src/logic-functions/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
|
||||
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
|
||||
const result = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
/*databaseEventTriggerSettings: {
|
||||
eventName: 'people.created',
|
||||
},*/
|
||||
/*cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},*/
|
||||
});
|
||||
```
|
||||
|
||||
Tipos de gatilho disponíveis:
|
||||
* **httpRoute**: Expõe sua função em um caminho e método HTTP **no endpoint `/s/`**:
|
||||
> por exemplo, `path: '/post-card/create'` é acessível em `https://your-twenty-server.com/s/post-card/create`
|
||||
* **cron**: Executa sua função em um agendamento usando uma expressão CRON.
|
||||
* **databaseEvent**: Executa em eventos do ciclo de vida de objetos do espaço de trabalho. Quando a operação do evento é `updated`, campos específicos a serem observados podem ser especificados no array `updatedFields`. Se deixar indefinido ou vazio, qualquer atualização acionará a função.
|
||||
> por exemplo, `person.updated`, `*.created`, `company.*`
|
||||
|
||||
<Note>
|
||||
Você também pode executar manualmente uma função usando a CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
|
||||
```
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
Você pode acompanhar os logs com:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty logs
|
||||
```
|
||||
</Note>
|
||||
|
||||
#### Payload de gatilho de rota
|
||||
|
||||
Quando um gatilho de rota invoca sua função de lógica, ela recebe um objeto `RoutePayload` que segue o [formato HTTP API v2 da AWS](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||||
Importe o tipo `RoutePayload` de `twenty-sdk`:
|
||||
|
||||
```ts
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||||
const { method, path } = event.requestContext.http;
|
||||
|
||||
return { message: 'Success' };
|
||||
};
|
||||
```
|
||||
|
||||
O tipo `RoutePayload` tem a seguinte estrutura:
|
||||
|
||||
| Propriedade | Tipo | Descrição | Exemplo |
|
||||
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Cabeçalhos HTTP (apenas aqueles listados em `forwardedRequestHeaders`) | veja a seção abaixo |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parâmetros de query string (valores múltiplos unidos por vírgulas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parâmetros de caminho extraídos do padrão de rota | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Corpo da requisição analisado (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Corpo da solicitação UTF-8 original, antes da análise de JSON. Útil para verificar assinaturas de webhook no estilo HMAC (por exemplo, `X-Hub-Signature-256` do GitHub, Stripe). `undefined` quando o ambiente de execução não o preservou. | |
|
||||
| `isBase64Encoded` | `boolean` | Se o corpo está codificado em base64 | |
|
||||
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Caminho bruto da requisição | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
Por padrão, os cabeçalhos HTTP das requisições recebidas **não** são repassados para sua função de lógica por motivos de segurança.
|
||||
Para acessar cabeçalhos específicos, liste-os explicitamente no array `forwardedRequestHeaders`:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/webhook',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
No seu manipulador, acesse os cabeçalhos encaminhados assim:
|
||||
|
||||
```ts
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-webhook-signature'];
|
||||
const contentType = event.headers['content-type'];
|
||||
|
||||
// Validate webhook signature...
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Os nomes dos cabeçalhos são normalizados para minúsculas. Acesse-os usando chaves em minúsculas (por exemplo, `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Expor uma função como ferramenta de IA ou como ação de fluxo de trabalho
|
||||
|
||||
As funções de lógica podem ser expostas em duas superfícies, cada uma com seu próprio gatilho:
|
||||
|
||||
* **`toolTriggerSettings`** — torna a função disponível para os recursos de IA do Twenty (chat, MCP, chamadas de função). Usa o JSON Schema padrão, o formato que os LLMs entendem nativamente.
|
||||
* **`workflowActionTriggerSettings`** — torna a função visível como uma etapa no construtor visual de fluxos de trabalho. Usa o `InputSchema` avançado do Twenty para que o construtor possa renderizar editores de campo adequados, seletores de variáveis e rótulos.
|
||||
|
||||
Uma função pode optar por uma, pela outra ou por ambas. Ficam ao lado de `cronTriggerSettings`, `databaseEventTriggerSettings` e `httpRouteTriggerSettings` — mesmo padrão, mesmo formato.
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
toolTriggerSettings: {},
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* Uma função pode misturar superfícies — declare tanto `toolTriggerSettings` quanto `workflowActionTriggerSettings` para expô-la no chat E no construtor de fluxos de trabalho.
|
||||
* `toolTriggerSettings.inputSchema` e `workflowActionTriggerSettings.inputSchema` são opcionais. Quando omitidos, o construtor de manifestos os infere a partir do código-fonte do handler (JSON Schema para a ferramenta de IA, `InputSchema` do Twenty para a ação de fluxo de trabalho). Forneça um explicitamente quando quiser uma tipagem mais rica — por exemplo, com campos compatíveis com `FieldMetadataType`, como `CURRENCY` ou `RELATION` para o construtor de fluxos de trabalho, ou com campos `description` que o agente de IA pode ler:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Escreva uma boa `description`.** Os agentes de IA dependem do campo `description` da função para decidir quando usar a ferramenta. Seja específico sobre o que a ferramenta faz e quando ela deve ser chamada.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Defina uma função de lógica de pós-instalação (uma por aplicativo)">
|
||||
|
||||
Uma função de pós-instalação é uma função lógica que é executada automaticamente assim que seu aplicativo terminar de ser instalado em um espaço de trabalho. O servidor a executa **depois** que os metadados do aplicativo forem sincronizados e o cliente do SDK for gerado, para que o espaço de trabalho esteja totalmente pronto para uso e o novo esquema esteja disponível. Casos de uso típicos incluem popular dados padrão, criar registros iniciais, configurar as definições do espaço de trabalho ou provisionar recursos em serviços de terceiros.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
```
|
||||
|
||||
Você também pode executar manualmente a função de pós-instalação a qualquer momento usando a CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* As funções de pós-instalação usam `definePostInstallLogicFunction()` — uma variante especializada que omite as configurações de gatilho (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
* O manipulador recebe um `InstallPayload` com `{ previousVersion?: string; newVersion: string }` — `newVersion` é a versão que está sendo instalada, e `previousVersion` é a versão que foi instalada anteriormente (ou `undefined` em uma instalação nova). Use esses valores para distinguir instalações novas de atualizações e para executar lógica de migração específica da versão.
|
||||
* **Quando o hook é executado**: apenas em instalações novas, por padrão. Passe `shouldRunOnVersionUpgrade: true` se você também quiser que ele seja executado quando o app for atualizado a partir de uma versão anterior. Quando omitida, a flag tem valor padrão `false` e as atualizações ignoram o hook.
|
||||
* **Modelo de execução — assíncrono por padrão, síncrono opcional**: a flag `shouldRunSynchronously` controla *como* a pós-instalação é executada.
|
||||
* `shouldRunSynchronously: false` *(padrão)* — o hook é **enfileirado na fila de mensagens** com `retryLimit: 3` e é executado de forma assíncrona em um worker. A resposta da instalação retorna assim que o job é enfileirado, então um manipulador lento ou com falha não bloqueia quem chamou. O worker tentará novamente até três vezes. **Use isto para jobs de longa duração** — popular grandes conjuntos de dados, chamar APIs de terceiros lentas, provisionar recursos externos, qualquer coisa que possa exceder uma janela razoável de resposta HTTP.
|
||||
* `shouldRunSynchronously: true` — o hook é executado **inline durante o fluxo de instalação** (mesmo executor da pré-instalação). A requisição de instalação bloqueia até o manipulador terminar e, se ele lançar uma exceção, quem chamou a instalação recebe um `POST_INSTALL_ERROR`. Sem novas tentativas automáticas. **Use isto para trabalhos rápidos que precisam ser concluídos antes da resposta** — por exemplo, emitir um erro de validação para o usuário ou fazer uma configuração rápida da qual o cliente dependerá imediatamente após a chamada de instalação retornar. Tenha em mente que a migração de metadados já foi aplicada quando a pós-instalação é executada, então uma falha no modo síncrono **não** reverte as alterações de esquema — ela apenas expõe o erro.
|
||||
* Garanta que seu manipulador seja idempotente. No modo assíncrono, a fila pode tentar novamente até três vezes; em qualquer modo, o hook pode ser executado novamente em atualizações quando `shouldRunOnVersionUpgrade: true`.
|
||||
* As variáveis de ambiente `APPLICATION_ID`, `APP_ACCESS_TOKEN` e `API_URL` estão disponíveis dentro do manipulador (assim como em qualquer outra função de lógica), então você pode chamar a API da Twenty com um token de acesso de aplicativo com escopo para o seu app.
|
||||
* É permitida apenas uma função de pós-instalação por app. A geração do manifesto apresentará erro se mais de uma for detectada.
|
||||
* O `universalIdentifier`, `shouldRunOnVersionUpgrade` e `shouldRunSynchronously` da função são anexados automaticamente ao manifesto do aplicativo no campo `postInstallLogicFunction` durante o build — você não precisa referenciá-los em `defineApplication()`.
|
||||
* O tempo limite padrão é definido como 300 segundos (5 minutos) para permitir tarefas de configuração mais longas, como o pré-carregamento de dados.
|
||||
* **Não executado no modo de desenvolvimento**: quando um app é registrado localmente (via `yarn twenty dev`), o servidor pula completamente o fluxo de instalação e sincroniza arquivos diretamente pelo watcher da CLI — portanto, a pós-instalação nunca é executada no modo de desenvolvimento, independentemente de `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` para acioná-lo manualmente em um workspace em execução.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Defina uma função de lógica de pré-instalação (uma por aplicativo)">
|
||||
|
||||
Uma função de pré-instalação é uma função de lógica que é executada automaticamente durante a instalação, **antes que a migração de metadados do workspace seja aplicada**. Ela compartilha o mesmo formato de payload que a pós-instalação (`InstallPayload`), mas está posicionada mais cedo no fluxo de instalação para poder preparar o estado do qual a próxima migração depende — usos típicos incluem fazer backup de dados, validar a compatibilidade com o novo esquema ou arquivar registros que estão prestes a ser reestruturados ou removidos.
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Você também pode executar manualmente a função de pré-instalação a qualquer momento usando a CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* Funções de pré-instalação usam `definePreInstallLogicFunction()` — a mesma configuração especializada da pós-instalação, apenas anexada a um ponto diferente do ciclo de vida.
|
||||
* Os manipuladores de pré e pós-instalação recebem o mesmo tipo `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importe-o uma vez e reutilize-o para ambos os hooks.
|
||||
* **Quando o hook é executado**: posicionado imediatamente antes da migração de metadados do workspace (`synchronizeFromManifest`). Antes de executar, o servidor realiza uma "sincronização simplificada" puramente aditiva que registra a função de pré-instalação da **nova** versão nos metadados do workspace — nada mais é alterado — e então a executa. Como essa sincronização é apenas aditiva, os objetos, campos e dados da versão anterior ainda estão intactos quando seu manipulador é executado: você pode ler e fazer backup com segurança do estado pré-migração.
|
||||
* **Modelo de execução**: a pré-instalação é executada **de forma síncrona** e **bloqueia a instalação**. Se o manipulador lançar uma exceção, a instalação é abortada antes que quaisquer alterações de esquema sejam aplicadas — o workspace permanece na versão anterior em um estado consistente. Isto é intencional: a pré-instalação é sua última chance de recusar uma atualização arriscada.
|
||||
* Assim como na pós-instalação, é permitida apenas uma função de pré-instalação por app. Ela é anexada ao manifesto do aplicativo sob `preInstallLogicFunction` automaticamente durante o build.
|
||||
* **Não é executada no modo de desenvolvimento**: igual à pós-instalação — o fluxo de instalação é totalmente ignorado para apps registrados localmente, portanto a pré-instalação nunca é executada com `yarn twenty dev`. Use `yarn twenty exec --preInstall` para acioná-lo manualmente.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pré-instalação vs pós-instalação: quando usar cada um" description="Escolhendo o hook de instalação correto">
|
||||
|
||||
Ambos os hooks fazem parte do mesmo fluxo de instalação e recebem o mesmo `InstallPayload`. A diferença é **quando** eles são executados em relação à migração de metadados do workspace, e isso muda quais dados eles podem manipular com segurança.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
A pré-instalação é sempre **síncrona** (ela bloqueia a instalação e pode abortá-la). A pós-instalação é **assíncrona por padrão** — enfileirada em um worker com novas tentativas automáticas — mas pode optar por execução síncrona com `shouldRunSynchronously: true`. Veja o acordeão `definePostInstallLogicFunction` acima para saber quando usar cada modo.
|
||||
|
||||
**Use `post-install` para qualquer coisa que precise que o novo esquema exista.** Este é o caso mais comum:
|
||||
|
||||
* Popular dados padrão (criando registros iniciais, visualizações padrão, conteúdo de demonstração) em objetos e campos recém-adicionados.
|
||||
* Registrar webhooks com serviços de terceiros agora que o app tem suas credenciais.
|
||||
* Chamar sua própria API para finalizar a configuração que depende dos metadados sincronizados.
|
||||
* Lógica idempotente de "garantir que isso exista" que deve reconciliar o estado em cada atualização — combine com `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Exemplo — popular um registro `PostCard` padrão após a instalação:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
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!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Use `pre-install` quando uma migração, de outra forma, destruiria ou corromperia dados existentes.** Como a pré-instalação roda contra o esquema *anterior* e sua falha reverte a atualização, é o lugar certo para qualquer coisa arriscada:
|
||||
|
||||
* **Fazer backup de dados que estão prestes a ser removidos ou reestruturados** — por exemplo, você está removendo um campo na v2 e precisa copiar seus valores para outro campo ou exportá-los para um armazenamento antes que a migração seja executada.
|
||||
* **Arquivar registros que uma nova restrição invalidaria** — por exemplo, um campo está se tornando `NOT NULL` e você precisa excluir ou corrigir linhas com valores nulos primeiro.
|
||||
* **Validar a compatibilidade e recusar a atualização se os dados atuais não puderem ser migrados de forma limpa** — lance uma exceção no manipulador e a instalação é abortada sem alterações aplicadas. Isto é mais seguro do que descobrir a incompatibilidade no meio da migração.
|
||||
* **Renomear ou reatribuir chaves de dados** antes de uma alteração de esquema que perderia a associação.
|
||||
|
||||
Exemplo — arquivar registros antes de uma migração destrutiva:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: 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 },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Regra geral:**
|
||||
|
||||
| Você quer... | Usar |
|
||||
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Popular dados padrão, configurar o workspace, registrar recursos externos | `post-install` |
|
||||
| Executar processos longos de popular dados ou chamadas a terceiros que não devem bloquear a resposta da instalação | `post-install` (padrão — `shouldRunSynchronously: false`, com novas tentativas do worker) |
|
||||
| Executar uma configuração rápida da qual o chamador dependerá imediatamente após o retorno da chamada de instalação | `post-install` com `shouldRunSynchronously: true` |
|
||||
| Ler ou fazer backup de dados que a próxima migração perderia | `pre-install` |
|
||||
| Rejeitar uma atualização que corromperia dados existentes | `pre-install` (lançar uma exceção no manipulador) |
|
||||
| Executar reconciliação em cada atualização | `post-install` com `shouldRunOnVersionUpgrade: true` |
|
||||
| Fazer uma configuração única apenas na primeira instalação | `post-install` com `shouldRunOnVersionUpgrade: false` (padrão) |
|
||||
|
||||
<Note>
|
||||
Em caso de dúvida, use **post-install** como padrão. Recurra à pré-instalação somente quando a própria migração for destrutiva e você precisar interceptar o estado anterior antes que ele desapareça.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Clientes de API tipados (twenty-client-sdk)
|
||||
|
||||
O pacote `twenty-client-sdk` fornece dois clientes GraphQL tipados para interagir com a API do Twenty a partir das suas funções de lógica e componentes de front-end.
|
||||
|
||||
| Cliente | Importar | Endpoint | Gerado? |
|
||||
| ------------------- | ---------------------------- | -------------------------------------------------------------------- | -------------------------- |
|
||||
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — dados do espaço de trabalho (registros, objetos) | Sim, em tempo de dev/build |
|
||||
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configuração do espaço de trabalho, upload de arquivos | Não, vem pré-compilado |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="CoreApiClient" description="Consultar e modificar dados do espaço de trabalho (registros, objetos)">
|
||||
|
||||
`CoreApiClient` é o cliente principal para consultar e mutar dados do espaço de trabalho. Ele é **gerado a partir do schema do seu espaço de trabalho** durante `yarn twenty dev` ou `yarn twenty build`, então é totalmente tipado para corresponder aos seus objetos e campos.
|
||||
|
||||
```ts
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Query records
|
||||
const { companies } = await client.query({
|
||||
companies: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
name: true,
|
||||
domainName: {
|
||||
primaryLinkLabel: true,
|
||||
primaryLinkUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a record
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Acme Corp',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
O cliente usa uma sintaxe de selection-set: passe `true` para incluir um campo, use `__args` para argumentos e aninhe objetos para relações. Você tem preenchimento automático e verificação de tipos completos com base no schema do seu espaço de trabalho.
|
||||
|
||||
<Note>
|
||||
**CoreApiClient é gerado em tempo de dev/build.** Se você usá-lo sem executar primeiro `yarn twenty dev` ou `yarn twenty build`, ele lançará um erro. A geração ocorre automaticamente — a CLI analisa o schema GraphQL do seu espaço de trabalho e gera um cliente tipado usando `@genql/cli`.
|
||||
</Note>
|
||||
|
||||
#### Usando CoreSchema para anotações de tipo
|
||||
|
||||
`CoreSchema` fornece tipos TypeScript que correspondem aos objetos do seu espaço de trabalho — útil para tipar o estado de componentes ou parâmetros de função:
|
||||
|
||||
```ts
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
const [company, setCompany] = useState<
|
||||
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
|
||||
>(undefined);
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
company: {
|
||||
__args: { filter: { position: { eq: 1 } } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
setCompany(result.company);
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="MetadataApiClient" description="Configuração do espaço de trabalho, aplicativos e upload de arquivos">
|
||||
|
||||
`MetadataApiClient` é fornecido pré-compilado com o SDK (não é necessário gerar). Ele consulta o endpoint `/metadata` para configuração do espaço de trabalho, aplicativos e upload de arquivos.
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
// List first 10 objects in the workspace
|
||||
const { objects } = await metadataClient.query({
|
||||
objects: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
labelSingular: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
__args: {
|
||||
filter: {},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Carregamento de arquivos
|
||||
|
||||
`MetadataApiClient` inclui um método `uploadFile` para anexar arquivos a campos do tipo arquivo:
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
| Parâmetro | Tipo | Descrição |
|
||||
| ---------------------------------- | -------- | ----------------------------------------------------------------- |
|
||||
| `fileBuffer` | `Buffer` | O conteúdo bruto do arquivo |
|
||||
| `filename` | `string` | O nome do arquivo (usado para armazenamento e exibição) |
|
||||
| `contentType` | `string` | Tipo MIME (padrão para `application/octet-stream` se omitido) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | O `universalIdentifier` do campo do tipo de arquivo no seu objeto |
|
||||
|
||||
Pontos-chave:
|
||||
* Usa o `universalIdentifier` do campo (não o ID específico do espaço de trabalho), de modo que seu código de upload funcione em qualquer espaço de trabalho onde seu app esteja instalado.
|
||||
* A `url` retornada é uma URL assinada que você pode usar para acessar o arquivo enviado.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
Quando seu código é executado no Twenty (funções de lógica ou componentes de front-end), a plataforma injeta credenciais como variáveis de ambiente:
|
||||
|
||||
* `TWENTY_API_URL` — URL base da API do Twenty
|
||||
* `TWENTY_APP_ACCESS_TOKEN` — Chave de curta duração com escopo para o papel de função padrão do seu aplicativo
|
||||
|
||||
Você **não** precisa passá-las para os clientes — eles leem de `process.env` automaticamente. As permissões da chave de API são determinadas pelo papel referenciado em `defaultRoleUniversalIdentifier` no seu `application-config.ts`.
|
||||
</Note>
|
||||
@@ -1,295 +0,0 @@
|
||||
---
|
||||
title: Publicação
|
||||
icon: carregar
|
||||
description: Distribua seu aplicativo Twenty no Marketplace ou implante-o internamente.
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Depois que seu aplicativo estiver [compilado e testado localmente](/l/pt/developers/extend/apps/building), você tem dois caminhos para distribuí-lo:
|
||||
|
||||
* **Implantar um tarball** — envie seu aplicativo diretamente para um servidor Twenty específico para uso interno ou privado.
|
||||
* **Publicar no npm** — liste seu aplicativo no Marketplace da Twenty para que qualquer espaço de trabalho possa descobrir e instalar.
|
||||
|
||||
Ambos os caminhos começam na mesma etapa de **build**.
|
||||
|
||||
## Compilando seu app
|
||||
|
||||
Execute o comando build para compilar seu app e gerar um `manifest.json` pronto para distribuição:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty build
|
||||
```
|
||||
|
||||
Isso compila seu código-fonte em TypeScript, transpila funções de lógica e componentes de front-end e grava tudo em `.twenty/output/`. Adicione `--tarball` para também gerar um pacote `.tgz` para distribuição manual ou para o comando de deploy.
|
||||
|
||||
## Implantando em um servidor (tarball)
|
||||
|
||||
Para aplicativos que você não quer disponibilizar publicamente — ferramentas proprietárias, integrações apenas para empresas ou builds experimentais — você pode implantar um tarball diretamente em um servidor Twenty.
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
Antes de implantar, você precisa de um remote configurado apontando para o servidor de destino. Os remotes armazenam a URL do servidor e as credenciais de autenticação localmente em `~/.twenty/config.json`.
|
||||
|
||||
Adicionar um remote:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --as production
|
||||
```
|
||||
|
||||
### Implantando
|
||||
|
||||
Compile e envie seu aplicativo para o servidor em uma única etapa:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty deploy
|
||||
# To deploy to a specific remote:
|
||||
# yarn twenty deploy --remote production
|
||||
```
|
||||
|
||||
### Compartilhando um aplicativo implantado
|
||||
|
||||
<Warning>
|
||||
Compartilhar aplicativos privados (tarball) entre espaços de trabalho é um recurso do plano **Enterprise**. A guia **Distribution** exibirá um aviso de atualização em vez dos controles de compartilhamento até que seu espaço de trabalho tenha uma chave Enterprise válida. Vá para [Configurações > Painel de Administração > Enterprise](/settings/admin-panel#enterprise) para ativá-lo.
|
||||
</Warning>
|
||||
|
||||
Aplicativos em tarball não são listados no marketplace público, então outros espaços de trabalho no mesmo servidor não os descobrirão ao navegar. Assim que o seu espaço de trabalho estiver no plano Enterprise, você pode compartilhar um app implantado desta forma:
|
||||
|
||||
1. Vá para **Configurações > Aplicações > Registros** e abra seu aplicativo
|
||||
2. Na guia **Distribuição**, clique em **Copiar link de compartilhamento**
|
||||
3. Compartilhe esse link com usuários de outros espaços de trabalho — ele os leva diretamente para a página de instalação do aplicativo
|
||||
|
||||
O link de compartilhamento usa a URL base do servidor (sem qualquer subdomínio de espaço de trabalho), para funcionar em qualquer espaço de trabalho no servidor.
|
||||
|
||||
### Gerenciamento de versões
|
||||
|
||||
Ao atualizar um aplicativo empacotado como tarball já implantado, o servidor exige que o `version` no `package.json` seja **estritamente maior** (de acordo com a ordenação do [semver](https://semver.org)) do que a versão atualmente implantada. Reimplantar a mesma versão, ou enviar uma inferior, é rejeitado antes que o tarball seja armazenado — você verá um erro `VERSION_ALREADY_EXISTS` na CLI.
|
||||
|
||||
Para lançar uma atualização:
|
||||
|
||||
1. Atualize o campo `version` no seu `package.json` (por exemplo, `1.2.3` → `1.2.4`, `1.3.0` ou `2.0.0`)
|
||||
2. Execute `yarn twenty deploy` (ou `yarn twenty deploy --remote production`)
|
||||
3. Os espaços de trabalho que têm o aplicativo instalado verão a atualização disponível em suas configurações
|
||||
|
||||
<Note>
|
||||
Tags de pré-lançamento funcionam como esperado: incrementar `1.0.0-rc.1` → `1.0.0-rc.2` é permitido, e uma versão final como `1.0.0` é corretamente reconhecida como superior a `1.0.0-rc.5`. A versão em `package.json` deve ser, ela própria, uma string semver válida.
|
||||
</Note>
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
### Compatibilidade da versão do servidor
|
||||
|
||||
Se o seu aplicativo usar um recurso introduzido em uma versão específica do servidor Twenty (por exemplo, provedores OAuth adicionados na v2.3.0), você deve declarar a versão mínima do servidor que seu aplicativo requer usando o campo `engines.twenty` em `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-my-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"twenty": ">=2.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
O valor é um [intervalo semver](https://github.com/npm/node-semver#ranges) padrão. Padrões comuns:
|
||||
|
||||
| Intervalo | Significado |
|
||||
| ---------------------------------- | ---------------------------------------------------------- |
|
||||
| `>=2.3.0` | Qualquer servidor a partir de 2.3.0 |
|
||||
| `>=2.3.0 \<3.0.0` | 2.3.0 ou posterior, mas abaixo da próxima versão principal |
|
||||
| `^2.3.0` | O mesmo que `>=2.3.0 \<3.0.0` |
|
||||
|
||||
**O que acontece no momento da implantação e da instalação:**
|
||||
|
||||
* Se `engines.twenty` estiver definido e a versão do servidor de destino não satisfizer o intervalo, a implantação (upload do tarball) ou a instalação será rejeitada com o erro `SERVER_VERSION_INCOMPATIBLE` e uma mensagem indicando tanto o intervalo exigido quanto a versão real do servidor.
|
||||
* Se `engines.twenty` **não estiver definido**, o aplicativo é aceito em qualquer versão do servidor (retrocompatível com os aplicativos existentes).
|
||||
* Se o servidor não tiver `APP_VERSION` configurado, a verificação será ignorada.
|
||||
|
||||
<Note>
|
||||
O servidor realiza a verificação definitiva — ele valida `engines.twenty` tanto no upload do tarball quanto na instalação no workspace. Se você implantar um tarball fora de banda ou instalar a partir do marketplace, o servidor ainda impõe a compatibilidade.
|
||||
</Note>
|
||||
|
||||
## CI/CD automatizado (fluxos de trabalho pré-configurados)
|
||||
|
||||
Os apps gerados com `create-twenty-app` já vêm com dois fluxos de trabalho do GitHub Actions prontos, em `.github/workflows/`. Eles estão prontos para executar assim que você fizer push do repositório para o GitHub — nenhuma configuração extra é necessária para CI, e CD requer apenas um único segredo.
|
||||
|
||||
### CI — `ci.yml`
|
||||
|
||||
Executa testes de integração a cada push para `main` e a cada pull request.
|
||||
|
||||
**O que faz:**
|
||||
|
||||
1. Faz checkout do código-fonte do seu app.
|
||||
2. Inicia uma instância de teste do Twenty isolada usando a ação composta `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (o equivalente em CI de `yarn twenty server start --test`).
|
||||
3. Habilita o Corepack, configura o Node.js a partir do seu `.nvmrc` e instala as dependências com `yarn install --immutable`.
|
||||
4. Executa `yarn test`, passando `TWENTY_API_URL` e `TWENTY_API_KEY` da instância iniciada para que seus testes possam se comunicar com um servidor real.
|
||||
|
||||
**Opções de configuração:**
|
||||
|
||||
* `TWENTY_VERSION` (env, padrão `latest`) — fixe a versão do servidor Twenty usada no CI editando isto em `ci.yml`.
|
||||
* A concorrência é agrupada por `github.ref` e cancela execuções em andamento quando há novos pushes.
|
||||
|
||||
Nenhum segredo é necessário — a instância de teste é efêmera e existe apenas durante a execução do job.
|
||||
|
||||
### CD — `cd.yml`
|
||||
|
||||
Faz o deploy do seu app para um servidor Twenty configurado a cada push para `main` e, opcionalmente, a partir de um pull request quando o rótulo `deploy` é aplicado.
|
||||
|
||||
**O que faz:**
|
||||
|
||||
1. Faz checkout do head do PR (para PRs rotulados) ou do commit enviado.
|
||||
2. Executa `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — o equivalente em CI de `yarn twenty deploy`.
|
||||
3. Executa `twentyhq/twenty/.github/actions/install-twenty-app@main` para que a versão recém-implantada seja instalada no workspace de destino.
|
||||
|
||||
**Configuração obrigatória:**
|
||||
|
||||
| Configuração | Onde | Finalidade |
|
||||
| ----------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `TWENTY_DEPLOY_URL` | `env` em `cd.yml` (padrão `http://localhost:3000`) | O servidor Twenty para o qual fazer o deploy. Altere isto para a URL real do seu servidor antes do primeiro uso. |
|
||||
| `TWENTY_DEPLOY_API_KEY` | Repositório do GitHub **Settings → Secrets and variables → Actions** | Chave de API com permissão de deploy no servidor de destino. |
|
||||
|
||||
<Note>
|
||||
O `TWENTY_DEPLOY_URL` padrão de `http://localhost:3000` é um placeholder — ele não alcançará nada a partir de um runner hospedado pelo GitHub. Atualize-o para a URL pública do seu servidor (ou use um runner self-hosted com acesso à rede) antes de habilitar o CD.
|
||||
</Note>
|
||||
|
||||
**Acionando um deploy de pré-visualização a partir de um PR:**
|
||||
|
||||
Adicione o rótulo `deploy` a um pull request. A condição `if:` em `cd.yml` executará o job para esse PR usando o commit HEAD do PR, permitindo que você valide uma alteração no servidor de destino antes de fazer o merge.
|
||||
|
||||
### Fixando as ações reutilizáveis
|
||||
|
||||
Ambos os fluxos de trabalho fazem referência a ações reutilizáveis em `@main`, portanto as atualizações de ações no repositório `twentyhq/twenty` são aplicadas automaticamente. Se você quiser builds determinísticos, substitua `@main` por um SHA de commit ou uma tag de release em cada linha `uses:`.
|
||||
|
||||
## Publicação no npm
|
||||
|
||||
Publicar no npm torna seu aplicativo descobrível no Marketplace da Twenty. Qualquer espaço de trabalho da Twenty pode navegar, instalar e atualizar aplicativos do Marketplace diretamente pela UI.
|
||||
|
||||
### Requisitos
|
||||
|
||||
* Uma conta no [npm](https://www.npmjs.com)
|
||||
* A palavra-chave `twenty-app` no array `keywords` do seu `package.json` (adicione-a manualmente — não é incluída por padrão no template `create-twenty-app`)
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-app-postcard-sender",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["twenty-app"]
|
||||
}
|
||||
```
|
||||
|
||||
### Metadados do Marketplace
|
||||
|
||||
A configuração `defineApplication()` oferece suporte a campos opcionais que controlam como seu app aparece no marketplace. Use `logoUrl` e `screenshots` para referenciar imagens da pasta `public/`:
|
||||
|
||||
```ts src/application-config.ts
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'My App',
|
||||
description: 'A great app',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
logoUrl: 'public/logo.png',
|
||||
screenshots: [
|
||||
'public/screenshot-1.png',
|
||||
'public/screenshot-2.png',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Veja o [acordeão de defineApplication](/l/pt/developers/extend/apps/building#defineentity-functions) na página Building Apps para a lista completa de campos do marketplace (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
|
||||
|
||||
#### Dimensões recomendadas para capturas de tela
|
||||
|
||||
O marketplace renderiza `screenshots` em um contêiner fixo de `8:5` (por exemplo, `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
Capturas de tela de qualquer proporção são exibidas por completo e nunca são cortadas, mas qualquer coisa significativamente mais alta ou mais estreita que `8:5` exibirá faixas vazias nas laterais.
|
||||
</Note>
|
||||
|
||||
### Publicar
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty publish
|
||||
```
|
||||
|
||||
Para publicar sob uma dist-tag específica (por exemplo, `beta` ou `next`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty publish --tag beta
|
||||
```
|
||||
|
||||
### Como funciona a descoberta no marketplace
|
||||
|
||||
O servidor Twenty sincroniza seu catálogo do marketplace a partir do registro do npm **a cada hora**.
|
||||
|
||||
Você pode acionar a sincronização imediatamente em vez de esperar:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty server catalog-sync --remote production
|
||||
```
|
||||
|
||||
Os metadados exibidos no marketplace vêm da sua configuração `defineApplication()` — campos como `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` e `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Se o seu aplicativo não definir um `aboutDescription` em `defineApplication()`, o marketplace usará automaticamente o `README.md` do seu pacote no npm como conteúdo da página Sobre. Isso significa que você pode manter um único README tanto para o npm quanto para o marketplace da Twenty. Se quiser uma descrição diferente no marketplace, defina explicitamente `aboutDescription`.
|
||||
</Note>
|
||||
|
||||
### Publicação via CI
|
||||
|
||||
Use este workflow do GitHub Actions para publicar automaticamente a cada release (usa [OIDC](https://docs.npmjs.com/trusted-publishers)):
|
||||
|
||||
```yaml filename=".github/workflows/publish.yml"
|
||||
name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: yarn install --immutable
|
||||
- run: npx twenty build
|
||||
- run: npm publish --provenance --access public
|
||||
working-directory: .twenty/output
|
||||
```
|
||||
|
||||
Para outros sistemas de CI (GitLab CI, CircleCI etc.), aplicam-se os mesmos três comandos: `yarn install`, `yarn twenty build` e, em seguida, `npm publish` a partir de `.twenty/output`.
|
||||
|
||||
<Note>
|
||||
**Proveniência do npm** é opcional, mas recomendada. Publicar com `--provenance` adiciona um selo de confiança à sua listagem no npm, permitindo que os usuários verifiquem que o pacote foi construído a partir de um commit específico em um pipeline de CI público. Consulte a [documentação de proveniência do npm](https://docs.npmjs.com/generating-provenance-statements) para instruções de configuração.
|
||||
</Note>
|
||||
|
||||
## Instalando aplicativos
|
||||
|
||||
Depois que um app é publicado (npm) ou implantado (tarball), os espaços de trabalho podem instalá-lo pela interface do usuário.
|
||||
|
||||
Vá para a página **Configurações > Aplicações** no Twenty, onde é possível navegar e instalar tanto apps do marketplace quanto apps implantados por tarball.
|
||||
|
||||
{/* TODO: add screenshot of the UI when the app is registered */}
|
||||
|
||||
Você também pode instalar apps pela linha de comando:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty install
|
||||
```
|
||||
|
||||
<Note>
|
||||
O servidor impõe o versionamento semver na instalação, espelhando as regras da implantação:
|
||||
|
||||
* Instalar a mesma versão que já está instalada no seu espaço de trabalho é rejeitado com um erro `APP_ALREADY_INSTALLED`.
|
||||
* Instalar uma versão inferior à atualmente instalada é rejeitado com um erro `CANNOT_DOWNGRADE_APPLICATION`.
|
||||
|
||||
Para instalar uma versão mais recente, implante ou publique-a primeiro e, em seguida, execute novamente `yarn twenty install`.
|
||||
</Note>
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
title: Habilidades e agentes
|
||||
description: Defina habilidades e agentes de IA para o seu aplicativo.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Skills and agents are currently in alpha. O recurso é funcional, mas ainda está evoluindo.
|
||||
</Warning>
|
||||
|
||||
Os aplicativos podem definir capacidades de IA que residem dentro do espaço de trabalho — instruções de habilidades reutilizáveis e agentes com prompts de sistema personalizados.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineSkill" description="Defina habilidades de agentes de IA">
|
||||
|
||||
As habilidades definem instruções e capacidades reutilizáveis que os agentes de IA podem usar no seu espaço de trabalho. Use `defineSkill()` para definir habilidades com validação integrada:
|
||||
|
||||
```ts src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'Sales Outreach',
|
||||
description: 'Guides the AI agent through a structured sales outreach process',
|
||||
icon: 'IconBrain',
|
||||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||||
1. Research the company and recent news
|
||||
2. Identify the prospect's role and likely pain points
|
||||
3. Draft a personalized message referencing specific details
|
||||
4. Keep the tone professional but conversational`,
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* `name` é uma string de identificador exclusivo para a habilidade (recomenda-se kebab-case).
|
||||
* `label` é o nome de exibição legível por humanos mostrado na UI.
|
||||
* `content` contém as instruções da habilidade — este é o texto que o agente de IA usa.
|
||||
* `icon` (opcional) define o ícone exibido na UI.
|
||||
* `description` (opcional) fornece contexto adicional sobre a finalidade da habilidade.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineAgent" description="Defina agentes de IA com prompts personalizados">
|
||||
|
||||
Agentes são assistentes de IA que vivem dentro do seu espaço de trabalho. Use `defineAgent()` para criar agentes com um prompt de sistema personalizado:
|
||||
|
||||
```ts src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'Helps the sales team draft outreach emails and research prospects',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* `name` é a string de identificador exclusiva do agente (recomenda-se kebab-case).
|
||||
* `label` é o nome de exibição mostrado na UI.
|
||||
* `prompt` é o prompt do sistema que define o comportamento do agente.
|
||||
* `description` (opcional) fornece contexto sobre o que o agente faz.
|
||||
* `icon` (opcional) define o ícone exibido na UI.
|
||||
* `modelId` (opcional) substitui o modelo de IA padrão usado pelo agente.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
Reference in New Issue
Block a user