Saltar para o conteúdo principal
Componentes de front-end são componentes React que renderizam diretamente dentro da UI do Twenty. Eles são executados em um Web Worker isolado usando Remote DOM — seu código é sandboxed, mas renderiza nativamente na página, não em um iframe.

Onde os componentes de front-end podem ser usados

Os componentes de front-end podem ser renderizados em dois locais dentro do Twenty:
  • Painel lateral — Componentes de front-end não headless abrem no painel lateral direito. Este é o comportamento padrão quando um componente de front-end é acionado pelo menu de comandos.
  • Widgets (painéis e páginas de registro) — Componentes de front-end podem ser incorporados como widgets 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.
Um componente de front-end por si só não é acessível pela UI — é preciso exibi-lo. As duas 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.

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
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,
});
src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';

export default defineCommandMenuItem({
  universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
  shortLabel: 'Hello',
  label: 'Hello World',
  icon: 'IconBolt',
  isPinned: true,
  availabilityType: 'GLOBAL',
  frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
Após sincronizar com yarn twenty dev (ou executando uma única vez o yarn twenty dev --once), a ação rápida aparece no canto superior direito da página:
Botão de ação rápida no canto superior direito
Clique nele para renderizar o componente inline.

Campos de configuração

CampoObrigatórioDescrição
universalIdentifierSimID único e estável para este componente
componentSimUma função de componente React
nameNãoNome de Exibição
descriptionNãoDescrição do que o componente faz
isHeadlessNãoDefina 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 para detalhes.

Headless vs não headless

Os componentes de front-end têm dois modos de renderização controlados pela opção isHeadless: Não headless (padrão) — O componente renderiza uma interface visível. Quando acionado pelo menu de comandos, ele é aberto no painel lateral. Este é o comportamento padrão quando isHeadless é false ou omitido. Headless (isHeadless: true) — O componente é montado de forma invisível em segundo plano. Ele não abre o painel lateral. Componentes headless são projetados para ações que executam lógica e, em seguida, se desmontam — por exemplo, executar uma tarefa assíncrona, navegar para uma página ou exibir um modal de confirmação. Eles se combinam naturalmente com os componentes Command do SDK descritos abaixo.
src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';

const SyncTracker = () => {
  const recordId = useRecordId();

  useEffect(() => {
    enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
  }, [recordId]);

  return null;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'sync-tracker',
  description: 'Tracks record views silently',
  isHeadless: true,
  component: SyncTracker,
});
Como o componente retorna null, o Twenty ignora renderizar um contêiner para ele — nenhum espaço vazio aparece no layout. O componente ainda tem acesso a todos os hooks e à API de comunicação do host.

Componentes Command do SDK

O pacote twenty-sdk fornece quatro componentes auxiliares Command projetados para componentes de front-end headless. Cada componente executa uma ação ao montar, trata erros exibindo uma notificação de snackbar e desmonta automaticamente o componente de front-end ao concluir. Importe-os de twenty-sdk/command:
  • Command — Executa um callback assíncrono via a prop execute.
  • CommandLink — Navega para um caminho do app. Props: to, params, queryParams, options.
  • CommandModal — Abre um modal de confirmação. Se o usuário confirmar, executa o callback execute. Props: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — Abre uma página específica do painel lateral. Props: page, pageTitle, pageIcon.
Aqui está um exemplo completo de um componente de front-end headless usando Command para executar uma ação a partir do menu de comandos:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

const RunAction = () => {
  const execute = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      createTask: {
        __args: { data: { title: 'Created by my app' } },
        id: true,
      },
    });
  };

  return <Command execute={execute} />;
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
});
src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';

export default defineCommandMenuItem({
  universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
  label: 'Run my action',
  icon: 'IconPlayerPlay',
  frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
E um exemplo usando CommandModal para solicitar confirmação antes de executar:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';

const DeleteDraft = () => {
  const execute = async () => {
    // perform the deletion
  };

  return (
    <CommandModal
      title="Delete draft?"
      subtitle="This action cannot be undone."
      execute={execute}
      confirmButtonText="Delete"
      confirmButtonAccent="danger"
    />
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
  name: 'delete-draft',
  description: 'Deletes a draft with confirmation',
  component: DeleteDraft,
  isHeadless: true,
});

Chamando uma função lógica

Os componentes de front são executados no navegador em um Web Worker isolado, 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 é exposta sob o endpoint /s/ em ${TWENTY_API_URL}/s\<path>. Seu componente de front chama essa rota com o RestApiClient de twenty-client-sdk/rest, que autentica com o TWENTY_APP_ACCESS_TOKEN que a Twenty injeta no worker. O RestApiClient foi criado exatamente para isso. Ele lê TWENTY_API_URL e TWENTY_APP_ACCESS_TOKEN do ambiente do worker, adiciona o cabeçalho Authorization: Bearer, serializa e analisa JSON e lança um RestApiClientError quando o token ou a URL estão ausentes ou a resposta não é 2xx — para que você não precise reimplementar esse boilerplate em todos os componentes. 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
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { RestApiClient } from 'twenty-client-sdk/rest';

const SyncPrs = () => {
  const execute = async () => {
    const client = new RestApiClient();

    await client.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 cliente é o caminho público da rota — o httpRouteTriggerSettings.path da função de lógica, prefixado com /s. Mantenha isAuthRequired: true; o cliente fornece o token de acesso do app que o Twenty emite para o seu componente:
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,
  },
});
TWENTY_API_URL e TWENTY_APP_ACCESS_TOKEN são injetados 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.

Referência do RestApiClient

Importe RestApiClient de twenty-client-sdk/rest. Ele pertence à mesma família de clientes que CoreApiClient e MetadataApiClient, mas tem como alvo as rotas HTTP do seu app em vez da API GraphQL.
MétodoDescriçã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
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:
const client = new RestApiClient({
  baseUrl: 'https://api.example.com',
  token: 'my-token',
});
Requisições com falha geram um erro RestApiClientError que expõe status, statusText, url e o body analisado:
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';

const client = new RestApiClient();

try {
  const prs = await client.get('/s/github/fetch-prs', {
    query: { state: 'open' },
  });
} 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:
src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
  useUserId,
  useRecordId,
  useFrontComponentId,
} from 'twenty-sdk/front-component';

