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

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

View File

@@ -0,0 +1,118 @@
---
title: Itens do menu de comandos
description: Exponha componentes de front-end como ações rápidas e entradas do menu de comandos (Cmd+K) com `defineCommandMenuItem`.
icon: terminal
---
Um **item de menu de comando** é a ponte entre o usuário e um [front component](/l/pt/developers/extend/apps/layout/front-components). Ele registra o componente no menu de comandos (Cmd+K) do Twenty e, opcionalmente, como um botão fixado 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',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Campos de configuração
| 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 | **Obsoleto** — ignorado em favor do ícone da aplicação; a compilação emite um aviso se definido |
| `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), `'GLOBAL_OBJECT_CONTEXT'` (apenas em páginas com um contexto de objeto — páginas de índice e de registro), `'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 que controla dinamicamente a visibilidade (veja abaixo) |
## Comandos sem interface
Um item do menu de comandos emparelhado com um [componente de front-end sem interface](/l/pt/developers/extend/apps/layout/front-components#headless-vs-non-headless) é a forma idiomática de disponibilizar uma ação de um clique — executar código, navegar ou confirmar e executar. A página de Front Components aborda os [SDK Command components](/l/pt/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) que lidam com o padrão de ação e desmontagem.
Um fluxo típico: um componente headless renderiza `<Command execute={...} />` (veja o [exemplo completo](/l/pt/developers/extend/apps/layout/front-components#sdk-command-components)), e o item de menu de comando aponta para ele:
```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',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## 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,
),
});
```
<Note>
`RECORD_SELECTION` já implica uma seleção não vazia — use `numberOfSelectedRecords` apenas para contagens específicas (por exemplo, `>= 2`).
</Note>
### Variáveis de contexto
Elas 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 |

View File

@@ -0,0 +1,849 @@
---
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 é executado dentro de um iframe de origem opaca e em sandbox, mas sua interface ainda é renderizada de forma nativa na página em vez de ficar confinada a esse iframe.
<Warning>
Os componentes da frente ainda estão em desenvolvimento activo. Seu código roda contra um DOM parcial, não uma página real do navegador, então usos avançados podem falhar, geralmente silenciosamente. Veja [Limitação atual](#current-limitations).
</Warning>
## Onde os componentes de front-end podem ser usados
Os componentes de front-end podem ser renderizados em três 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 dentro de [layouts de página](/l/pt/developers/extend/apps/layout/page-layouts). 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.
* **Configurações do aplicativo** — Definido com [`defineSettingsFrontComponent()`](#custom-settings-component), o componente de front-end é renderizado como uma seção dentro da aba **Settings** do aplicativo, no lugar da interface padrão de configuração de variáveis.
Um componente de front-end por si só não é acessível pela UI — é preciso *exibi-lo*. As três maneiras de fazer isso são:
* **Associe-o a um [item do menu de comandos](/l/pt/developers/extend/apps/layout/command-menu-items)** — registra-o no menu de comandos (Cmd+K) e, opcionalmente, como uma ação rápida fixada.
* **Incorpore-o como um widget em um [layout de página](/l/pt/developers/extend/apps/layout/page-layouts)** — posiciona-o na página de detalhes de um registro ou em um painel.
* **Definindo-o com [`defineSettingsFrontComponent()`](#custom-settings-component)** — o componente é renderizado como uma seção dentro da aba **Settings** do aplicativo, no lugar da interface padrão de configuração de variáveis.
## Exemplo básico
A maneira mais rápida de ver um componente de front-end em ação é associá-lo a um [`defineCommandMenuItem`](/l/pt/developers/extend/apps/layout/command-menu-items), para que ele apareça 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',
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 apply`), 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 [Layouts de página](/l/pt/developers/extend/apps/layout/page-layouts) para detalhes.
## Componente de configurações personalizadas
Para substituir a interface de configuração de variáveis gerada automaticamente na aba **Settings** do seu aplicativo pelo seu próprio componente, defina-o com `defineSettingsFrontComponent` em vez de `defineFrontComponent`. Ele usa os mesmos [campos de configuração](#configuration-fields) (exceto `isHeadless`, que não é aceito, já que um componente de configurações sempre renderiza uma interface visível) e, adicionalmente, marca o componente como a interface de configurações do app.
O componente é renderizado como uma seção **dentro** da aba Settings, e não como uma substituição de toda a aba. As seções gerenciadas pelo sistema do Twenty — atualização automática, App URL e conexões — são sempre renderizadas acima dela e não podem ser substituídas pelo app.
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
Apenas um componente de configurações de front-end é permitido por app; declarar mais de um faz com que a build falhe. Quando presente, a aba **Settings** do app renderiza este componente no lugar da interface padrão de configuração de variáveis.
## 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 { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
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/front-component`:
* **`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 do painel lateral. As props dependem de `page` — por exemplo, `ViewRecord` recebe `recordId` + `objectNameSingular` (além de um id de `tab` opcional para abrir o registro em uma guia específica), outras páginas recebem `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/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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',
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/front-component';
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,
});
```
E um exemplo usando `CommandOpenSidePanelPage` para abrir o registro atual no painel lateral em uma guia específica. `tab` é um id de guia de layout de página (layouts padrão usam ids como `company-tab-emails` ou `company-tab-timeline`; layouts personalizados usam o próprio id da guia). Se o id não existir no layout do registro, a guia padrão será aberta em seu lugar:
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## Chamando uma função lógica
Os componentes de front são executados no navegador em um Web Worker em sandbox dentro de um iframe de origem opaca, enquanto as [funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) são executadas no servidor. Não há chamada direta no mesmo processo entre os dois — em vez disso, um componente de front acessa uma função lógica via HTTP.
Uma função lógica declarada com `httpRouteTriggerSettings` é acessível por HTTP em seu caminho de rota. `RestApiClient` trata caminhos que começam com `/s/` como rotas de aplicativo, resolve-os para a URL a partir da qual suas funções são servidas e os autentica com `TWENTY_APP_ACCESS_TOKEN`.
> **No Twenty Cloud, funções lógicas acionadas por HTTP são servidas em um domínio dedicado por workspace** em `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Para chamadores externos, copie a URL exata das configurações de **HTTP trigger** da função ou da guia **Settings** do aplicativo.
Um componente de front headless pode executar a chamada ao montar via o componente `Command` e, em seguida, desmontar automaticamente:
```tsx src/front-components/sync-prs.tsx
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
O caminho passado para o `RestApiClient` é o `httpRouteTriggerSettings.path` da função de lógica, prefixado com `/s`. Mantenha `isAuthRequired: true`; o `TWENTY_APP_ACCESS_TOKEN` que a Twenty gera para o seu componente autentica a solicitação:
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_APP_ACCESS_TOKEN` é injetado automaticamente — consulte [Variáveis de aplicação](#application-variables). Como as variáveis de aplicação secretas nunca são expostas aos componentes de front, mantenha as chaves de API e outra lógica sensível na função lógica, não no componente de front.
</Note>
### Chamando a API REST da Twenty
Para chamar rotas HTTP do aplicativo ou ler e gravar registros da Twenty a partir de um front component, use `RestApiClient` de `twenty-client-sdk/rest`. Ele envia caminhos `/s/...` para a URL base das funções do seu workspace e qualquer outro caminho, incluindo `/rest/...`, para `TWENTY_API_URL`.
Ele sempre atua como a pessoa que está visualizando a página. `runAs: 'application'` é uma opção apenas de função de lógica: um componente nunca recebe o token do seu próprio aplicativo, portanto, solicitá-lo aqui gera um erro. Coloque o trabalho que precisa do acesso do próprio aplicativo atrás de uma função de lógica e chame-a em vez disso.
| Método | Descrição |
| --------------------------------- | ------------------------------------------------------------------------------- |
| `get(path, options?)` | Envia uma requisição `GET` |
| `post(path, body?, options?)` | Envia uma requisição `POST` |
| `put(path, body?, options?)` | Envia uma requisição `PUT` |
| `patch(path, body?, options?)` | Envia uma requisição `PATCH` |
| `delete(path, options?)` | Envia uma requisição `DELETE` |
| `request(method, path, options?)` | Requisição genérica com qualquer método HTTP |
| `resolveUrl(path, options?)` | Resolve um caminho para sua URL completa sem enviar uma requisição (para links) |
`options` aceita `headers`, `query` (um registro de parâmetros de query string; valores nulos ou indefinidos são ignorados) e um `AbortSignal` via `signal`. Um objeto `body` que não seja `FormData` é serializado em JSON automaticamente. Em um `401`, o cliente atualiza o access token uma vez por meio do host e tenta a requisição novamente.
A URL base e o token são resolvidos do ambiente por padrão. Passe substituições (overrides) para o construtor quando necessário — por exemplo, em testes:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
Requisições com falha geram um erro `RestApiClientError` que expõe `status`, `statusText`, `url` e o `body` analisado:
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const people = await client.get('/rest/people', {
query: { limit: 10 },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## 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,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
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 |
| `useTimelineActivityId()` | `string` ou `null` | O ID da atividade atual da linha do tempo ao renderizar uma linha personalizada da linha do tempo |
| `useColorScheme()` | `'light'` ou `'dark'` | O esquema de cores ativo da interface do host (`System` já está resolvido) |
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
## Variáveis de aplicação
Variáveis de aplicação definidas em [`defineApplication()`](/l/pt/developers/extend/apps/config/application) com `isSecret: false` estão disponíveis nos componentes de front por meio do utilitário `getApplicationVariable`:
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
Variáveis secretas (`isSecret: true`) **não** são expostas aos componentes de front. Elas estão disponíveis apenas em [funções de lógica](/l/pt/developers/extend/apps/logic/logic-functions), que são executadas no lado do servidor. Isso impede que valores sigilosos, como chaves de API, sejam enviados para o navegador.
</Warning>
`getApplicationVariable` sempre retorna uma **string** (ou `undefined`), independentemente do `type` declarado da variável. A string é serializada de forma consistente por tipo (booleanos como `"true"` / `"false"`, números como strings decimais, arrays / objetos como JSON), o mesmo formato usado para a logic-function `process.env` — faça você mesmo o parse (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Veja [Tipos de variáveis](/l/pt/developers/extend/apps/config/application#variable-types).
As seguintes variáveis de sistema estão sempre disponíveis via `process.env`:
| Variável | Descrição |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | URL base da API principal da Twenty |
| `TWENTY_APP_ACCESS_TOKEN` | Token de curta duração com escopo limitado à interseção entre a função da pessoa conectada e a do seu aplicativo, de modo que um componente nunca possa fazer mais do que a pessoa que o está visualizando |
### `TWENTY_FUNCTIONS_URL`
A Twenty também injeta `TWENTY_FUNCTIONS_URL` em front components e funções de lógica: a URL base a partir da qual as funções de lógica acionadas por HTTP do seu aplicativo são servidas.
Ela existe porque essa URL nem sempre é o próprio servidor da Twenty. No Twenty Cloud, as rotas do aplicativo são servidas em um domínio dedicado por workspace (`https://\<your-workspace-subdomain>.withtwenty.com`, ou o domínio público primário da aplicação quando um é configurado) para que respostas criadas pelo aplicativo sejam executadas em uma origem isolada, em vez de na origem do aplicativo Twenty. Instâncias self-hosted e locais servem rotas do aplicativo sob o prefixo `/s` no próprio servidor e podem não definir a variável. Como a URL base varia por workspace e por instância, seu código não pode defini-la de forma fixa — o servidor injeta o valor correto em tempo de execução.
Você raramente precisa lê-la diretamente. Chame suas rotas por meio de `RestApiClient` com um caminho prefixado com `/s/` e o cliente resolverá a URL para você: ele remove o prefixo `/s` e direciona para `TWENTY_FUNCTIONS_URL`, recorrendo a `\<TWENTY_API_URL>/s` quando a variável não está definida. Use `resolveUrl('/s/\<path>')` para obter a URL absoluta sem enviar uma requisição, por exemplo, para um link. Leia a variável diretamente apenas ao construir uma URL manualmente:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## 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 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Carrega um `Blob` em um campo FILES; resulta em `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` ou `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` |
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 { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
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,
});
```
### Armazenamento
`localStorage` e `sessionStorage` funcionam como em uma página normal, com a API síncrona padrão. Suas chaves são limitadas à instalação do seu app e ao usuário conectado: nenhum outro app pode lê-las, e outro usuário que entrar no mesmo navegador começará com um armazenamento vazio. Os valores gravados em `localStorage` permanecem no dispositivo entre recarregamentos; `sessionStorage` permanece durante a sessão do navegador.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty armazena os valores em nome do seu app, então uma gravação é aplicada localmente imediatamente e salva em segundo plano. Leituras nunca esperam pelo host. Nada é sincronizado: os valores não acompanham o usuário para outro navegador ou máquina, portanto use o [armazenamento de chave-valor](/l/pt/developers/extend/apps/logic/key-value-store) de uma função de lógica para qualquer coisa que precise sobreviver a uma troca de dispositivo.
As gravações têm limite, e cada limite contabiliza caracteres em vez de bytes: as chaves têm no máximo 512 caracteres, um único valor no máximo 262.144 caracteres e cada armazenamento no máximo 1.048.576 caracteres por app e usuário. Uma gravação que ultrapasse um limite gera um `QuotaExceededError`, como na API do navegador.
### 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 } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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,
});
```
Exiba-o com um [item de menu de comando](/l/pt/developers/extend/apps/layout/command-menu-items) restrito a seleções de registros:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## Recursos públicos
Componentes de front-end podem acessar arquivos do diretório `public/` do app usando `getPublicAssetUrl`:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
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/config/public-assets) para obter detalhes.
## Compartilhar dependências pela frente de componentes
Por padrão, cada componente frontal junta sua própria cópia das bibliotecas que importa, então um aplicativo com cinco componentes é React cinco vezes. Declarar as dependências compartilhadas no `pacote` do seu aplicativo. son\` para construir essas bibliotecas uma vez e ter cada componente do aplicativo carregá-las de um único arquivo em cache:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Cada componente então importa suas dependências exatamente como antes — nada muda no seu código do componente:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
Algumas coisas que você precisa saber:
* **Um pacote de dependências compartilhado por aplicativo.** O pacote é construído a partir das dependências do seu aplicativo, então você mantém controle total das versões que você enviou.
* **Liste os especificadores exatos que você importa.** `vinte e input` e `vinte e seis/display` são duas referências; um nome de pacote só por si não cobre seus subcaminhos. A listagem do `react` cobre automaticamente o `react/jsx-runtime`.
* **Compartilhe `react-dom/client` ao lado de `react`.** Cada componente renderiza através de `createRoot`, então deixar de fora significa que cada componente ainda empacota React DOM.
* \*\*O pacote é armazenado em cache. \* É servido sob uma URL de hash de conteúdo com um cache imutável de longa duração, portanto, ele é baixado uma vez e reutilizado em todos os componentes do aplicativo até que uma de suas dependências seja alterada.
* **Componentes que importam nenhum dos pacotes compartilhados nunca baixá-lo.**
## Estilização
Componentes de front-end suportam várias abordagens de estilização. Você pode usar:
* **Estilos inline** — `style={{ color: 'red' }}`
* **Componentes de UI da Twenty** — a própria biblioteca de componentes da Twenty; consulte [Usando componentes de UI da Twenty](#using-twenty-ui-components) abaixo
* **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
## Usando componentes de UI da Twenty
Twenty distribui sua biblioteca de componentes como o pacote [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). Os componentes de front-end podem usá-lo para botões, tags, pílulas de status, chips, avatares, ícones, tipografia e tokens de tema que correspondem automaticamente ao tema claro e escuro do espaço de trabalho.
### Instalação
Adicione o pacote ao seu app, fixado na versão fornecida pela sua instância do Twenty:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` é empacotado no seu componente de front-end em tempo de build, então ele só precisa ser uma dependência do seu app — não há nada para configurar em tempo de execução.
### Importando componentes
Importe a partir do subcaminho correspondente em vez da raiz do pacote, para que apenas os componentes que você usa acabem no seu bundle:
| Subcaminho | O que ele exporta |
| --------------------------- | ----------------------------------------------- |
| `twenty-ui/input` | `Button` e inputs de formulário |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` e mais |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` e mais |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` e mais |
| `twenty-ui/icon` | Componentes `Icon*` (por exemplo, `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
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,
});
```
### Ícones
Importe ícones individuais de `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Cada ícone nomeado é tree-shaken, então importar alguns adiciona pouco ao seu bundle. Evite `IconsProvider`, `useIcons` e `iconsState` — eles trazem todo o conjunto de ícones Tabler (vários MB).
### Temas e tokens de tema
Os componentes do Twenty UI correspondem automaticamente ao tema claro e escuro do espaço de trabalho — o renderizador aplica o esquema de cores ativo no host, e os componentes resolvem suas cores com base nele.
Para usar os mesmos tokens de design nos seus próprios estilos inline, chame o hook `useTheme()`. Ele retorna os tokens de tema do Twenty (espaçamento, cores, raios, fontes) conectados ao tema ativo, sem necessidade de configurar `ThemeProvider` no seu componente:
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
Como `useTheme()` é um hook, você lê os tokens dentro do corpo do componente, então os valores sempre refletem o tema em tempo real. O mesmo mapa de tokens também é exportado como a constante `themeCssVariables`, mas prefira `useTheme()` em componentes de front-end — uma constante em nível de módulo que desreferencia `themeCssVariables` pode ser indefinida enquanto o manifesto do app é extraído.
Para diferenciar explicitamente com base no esquema ativo, leia-o com `useColorScheme()` de `twenty-sdk/front-component`, que retorna 'light' ou 'dark'.
## Limitações atuais
Componentes frontais estão em desenvolvimento ativo. Renderização, estilização, tratamento de eventos, medição de elementos e armazenamento no navegador funcionam bem. Qualquer coisa que vá além disso (chamar um método do DOM em uma ref, observar redimensionamentos de elementos, criar um portal para fora da sua árvore) está ausente ou incompleta hoje, e a maioria falha silenciosamente: sem exceção nem erro de TypeScript, já que o arcabouço é tipado com base no DOM completo do navegador.
Se um desses blocos, [abra um issue](https://github.com/twentyhq/twenty/issues/new/choose) para que seja priorizado.
### Layout e medição
Os elementos podem se medir: o host espelha a geometria no sandbox, então as leituras são respondidas localmente, mas podem estar defasadas em até um quadro, e a primeira leitura de um elemento nunca medido retorna zeros. Depois de escrever, releia em um callback de `requestAnimationFrame` ou em um efeito.
| API | O que acontece |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Funciona, a partir do espelho; atribuir `scrollTop` / `scrollLeft` não faz nada (no-op) |
| `getClientRects()`, `window.matchMedia()` | Lançamento |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Funciona, relatando o **viewport do navegador**; o tamanho próprio do seu widget é `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Retorna apenas os estilos inline do elemento, nunca a cascata calculada pelo host |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (`typeof` guardas fazem trabalho) |
| `MutationObserver` | Funciona, incluindo `subtree`, `attributeFilter`, valores antigos e `takeRecords()` |
O posicionamento a partir de `getBoundingClientRect` agora funciona, mas qualquer coisa que observe mudanças de tamanho por meio de `ResizeObserver` (o `ResponsiveContainer` do recharts, o `autoUpdate` do Floating UI) ainda não funciona. Ainda assim, prefira CSS para layout: sua folha de estilos alcança a página real, então flexbox, grid, `aspect-ratio`, `clamp()` e `@container` se comportam normalmente, sem atraso de frame.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` and `queueMicrotask` trabalham sem o prefixo `window.`. Apenas `window.requestAnimationFrame(...)` e amigos lançam.
</Note>
### DOM access
Um `ref` fornece um elemento da sandbox, não um `HTMLElement`.
| O que você escreve | O que acontece | Usar em vez disso |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Lançamento | Componentes controlados; leia valores de `event.target` |
| `element.classList.add(...)` | Lança (`classList` é `undefined`) | Construa a string `className` |
| `document.createTreeWalker()` | Lançamento | `querySelector()` / `querySelectorAll()` e `getElementById()` funcionam; `getElementsByClassName()` também funciona, mas retorna uma coleção estática, não uma `HTMLCollection` dinâmica |
| `document.activeElement` | Sempre `indefinido` | Rastreie o foco com `onFocus` / `onBlur` |
| `canvas` | Não renderiza nada, sem erro | SVG ou desenhar fora da tela e mostrar o resultado em um elemento `img` |
| `createPortal(node, document.body)` | Não renderiza nada, enquanto `isConnected` reporta sucesso | Sobrepõe em linha com `posição: absoluto`, ou passa a biblioteca seu próprio elemento de contêiner |
A lacuna do portal é o motivo da Radix, da interface do usuário, MUI e da interface de reacção, que não tornam nada por padrão. A maioria aceita uma propriedade de container; aponta-a para um elemento que você renderizou.
### Eventos
Mouse, pointer, touch, drag, teclado, foco, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` e `animationend`/`transitionend` são encaminhados ao host, além de alguns específicos por elemento: `load`/`error` em `img`, área de transferência e composição em `input`/`textarea`, mídia em `video`/`audio`, `toggle` em `details`/`dialog`. Qualquer outra coisa (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, captura de ponteiro, `onLoad` fora de `img`) é descartada sem aviso.
`document.addEventListener()` e `janela. ddEventListener()` registra-se sem erro e nunca dispara, é por isso que um arrastar para o mesmo assim que o ponteiro deixa o elemento no qual ele iniciou. `event.preventDefault()` também não atravessa; formulário de envio, `dragover`/`drop` e cliques de links já estão guardados para você.
### Atributos e estilos
Cada elemento encaminha suas próprias propriedades para o DOM host (`href` em `a`, `src`/`alt` em `img`, `value`/`placeholder`/`disabled` em `input` e assim por diante), além de um conjunto comum em todos os elementos: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` e qualquer atributo `aria-*` / `data-*` (com hífen, portanto `ariaLabel` é descartado). Qualquer coisa fora que seja silenciosamente descartada, então expressa estado personalizado como `data-*`.
O CSS do componente, seja de `import './styles.css'`, CSS-in-JS ou um elemento `style`, é injetado no `head` da página host **sem escopo**. Então os nomes das classes colidem com os próprios do Twenty (prefixe-os, e nunca escrevem bare `div { ... }` seletores) e `@media` coincidem com a janela do navegador ao invés do seu widget (use `@container` com seu próprio `container-type`). Propriedades `style` embutidas não são afetadas.
### Armazenamento e rede
`localStorage` e `sessionStorage` são fornecidos pela Twenty em vez do navegador: o componente é executado em um worker em uma origem opaca, portanto o host armazena os valores em nome do seu app. Veja [armazenamento](#storage) para saber mais sobre seu escopo e limites. IndexedDB, cookies, a Cache API e `BroadcastChannel` continuam indisponíveis. Para manter o estado entre dispositivos, chame uma [função de lógica](/l/pt/developers/extend/apps/logic/logic-functions) e use seu [armazenamento de chave-valor](/l/pt/developers/extend/apps/logic/key-value-store).
`fetch` funciona, com advertências:
* Chamadas para a API de Vinte e as rotas do seu aplicativo são procuradas pelo host, então prefira [`RestApiClient`](#calling-the-twenty-rest-api). Em chamadas de proxied, `AbortSignal` e as outras opções `RequestInit` são descartadas, e apenas os corpos `string` e `URLSearchParams` são suportados.
* Outras origens deixam o sandbox com `Origin: null`, então uma API de terceiros responde apenas se envia `Access-Control-Allow-Origin: *`. Chame-a de uma função lógica.
* `fetch('/rest/people')` nunca corresponde a 20 API, porque o sandbox não tem URL de página para resolver um caminho relativo contra.
### Captura de mídia
`navigator.mediaDevices.getUserMedia()` e `MediaRecorder` funcionam dentro de front components por meio de sandbox polyfills, portanto o código padrão de gravação é executado sem alterações e `MediaRecorder.isTypeSupported` responde para combinações comuns de contêiner/codec. Objetos detalhados de restrição de `getUserMedia` são aceitos, mas não encaminhados — o host captura com seus padrões para os tipos solicitados — e apenas uma captura pode estar ativa por vez entre aplicativos. Armazene um `Blob` gravado com a função host `uploadFile`.
### Outras lacunas
* **Conteúdo de arquivo.** Um `input` do tipo `file` fornece ao seu manipulador apenas os metadados do arquivo, não os bytes, portanto `FileReader` não está disponível. Para fazer upload de um `Blob` que seu código já possui — por exemplo, um produzido por `MediaRecorder` — use a função host `uploadFile`.
* **Arrastar e soltar payloads.** Arraste eventos disparados, mas `event.dataTransfer` é `undefined`.
* **Node embutidos.** `fs`, `path` and `node:crypto` falham na compilação, então mova o nó para uma [função lógica](/l/pt/developers/extend/apps/logic/logic-functions). Criptografia Web, `fetch`, `TextEncoder` e `URL` estão disponíveis.
* **`iframe`** é sempre colocado novamente em sandbox sem `allow-same-origin`, portanto uma incorporação que dependa da própria sessão será renderizada como desconectada. Isso também não tem `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Itens do menu de navegação
description: Adicione entradas personalizadas à barra lateral do espaço de trabalho — links para visualizações salvas ou URLs externas.
icon: bars
---
Um **item do menu de navegação** é uma entrada na barra lateral esquerda. Use `defineNavigationMenuItem()` para disponibilizar links personalizados na barra lateral — normalmente um por [visualização](/l/pt/developers/extend/apps/layout/views) que você disponibiliza — ou para apontar para URLs externas.
```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 onde o item de menu aponta. Cada tipo é associado a um campo identificador específico:
| Tipo | O que faz | Campo obrigatório |
| ------------------------------------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | Abre uma visualização salva | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Abre uma URL externa | `link` |
| `NavigationMenuItemType.FOLDER` | Agrupa itens aninhados sob um rótulo | `name` (e itens filhos fazem referência à pasta por meio de `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Abre a página de índice padrão de um objeto | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Abre um layout de página independente | `pageLayoutUniversalIdentifier` |
* `position` controla a ordenação na barra lateral.
* O enum também contém `NavigationMenuItemType.RECORD`, usado internamente para favoritos de registros criados pelo usuário — não pode ser usado a partir de um manifesto de app (não há nenhum campo para fazer referência a um registro).
* `icon` e `color` são opcionais e personalizam a aparência da entrada.
* `folderUniversalIdentifier` também está disponível em qualquer item para aninhá-lo dentro de um pai do tipo `FOLDER`.
<Note>
**Armadilha comum:** criar um objeto sem um item de menu de navegação associado torna esse objeto inacessível a partir da barra lateral. A menos que seja um objeto técnico/interno, todo objeto personalizado deve ter uma entrada na barra lateral apontando para ele.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Visão Geral
description: Coloque seu app dentro da interface do Twenty — entradas na barra lateral, visualizações salvas, abas na página de registro e componentes React em sandbox.
icon: table-columns
---
A **camada de layout** de um app do Twenty é tudo o que o usuário vê: onde o app aparece na barra lateral, quais visualizações de lista ele fornece, como suas páginas de detalhes de registro são organizadas e quais componentes React personalizados são renderizados dentro dessas páginas.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## Nesta seção
<CardGroup cols={2}>
<Card title="Visualizações" icon="list" href="/l/pt/developers/extend/apps/layout/views">
`defineView` — configurações salvas de lista: colunas visíveis, filtros, grupos.
</Card>
<Card title="Itens do menu de navegação" icon="bars" href="/l/pt/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — entradas da barra lateral apontando para visualizações ou URLs externas.
</Card>
<Card title="Layouts de Página" icon="table-columns" href="/l/pt/developers/extend/apps/layout/page-layouts">
`definePageLayout` e `definePageLayoutTab` — abas e widgets na página de detalhes de um registro.
</Card>
<Card title="Componentes de front-end" icon="window-maximize" href="/l/pt/developers/extend/apps/layout/front-components">
`defineFrontComponent` — componentes React em sandbox que são renderizados dentro do Twenty.
</Card>
<Card title="Itens do menu de comandos" icon="terminal" href="/l/pt/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — registra componentes de front-end como entradas no Cmd+K e ações rápidas.
</Card>
</CardGroup>
## Onde o app aparece
| Superfície | O que controla | Entidade |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Barra lateral** | Uma entrada personalizada que aponta para uma visualização salva ou URL externa | `defineNavigationMenuItem` |
| **Lista de registros** | Uma configuração salva para um objeto — colunas visíveis, ordem, filtros, grupos | `defineView` |
| **Página de detalhes do registro** | As abas e widgets em uma página de registro (do seu próprio objeto ou de um objeto padrão) | `definePageLayout`, `definePageLayoutTab` |
| **Dentro de qualquer uma das opções acima** | Um widget React personalizado — botões, formulários, dashboards, integrações | `defineFrontComponent` |
| **Configurações do app** | Uma seção de configuração personalizada dentro da aba Configurações do app, em vez da interface padrão de variáveis | `defineSettingsFrontComponent` |
| **Menu de comandos (Cmd+K)** | Uma ação rápida fixada ou comando oculto | `defineCommandMenuItem` |
Os componentes de front-end são executados dentro de um Web Worker isolado usando Remote DOM — eles são renderizados *nativamente* na página (não dentro de um iframe), mas não podem acessar diretamente a página host ou o DOM. A comunicação com o Twenty acontece por meio de uma API de host com passagem de mensagens.
Eles ainda estão em desenvolvimento ativo: o sandbox implementa um DOM parcial, portanto usos avançados podem falhar. Veja as [limitações atuais](/l/pt/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Layouts de Página
description: Personalize páginas de detalhes de registros — abas, widgets e onde os front components são renderizados — usando `definePageLayout` e `definePageLayoutTab`.
icon: table-columns
---
Um **layout de página** controla como a página de detalhes de um registro é organizada: quais abas aparecem e quais widgets elas contêm. Use `definePageLayout()` para declarar um layout para um objeto que você possui ou `definePageLayoutTab()` para adicionar uma única aba a um layout que já existe (seu ou um padrão da Twenty).
| Caso de uso | Entidade |
| ------------------------------------------------------------------------------ | --------------------- |
| Definir todo o layout para uma página de registro em um objeto que você possui | `definePageLayout` |
| Adicionar uma aba a um layout existente (seu próprio objeto ou um padrão) | `definePageLayoutTab` |
## definePageLayout
Use isto quando você possuir toda a página de detalhes — normalmente para um objeto personalizado que você próprio definiu.
```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.VERTICAL_LIST,
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` é um dos seguintes: `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` ou `'STANDALONE_PAGE'`. Use `'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`: `VERTICAL_LIST` para páginas de registro e páginas independentes, `GRID` para dashboards e `CANVAS` para um único widget que deve preencher a janela de visualização da aba. Uma aba `VERTICAL_LIST` empilha widgets verticalmente. Widgets integrados que gerenciam sua própria rolagem, como linhas do tempo, arquivos, notas, tarefas e fluxos de trabalho, preenchem uma janela de visualização; campos, componentes de front, gráficos e outros widgets fit-content são renderizados na altura de seu conteúdo ou na altura configurada. Uma aba `GRID` sempre organiza seus widgets como cartões em uma grade de 12 colunas. Um widget `CANVAS` não tem posição explícita; se uma aba de canvas contiver vários widgets, eles serão renderizados na altura do conteúdo, em vez de preencherem a viewport.
* Defina `layoutMode` explicitamente. Omití-lo faz com que você obtenha `VERTICAL_LIST` em uma `STANDALONE_PAGE` e `GRID` em todos os outros casos, o que raramente é o que você deseja em uma página de registro.
* Cada `widget` dentro de uma aba pode renderizar um [front component](/l/pt/developers/extend/apps/layout/front-components), uma lista de relações ou outros tipos de widget nativos.
* Um widget `FRONT_COMPONENT` pode definir `headerCommandMenuItemUniversalIdentifiers` como uma matriz ordenada de identificadores universais de itens de menu de comandos do mesmo app. Essas ações aparecem como botões de ícone no cabeçalho do cartão do widget e mantêm suas verificações de disponibilidade e permissões no nível do comando. Os identificadores devem ser exclusivos e devem ser resolvidos quando o app for instalado.
* `position` nas abas controla sua ordem. Use valores mais altos (por exemplo, 50) para colocar abas personalizadas após as nativas.
### Widgets de campos
Um widget `FIELD` renderiza um campo do registro. Para campos de relação, ele também pode incorporar uma lista de registros relacionados:
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` recebe o identificador universal de um campo no objeto do layout.
* `fieldDisplayMode` é um dentre `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` ou `'TABLE'`. `TABLE` incorpora uma visualização que lista os registros de um campo de relação um-para-muitos.
* `nestedRelationFieldMetadataId` é opcional e recebe o identificador universal de um campo de relação um-para-muitos no objeto de destino da relação, para listar registros a dois saltos de relação (por exemplo, uma página de Empresa listando as oportunidades das pessoas da empresa, ou uma página de Pessoa listando as oportunidades da empresa da pessoa). O primeiro salto pode ser um campo de relação um-para-muitos ou muitos-para-um, o segundo deve ser um-para-muitos (relações de junção não são compatíveis) e isso requer `fieldDisplayMode: 'TABLE'` — combiná-lo com qualquer outro modo de exibição é um erro de validação, já que um widget aninhado sempre é renderizado como uma visualização incorporada.
## definePageLayoutTab
Use isto quando você quiser apenas **adicionar** uma aba a um layout existente — por exemplo, uma aba de analytics na página padrão de Company ou uma aba de resumo de IA anexada ao layout do seu próprio objeto.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
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** e deve apontar para um layout de página que já exista no momento da instalação — seja um layout padrão da Twenty ou um definido pelo seu próprio aplicativo. Referências entre aplicativos para layouts pertencentes a outro aplicativo instalado não são compatíveis atualmente. Quando o layout pai estiver ausente, a instalação falha com um erro de validação claro.
* Para layouts padrão do Twenty, importe identificadores de `twenty-sdk/define`:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Cada entrada de layout também expõe suas `tabs` e seus `widgets`, para que você possa fazer referência a qualquer nível:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Um alias abreviado `STANDARD_PAGE_LAYOUT` também está disponível:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` têm escopo apenas para esta aba — eles referenciam [front components](/l/pt/developers/extend/apps/layout/front-components), 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ê possuir todo o layout.

View File

@@ -0,0 +1,113 @@
---
title: Visualizações
description: Distribua visualizações salvas e pré-configuradas — ordem das colunas, filtros, grupos — para objetos no seu app.
icon: list
---
Uma **visualização** é uma configuração salva de como os registros de um objeto são exibidos: quais campos aparecem, sua ordem, se estão visíveis e quaisquer filtros ou grupos aplicados. Use `defineView()` para enviar visualizações pré-configuradas com seu app.
Cada objeto recebe automaticamente sua principal visualização de lista (a tabela "All ..." que um item de navegação `OBJECT` abre), provisionada pelo servidor — incluindo os objetos que seu app cria, com uma coluna por campo que seu app declara. As visualizações que você define com `defineView()` são sempre visualizações **adicionais** em cima daquela padrão.
```ts src/views/example-view.ts
import { defineView } 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',
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. Pode ser um objeto personalizado que você definiu ou um objeto padrão do Twenty.
* A visualização principal de lista do objeto é de propriedade do servidor: `key` está obsoleto e é ignorado, portanto uma visualização definida no manifesto nunca poderá reivindicá-la. Envie um item de navegação `VIEW` se quiser que sua visualização fique a um clique de distância na barra lateral.
* `fields` controla quais colunas aparecem e em que ordem. Cada campo referencia um `fieldMetadataUniversalIdentifier`. Para fazer referência a um campo de sistema criado automaticamente, como `createdAt`, consulte [Direcionamento de metadados do sistema](/l/pt/developers/extend/apps/data/system-fields).
* Você também pode declarar `filters`, `filterGroups`, `sorts`, `groups` e `fieldGroups` para configurações avançadas.
* `position` controla a ordenação quando existem várias visualizações para o mesmo objeto.
## Propriedades opcionais
| Propriedade | Valores | Descrição |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (padrão), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Como os registros são dispostos. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` e `CALENDAR_WIDGET` também existem, mas são usados internamente por widgets de layout de página.) |
| `visibility` | `ViewVisibility.WORKSPACE` (padrão), `ViewVisibility.UNLISTED` | Se a visualização é listada em todo o espaço de trabalho ou oculta nos seletores. |
| `openRecordIn` | obsoleto | Não é mais lido: o local onde os registros são abertos agora é uma propriedade do [objeto](/l/pt/developers/extend/apps/data/objects) (`openRecordIn` em `defineObject()`), recorrendo à preferência de cada membro. |
| `ordenações` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Ordem de classificação padrão. |
| `isCompact` | `boolean` | Exibição compacta de linhas. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Agrupar registros (por exemplo, colunas kanban) por um campo. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Agregações e dimensionamento de colunas kanban. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Visualizações de calendário: layout, o campo de data que posiciona os registros e um campo de data final opcional. |
Todos os enums acima são exportados de `twenty-sdk/define`.
## Filtros
Uma visualização pode vir com filtros pré-aplicados. Cada filtro tem três coordenadas: o **campo** a ser filtrado, o **operador** (como comparar) e o **valor** (com o que comparar). As três precisam estar alinhadas — usar um operador que não se aplica a um tipo de campo será rejeitado no momento da sincronização.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Operadores compatíveis por tipo de campo
| Tipo de campo | Operandos suportados |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Tipos de campos com nomes semelhantes podem usar operandos completamente diferentes — `SELECT` e `MULTI_SELECT` sendo um caso comum.
### Formato do valor por operando
O campo `value` é sempre um valor serializável em JSON, mas o seu formato esperado depende do operando:
| Família do operando | Formato do valor | Exemplo |
| ----------------------------------------------------- | ----------------------------------- | ------------------------ |
| `IS`, `IS_NOT` em `SELECT` | array de chaves de opções (strings) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` em `MULTI_SELECT` | array de chaves de opções (strings) | `['TAG_A']` |
| `IS`, `IS_NOT` em `RELATION` | array de IDs de registros (uuids) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` em campos de texto | string | `'acme'` |
| `IS`, `IS_NOT` em `NUMBER` | string (o valor) | `'5'` |
| `IS` em `RATING` / `UUID` | string (o valor) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | string (o limite) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` em `DATE` / `DATE_TIME` | string ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | string vazia | `''` |
| `IS` em `BOOLEAN` | `'true'` ou `'false'` | `'true'` |
## Como as visualizações aparecem na UI
Uma visualização por si só não é acessível a partir da barra lateral. Para fazê-la aparecer lá, associe-a a um [item de menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items) do tipo `VIEW` que aponte para o `universalIdentifier` da visualização. Esse é o padrão canônico: todo objeto personalizado recebe uma exibição de índice padrão gerada automaticamente no momento da criação, então você só precisa incluir o item da barra lateral que a abre.