ad3291f4b4
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23338?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
699 lines
32 KiB
Plaintext
699 lines
32 KiB
Plaintext
---
|
|
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.
|
|
|
|
## 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`.
|
|
|
|
| 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 |
|
|
| `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 limitado ao escopo do papel do seu app |
|
|
|
|
### `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 |
|
|
|
|
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,
|
|
});
|
|
```
|
|
|
|
### 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.
|
|
|
|
## 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'.
|