Skip to main content
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.
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.

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. 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(), 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 — 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 — posiciona-o na página de detalhes de um registro ou em um painel.
  • Definindo-o com defineSettingsFrontComponent() — 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, para que ele apareça como um botão de ação rápida no canto superior direito da página:
src/front-components/hello-world.tsx
src/command-menu-items/hello-world.command-menu-item.ts
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:
Clique nele para renderizar o componente inline.

Campos de configuração

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 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 (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.
src/front-components/app-settings.tsx
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.
src/front-components/sync-tracker.tsx
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:
src/front-components/run-action.tsx
src/command-menu-items/run-action.command-menu-item.ts
E um exemplo usando CommandModal para solicitar confirmação antes de executar:
src/front-components/delete-draft.tsx
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:
src/front-components/open-company-emails.tsx

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 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:
src/front-components/sync-prs.tsx
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:
src/logic-functions/fetch-prs.logic-function.ts
TWENTY_APP_ACCESS_TOKEN é injetado automaticamente — consulte Variáveis de aplicação. 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.

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. 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:
Requisições com falha geram um erro RestApiClientError que expõe status, statusText, url e o body analisado:

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:
src/front-components/record-info.tsx
Hooks disponíveis:

Variáveis de aplicação

Variáveis de aplicação definidas em defineApplication() com isSecret: false estão disponíveis nos componentes de front por meio do utilitário getApplicationVariable:
src/front-components/greeting.tsx
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, que são executadas no lado do servidor. Isso impede que valores sigilosos, como chaves de API, sejam enviados para o navegador.
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. As seguintes variáveis de sistema estão sempre disponíveis via process.env:

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:

API de comunicação do host

Componentes de front-end podem acionar navegação, modais e notificações usando funções de twenty-sdk: 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:
src/front-components/archive-record.tsx

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.
src/front-components/note-draft.tsx
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 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:
src/front-components/bulk-export.tsx
Exiba-o com um item de menu de comando restrito a seleções de registros:
src/command-menu-items/bulk-export.command-menu-item.ts

Recursos públicos

Componentes de front-end podem acessar arquivos do diretório public/ do app usando getPublicAssetUrl:
Veja a seção de recursos públicos 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:
package.json
Cada componente então importa suas dependências exatamente como antes — nada muda no seu código do componente:
src/front-components/counter.tsx
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 inlinestyle={{ color: 'red' }}
  • Componentes de UI da Twenty — a própria biblioteca de componentes da Twenty; consulte Usando componentes de UI da Twenty 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. 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:
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:

Ícones

Importe ícones individuais de 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:
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 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. 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.
requestAnimationFrame, fetch, setTimeout and queueMicrotask trabalham sem o prefixo window.. Apenas window.requestAnimationFrame(...) e amigos lançam.

DOM access

Um ref fornece um elemento da sandbox, não um HTMLElement. 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 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 e use seu armazenamento de chave-valor. 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. 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. 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.