const RecordInfo = () => {
  const userId = useUserId();
  const recordId = useRecordId();
  const componentId = useFrontComponentId();

  return (
    <div>
      <p>User: {userId}</p>
      <p>Record: {recordId ?? 'No record context'}</p>
      <p>Component: {componentId}</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
  name: 'record-info',
  component: RecordInfo,
});
Hooks disponíveis:
HookRetornaDescrição
useUserId()string ou nullO ID do usuário atual
useSelectedRecordIds()string[]Todos os IDs dos registros selecionados (array vazio se nenhum estiver selecionado)
useRecordId()string ou nullObsoleto. Use useSelectedRecordIds() em vez disso
useFrontComponentId()stringO ID desta instância do componente
useColorScheme()'light' ou 'dark'O esquema de cores ativo da interface do host (System já está resolvido)
useFrontComponentExecutionContext(selector)variaAcesse o contexto de execução completo com uma função seletora

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
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,
});
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.
As seguintes variáveis de sistema estão sempre disponíveis via process.env:
VariávelDescrição
TWENTY_API_URLURL base da API da Twenty
TWENTY_APP_ACCESS_TOKENToken de curta duração limitado ao escopo do papel do seu app

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çãoDescrição
navigate(to, params?, queryParams?, options?)Navegar para uma página no app
openSidePanelPage(params)Abrir um painel lateral
closeSidePanel()Fechar o painel lateral
openCommandConfirmationModal(params)Mostrar um diálogo de confirmação
enqueueSnackbar(params)Mostrar uma notificação do tipo toast
unmountFrontComponent()Desmontar o componente
updateProgress(progress)Atualizar um indicador de progresso
Aqui está um exemplo que usa a API do host para exibir um snackbar e fechar o painel lateral após a conclusão de uma ação:
src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';

const ArchiveRecord = () => {
  const recordId = useRecordId();

  const handleArchive = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      updateTask: {
        __args: { id: recordId, data: { status: 'ARCHIVED' } },
        id: true,
      },
    });

    await enqueueSnackbar({
      message: 'Record archived',
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Archive this record?</p>
      <button onClick={handleArchive}>Archive</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
  name: 'archive-record',
  description: 'Archives the current record',
  component: ArchiveRecord,
});

Trabalhando com vários registros

Use useSelectedRecordIds() para lidar com vários registros selecionados. Isso é útil para operações em lote:
src/front-components/bulk-export.tsx
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';

const BulkExport = () => {
  const selectedRecordIds = useSelectedRecordIds();

  const handleExport = async () => {
    const client = new CoreApiClient();

    for (const recordId of selectedRecordIds) {
      await client.mutation({
        updateTask: {
          __args: { id: recordId, data: { exported: true } },
          id: true,
        },
      });
    }

    await enqueueSnackbar({
      message: `Exported ${selectedRecordIds.length} records`,
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Export {selectedRecordIds.length} selected record(s)?</p>
      <button onClick={handleExport}>Export</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
  name: 'bulk-export',
  description: 'Export selected records',
  component: BulkExport,
  command: {
    universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
    label: 'Bulk Export',
    availabilityType: 'RECORD_SELECTION',
    conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
  },
});

Recursos públicos

Componentes de front-end podem acessar arquivos do diretório public/ do app usando getPublicAssetUrl:
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 para obter detalhes.

Estilização

Componentes de front-end suportam várias abordagens de estilização. Você pode usar:
  • Estilos inlinestyle={{ color: 'red' }}
  • Componentes de UI do Twenty — importe de twenty-sdk/ui (Button, Tag, Status, Chip, Avatar e mais)
  • Emotion — CSS-in-JS com @emotion/react
  • Styled-components — padrões styled.div
  • Tailwind CSS — classes utilitárias
  • Qualquer biblioteca CSS-in-JS compatível com React
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';

const StyledWidget = () => {
  return (
    <div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
      <Button title="Click me" onClick={() => alert('Clicked!')} />
      <Tag text="Active" color="green" />
      <Status color="green" text="Online" />
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
  name: 'styled-widget',
  component: StyledWidget,
});