i18n - docs translations (#22617)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22617?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
07a921f8ca
commit
18ca89bcdd
+81
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: 5. An AI agent
|
||||
icon: robot
|
||||
description: Deixe um agente gerar documentos a partir de um chat, usando sua ferramenta.
|
||||
---
|
||||
|
||||
Porque `generate-document` é exposto como uma **ferramenta**, um agente de IA pode chamá-lo.
|
||||
Vamos adicionar um agente e uma habilidade para que os usuários possam dizer apenas *"generate a proposal for
|
||||
Jeffery Griffin"*.
|
||||
|
||||
## A habilidade
|
||||
|
||||
A [skill](/l/pt/developers/extend/apps/logic/skills-and-agents) é reutilizável
|
||||
instruções — conhecimento que anexa a agentes. Nós ensinamos o modelo como usar
|
||||
a ferramenta.
|
||||
|
||||
```ts filename="src/skills/document-drafting.skill.ts"
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'document-drafting',
|
||||
label: 'Document drafting',
|
||||
icon: 'IconFileText',
|
||||
content: [
|
||||
'To generate a document, call the `generate-document` tool with:',
|
||||
'- `templateId`: the id of the document template to use.',
|
||||
'- `recordId`: the id of the Person or Company the document is for.',
|
||||
'',
|
||||
'If the user names a template or person instead of an id, find the record first,',
|
||||
'then pass its id. Make sure the template target matches the record type.',
|
||||
].join('\n'),
|
||||
});
|
||||
```
|
||||
|
||||
## O agente
|
||||
|
||||
Um [agent](/l/pt/developers/extend/apps/logic/skills-and-agents) emparelha um prompt com um modelo
|
||||
. Defina `responseFormat` explicitamente para evitar um aviso de construção.
|
||||
|
||||
```ts filename="src/agents/document-assistant.agent.ts"
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'document-assistant',
|
||||
label: 'Document Assistant',
|
||||
description: 'Generates documents from your templates and CRM records.',
|
||||
icon: 'IconFileText',
|
||||
responseFormat: { type: 'text' },
|
||||
prompt: [
|
||||
'You are the Document Assistant for a CRM.',
|
||||
'You help users generate personalized documents from reusable templates',
|
||||
'and the data already in their CRM. Use the generate-document tool, and',
|
||||
'always confirm what you created.',
|
||||
].join(' '),
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
O agente só pode chamar a ferramenta se o seu papel lhe permitir. Nós já definimos
|
||||
`canAccessAllTools: true` and `canBeAssignedToAgents: true` on the app's role in
|
||||
[Capítulo 2](/l/pt/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
|
||||
</Note>
|
||||
|
||||
## Experimente isso
|
||||
|
||||
Abra um chat com o **Assistente de Documentos** e peça para ele redigir um documento para uma
|
||||
pessoa no seu CRM. Ele encontra o registro, chama `gerar documento` e relata
|
||||
o documento que ele criou — que agora aparece na visão **Documentos**,
|
||||
exatamente como o menu de comando e caminhos do fluxo de trabalho.
|
||||
|
||||
Esse é o benefício de expor a lógica como uma ferramenta: **uma função, muitas portas da frente** —
|
||||
menu de comando, HTTP, passo do fluxo de trabalho e agora a linguagem natural.
|
||||
|
||||
**Após este passo:** o aplicativo está cheio de recursos e realmente útil. Hora de
|
||||
enviar.
|
||||
|
||||
<Card title="Próximo: publicação →" icon="rocket" href="/desenvolvedores/adicionar/apps/tutorials/document-gerador/publicação">
|
||||
Adicionar metadados do mercado e publicar.
|
||||
</Card>
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: 4. Construindo a interface
|
||||
icon: table-columns
|
||||
description: Views, navegação da barra lateral, um comando e componentes iniciais.
|
||||
---
|
||||
|
||||
No momento, os objetos só podem ser acessados através de Configurações. Vamos dar ao aplicativo uma presença
|
||||
real na UI: listar exibições, entradas da barra lateral, um clique
|
||||
**Gerar documento** comando, um componente frontal de record-page para **visualizar** um documento
|
||||
e uma guia de **editor** nativa para templates.
|
||||
|
||||
## Visualizações e navegação
|
||||
|
||||
A [view](/l/pt/developers/extend/apps/layout/views) é uma lista salva de um determinado objeto.
|
||||
Um [item de menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items)
|
||||
coloca essa visualização na barra lateral.
|
||||
|
||||
```ts filename="src/views/documents.view.ts"
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'All documents',
|
||||
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconFile',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0, isVisible: true, size: 280 },
|
||||
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 1, isVisible: true, size: 120 },
|
||||
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 2, isVisible: true, size: 200 },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Documents',
|
||||
icon: 'IconFile',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
Adicionar o mesmo par para modelos. Ambos agora aparecem na barra lateral:
|
||||
|
||||
<Frame caption="Documentos e Modelos na barra lateral, com o documento gerado listado.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Visualização de documentos com um documento gerado" />
|
||||
</Frame>
|
||||
|
||||
## Um componente frontal
|
||||
|
||||
A [front component](/l/pt/developers/extend/apps/layout/front-components) é um componente React
|
||||
sandboxed dentro de Twenty. Ours lê o registro selecionado, carrega o modelo de pessoa
|
||||
via `CoreApiClient`, e POSTs até a rota do último capítulo
|
||||
.
|
||||
|
||||
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
|
||||
const GenerateDocumentForm = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
|
||||
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
|
||||
const [templateId, setTemplateId] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
new CoreApiClient()
|
||||
.query({ documentTemplates: {
|
||||
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
|
||||
edges: { node: { id: true, name: true } } } })
|
||||
.then(({ documentTemplates }) => {
|
||||
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
|
||||
setTemplates(list);
|
||||
if (list[0]) setTemplateId(list[0].id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const generate = async () => {
|
||||
const apiBaseUrl = process.env.TWENTY_API_URL;
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
|
||||
const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ templateId, recordId }),
|
||||
}).then((r) => r.json());
|
||||
await enqueueSnackbar({
|
||||
message: res.success ? 'Document generated.' : 'Generation failed.',
|
||||
variant: res.success ? 'success' : 'error',
|
||||
});
|
||||
};
|
||||
|
||||
// ...render a <select> of templates and a Generate button
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'generate-document-form',
|
||||
component: GenerateDocumentForm,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Estilo com variáveis CSS inline (`var(--t-color-blue)`), não valores importados de
|
||||
`25ui`. O SDK simula esse pacote durante a build, portanto, imports em nível de módulo de
|
||||
constantes de tema seriam `undefined`. Veja o
|
||||
[componente inteiro](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
|
||||
</Warning>
|
||||
|
||||
## Um comando para abri-lo
|
||||
|
||||
Um [item de menu de comando](/l/pt/developers/extend/apps/layout/command-menu-items) com
|
||||
`availabilityType: 'RECORD_SELECTION'` aparece quando uma pessoa é selecionada, e
|
||||
abre o componente no painel lateral.
|
||||
|
||||
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
|
||||
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Generate document',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
availabilityObjectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
frontComponentUniversalIdentifier:
|
||||
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
## Experimente todo o fluxo
|
||||
|
||||
Abra **Pessoas**, marque uma pessoa e pressione <kbd>⌘K</kbd> / <kbd>Ctrl K</kbd>.
|
||||
"Gerar documento" aparece, marcado com seu aplicativo:
|
||||
|
||||
<Frame caption="O comando aparece quando uma pessoa é selecionada.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Menu de comando com o documento gerado" />
|
||||
</Frame>
|
||||
|
||||
Executá-lo — seu componente abre no painel lateral. Escolha um modelo, clique
|
||||
**Gerar** e uma nova terra em **Documentos**.
|
||||
|
||||
<Frame caption="O componente inicial, carregando templates e gerando no clique.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Gerar painel lateral do documento" />
|
||||
</Frame>
|
||||
|
||||
Cada documento gerado grava o seu aplicativo como seu autor:
|
||||
|
||||
<Frame caption="Criado pelo Gerador do Documento: Status Gerado.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Um registro de documento gerado" />
|
||||
</Frame>
|
||||
|
||||
## Pré-visualizar um documento em sua página de registro
|
||||
|
||||
Um componente frontal não é apenas para menus de comando. Você pode montar um como uma \*\*aba de uma página de registro
|
||||
. Vamos adicionar uma aba *Pré-visualização* ao registro de documento que renderiza o corpo
|
||||
Markdown como uma página polida e impressa.
|
||||
|
||||
O componente lê o id de registro atual de seu contexto de execução, carrega o documento
|
||||
e o renderiza. Componentes frontais executados em uma **sandbox** que só permite uma lista
|
||||
branca de tags HTML — injeção HTML bruta (`dangerouslySetInnerHTML`) e
|
||||
`\<style>` estão bloqueados — então renderizamos o Markdown como elementos React com estilos inline
|
||||
através de um pequeno [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
|
||||
helper.
|
||||
|
||||
```tsx filename="src/front-components/document-viewer.front-component.tsx"
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
|
||||
import { Markdown } from 'src/utils/markdown-to-react';
|
||||
|
||||
const DocumentViewer = () => {
|
||||
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
|
||||
// ...load { content, file } for recordId, then derive the links:
|
||||
const pdfUrl = document.file?.[0]?.url;
|
||||
const webUrl = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?id=${recordId}`;
|
||||
|
||||
// Render the template body, plus quick links to the web page and the PDF.
|
||||
// Links open in a new tab so they don't navigate the embedded component.
|
||||
return (
|
||||
<div style={styles.scroll}>
|
||||
<div style={styles.actions}>
|
||||
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
|
||||
Open web page
|
||||
</a>
|
||||
{pdfUrl ? (
|
||||
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
|
||||
Download PDF
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={styles.paper}>
|
||||
<div style={styles.body}>
|
||||
<Markdown content={document.content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'document-viewer',
|
||||
component: DocumentViewer,
|
||||
});
|
||||
```
|
||||
|
||||
Monte-o com um [layout da página](/l/pt/developers/extend/apps/layout/page-layouts). Um layout
|
||||
`RECORD_PAGE` adiciona abas à vista de registro de um objeto; um widget `FRONT_COMPONENT`
|
||||
em uma aba `CANVAS` hospeda o componente:
|
||||
|
||||
```ts filename="src/page-layouts/document-record.page-layout.ts"
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Document record page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [{
|
||||
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Preview',
|
||||
icon: 'IconEye',
|
||||
position: 50,
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [{
|
||||
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Document preview',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
}],
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
Abra qualquer documento — uma guia **Pré-visualizar** renderiza belíssima, com links para a página web compartilhável de
|
||||
e o PDF:
|
||||
|
||||
<Frame caption="A aba de Pré-visualização renderiza o documento com estilos embutidos, mais links rápidos.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Visualizador de documentos do componente frontal em uma aba de registros" />
|
||||
</Frame>
|
||||
|
||||
## Edite um modelo com o editor de texto rico
|
||||
|
||||
Os templates não precisam de um componente personalizado. Como o `body` é um campo
|
||||
`RICH_TEXT`, Twenty já fornece um editor de rich text completo para ele — o
|
||||
mesmo que os objetos padrão Note e Task usam. Nós apenas a apresentamos na
|
||||
página de registro de template.
|
||||
|
||||
Adicione uma aba com um widget `FIELD` no modo de exibição `EDITOR`, apontando para o campo `body`
|
||||
através de `fieldMetadataId`:
|
||||
|
||||
```ts filename="src/page-layouts/template-record.page-layout.ts"
|
||||
{
|
||||
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Template',
|
||||
position: 1,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
widgets: [{
|
||||
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Template',
|
||||
type: 'FIELD',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
|
||||
configuration: {
|
||||
configurationType: 'FIELD',
|
||||
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldDisplayMode: 'EDITOR',
|
||||
},
|
||||
}],
|
||||
}
|
||||
```
|
||||
|
||||
Um campo `RICH_TEXT` armazena tanto o JSON de blocos do editor quanto uma
|
||||
projeção em Markdown. O pipeline de geração lê essa projeção Markdown, então
|
||||
espaços reservados, o PDF, e a página da web compartilhável continuam funcionando inalterado —
|
||||
veja o
|
||||
[`template-record. leiaute-idade.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
|
||||
Agora editores escrevem modelos em um editor de texto rico:
|
||||
|
||||
<Frame caption="A aba Modelo: editor de texto rico nativo de 20 vinculado ao campo de corpo.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Registro de modelo com a aba de editor de texto rico nativo" />
|
||||
</Frame>
|
||||
|
||||
**Após este passo:** os documentos pré-visualizar lindo e os templates são editáveis
|
||||
dentro do aplicativo. Em seguida, deixe um atendente da IA gerá-los a partir de um chat.
|
||||
|
||||
<Card title="Próximo: um atendente de IA →" icon="robot" href="/desenvolvedores/extend/apps/tutorials/document-generator/ai-agent">
|
||||
Adicione um agente e uma habilidade que chama sua ferramenta.
|
||||
</Card>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: 1. Modelo de dados
|
||||
icon: database
|
||||
description: Modela documentos e templates com objetos, campos e uma relação.
|
||||
---
|
||||
|
||||
Nosso aplicativo precisa de dois objetos personalizados: **modelos de documentos** (o que escrever) e
|
||||
**documentos** (o resultado gerado). Vamos defini-los.
|
||||
|
||||
Crie cada arquivo de entidade com o CLI — gera uma pasta UUID válida e
|
||||
correta para você:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
```
|
||||
|
||||
Abaixo nós mostramos os arquivos concluídos.
|
||||
|
||||
<Note>
|
||||
Todo `*_UNIVERSAL_IDENTIFIER` constante vive em
|
||||
`src/constants/universal-identifiers.ts` e é importado onde usado. Os trechos
|
||||
abaixo omitem essas importações por brevidade — mantenha-as em seus próprios arquivos.
|
||||
</Note>
|
||||
|
||||
## O objeto modelo
|
||||
|
||||
Um template tem um `nome`, um `body` com `{{placeholders}}`, e um `alvo` que
|
||||
diz se é escrito para uma pessoa ou para uma empresa. O `body` é um campo
|
||||
`RICH_TEXT`, então Vinte dá um editor completo de texto rico.
|
||||
|
||||
```ts filename="src/objects/document-template.object.ts"
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'documentTemplate',
|
||||
namePlural: 'documentTemplates',
|
||||
labelSingular: 'Document template',
|
||||
labelPlural: 'Document templates',
|
||||
icon: 'IconFileText',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
|
||||
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
|
||||
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
|
||||
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
|
||||
defaultValue: `'PERSON'`,
|
||||
options: [
|
||||
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
|
||||
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
|
||||
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
|
||||
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
|
||||
] },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`SELECT` opção **valores** deve ser `UPPER_CASE` (`PERSON`, não `person`), e o
|
||||
`defaultValue` está entre aspas extras: `` `'PERSON'` ``. A `etiqueta` é o que
|
||||
usuários veem.
|
||||
</Warning>
|
||||
|
||||
## O objeto do documento
|
||||
|
||||
O documento gerado armazena o `conteúdo` renderizado e um `estado`. Defina
|
||||
da mesma forma, com uma seleção `status` de `DRAFT` / `GENERATED`. Arquivo completo:
|
||||
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
|
||||
|
||||
## Vinculando eles com uma relação
|
||||
|
||||
Cada documento deve apontar para o modelo de onde veio. Relações são
|
||||
**bidirecionais** — você define ambos os lados, cada um em seu próprio arquivo de campo.
|
||||
|
||||
```ts filename="src/fields/document-template-relation.field.ts"
|
||||
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
|
||||
|
||||
// The "many" side: each document belongs to one template.
|
||||
export default defineField({
|
||||
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'template',
|
||||
label: 'Template',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'templateId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
O outro lado (`template-documents-relation.field.ts`) é um campo
|
||||
`RelationType.ONE_TO_MANY` chamado `documentos` que aponta para o lado oposto.
|
||||
Ver [Relations](/l/pt/developers/extend/apps/data/relations) para o padrão completo.
|
||||
|
||||
## Veja em Vinte
|
||||
|
||||
Com `yarn 20 dev` executando, abra **Settings → Data model**. Ambos os objetos
|
||||
aparecem, marcados com seu aplicativo.
|
||||
|
||||
<Frame caption="Ambos os objetos personalizados, pertencentes ao aplicativo Gerador de Documentos.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Configurações do modelo de dados mostrando documentos e modelos de documento" />
|
||||
</Frame>
|
||||
|
||||
Crie um modelo para testar com — nomeie-a *proposta de vendas*, defina **destino** para
|
||||
*Personagem*, e cole um corpo com alguns espaços reservados:
|
||||
|
||||
```text
|
||||
Dear {{name.firstName}} {{name.lastName}},
|
||||
|
||||
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
|
||||
|
||||
Best,
|
||||
The Team
|
||||
```
|
||||
|
||||
<Frame caption="Um registro de template. O corpo mantém seus espaços reservados até que um documento seja gerado.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Um registro de modelo de proposta de vendas com corpo de placeholder" />
|
||||
</Frame>
|
||||
|
||||
**Após essa etapa:** você tem objetos `documentTemplate` e `documento`, ligados por
|
||||
a relação e um modelo para gerar. Em seguida, a lógica que a preenche.
|
||||
|
||||
<Card title="Próximo: gerando documentos →" icon="bolt" href="/desenvolvedores/extend/apps/tutorials/documento-gerador/documentos">
|
||||
Escreva a função lógica que preenche o template.
|
||||
</Card>
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: 2. Gerando documentos
|
||||
icon: bolt
|
||||
description: Uma função lógica, exposta como uma ferramenta de IA e uma ação de fluxo de trabalho.
|
||||
---
|
||||
|
||||
Agora o núcleo: uma [função lógica](/l/pt/developers/extend/apps/logic/logic-functions)
|
||||
que carrega um modelo e um registro, preenche os espaços reservados e salva um novo documento
|
||||
.
|
||||
|
||||
Vamos escrever a lógica do negócio uma vez na forma de **manipulador** e então expô-la através de
|
||||
vários gatilhos. Este capítulo conecta duas delas — uma **ferramenta de IA** e uma
|
||||
**ação de fluxo de trabalho**.
|
||||
|
||||
## O auxiliar de renderização
|
||||
|
||||
Mantenha uma lógica pura em seu próprio arquivo, para que seja fácil de testar unidades. Este encolher os tokens de um record
|
||||
em `{{dot.path}}` e substitui-los.
|
||||
|
||||
```ts filename="src/logic-functions/utils/render-template.ts"
|
||||
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
|
||||
|
||||
export const renderTemplate = (body: string, values: Record<string, string>) => {
|
||||
const missingTokens = new Set<string>();
|
||||
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
|
||||
const value = values[token];
|
||||
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
|
||||
return value;
|
||||
});
|
||||
return { content, missingTokens: [...missingTokens] };
|
||||
};
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Como este arquivo não tem efeitos colaterais, você pode resolvê-lo com testes de unidade
|
||||
(`yarn test:unit`). Ver [Testing](/l/pt/developers/extend/apps/operations/testing).
|
||||
</Tip>
|
||||
|
||||
## O manipulador
|
||||
|
||||
O manipulador usa o [`CoreApiClient`](/l/pt/developers/extend/apps/logic/logic-functions)
|
||||
para ler e escrever dados de CRM. Ele carrega o modelo, carrega o registro de destino, preenche
|
||||
o corpo e cria um `documento`.
|
||||
|
||||
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
|
||||
import { renderTemplate } from 'src/logic-functions/utils/render-template';
|
||||
|
||||
export const generateDocumentHandler = async (
|
||||
input: { templateId: string; recordId: string },
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Use a filtered list query, not the singular lookup: the singular query
|
||||
// throws when nothing matches, which would become a 500 instead of a 404.
|
||||
const { documentTemplates } = await client.query({
|
||||
documentTemplates: {
|
||||
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
|
||||
edges: { node: { id: true, name: true, body: true, target: true } },
|
||||
},
|
||||
});
|
||||
const documentTemplate = documentTemplates?.edges?.[0]?.node;
|
||||
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
|
||||
|
||||
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
|
||||
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
|
||||
|
||||
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
|
||||
|
||||
const { createDocument } = await client.mutation({
|
||||
createDocument: {
|
||||
__args: { data: {
|
||||
name: `${documentTemplate.name} — ${record.displayName}`,
|
||||
content, status: 'GENERATED', templateId: documentTemplate.id,
|
||||
} },
|
||||
id: true, name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, documentId: createDocument.id, content, missingTokens };
|
||||
};
|
||||
```
|
||||
|
||||
`loadRecordValues` executa uma consulta diferente para uma Pessoa vs. Uma Empresa e flattens
|
||||
o resultado — veja
|
||||
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).
|
||||
|
||||
## Expo-na como uma ferramenta e uma ação de fluxo de trabalho
|
||||
|
||||
Uma única `defineLogicFunction` pode carregar vários gatilhos. Aqui, `toolTriggerSettings`
|
||||
faz com que seja chamável por agentes IA e `workflowActionTriggerSettings` o transforma em
|
||||
passo do construtor de fluxo de trabalho. Ambos descrevem suas informações com um esquema JSON.
|
||||
|
||||
```ts filename="src/logic-functions/generate-document.ts"
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
|
||||
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
|
||||
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'generate-document',
|
||||
description: 'Generate a document from a template and a CRM record.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: generateDocumentInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Generate Document',
|
||||
icon: 'IconFileText',
|
||||
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
|
||||
outputSchema: [{ type: 'object', properties: {
|
||||
success: { type: 'boolean' }, documentId: { type: 'string' },
|
||||
} }],
|
||||
},
|
||||
handler: generateDocumentHandler,
|
||||
});
|
||||
```
|
||||
|
||||
O esquema de entrada é um esquema JSON simples que descreve `templateId` e `recordId` —
|
||||
vê [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).
|
||||
|
||||
## Concede-o acesso
|
||||
|
||||
Funções lógicas são executadas como o papel do aplicativo. Ele precisa ler templates e registrar
|
||||
e criar documentos, então permitir que em `src/roles/default-role.ts`:
|
||||
|
||||
```ts
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Document Generator default role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canAccessAllTools: true,
|
||||
canBeAssignedToAgents: true,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
|
||||
});
|
||||
```
|
||||
|
||||
`UPLOAD_FILE` permite que a função envie o PDF gerado na próxima seção.
|
||||
Ver [Roles](/l/pt/developers/extend/apps/config/roles) para permissões refinadas.
|
||||
|
||||
## Anexar arquivo PDF real
|
||||
|
||||
Um campo de texto renderizado é útil, mas os usuários querem um documento real. Vamos gerar um
|
||||
**PDF** e armazená-lo no registro como um arquivo para download.
|
||||
|
||||
Primeiro, dê ao objeto `documento` um campo `ARQUIVO` para segurar o PDF. Aplicativos enviam
|
||||
para seus **próprios** campos de arquivos, então este campo é o que encaminha o upload:
|
||||
|
||||
```ts filename="src/objects/document.object.ts"
|
||||
{
|
||||
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.FILES,
|
||||
name: 'file',
|
||||
label: 'File',
|
||||
icon: 'IconFileTypePdf',
|
||||
universalSettings: { maxNumberOfValues: 1 },
|
||||
}
|
||||
```
|
||||
|
||||
Agora renderize esse PDF. Uma app é um projeto Node real, então você pode adicionar qualquer pacote npm
|
||||
que você precisa e importá-lo como em qualquer outro lugar. Nós usamos **[pdf-lib](https://pdf-lib.js.org/)**
|
||||
para desenhar o PDF e **[marked](https://marked.js.org/)** para analisar o corpo do Markdown
|
||||
— a CLI os instala no tempo de execução da função para você:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add pdf-lib marked
|
||||
```
|
||||
|
||||
O auxiliar completo é
|
||||
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
|
||||
Ele analisa o Markdown em tokens com `marcado. exer`, então as envia com
|
||||
pdf-lib: títulos reais, execuções **bold**/*italic*, listas de marcadores e numerados,
|
||||
bloqueios e regras — uma renderização A4 polida e multi-página do modelo
|
||||
em si, ao invés de uma parede de texto.
|
||||
|
||||
<Frame caption="O PDF: tipografia real e formatação Markdown, renderizando o corpo do modelo.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Um PDF gerado polido e comercializável" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
As fontes internas do pdf-lib usam codificação WinAnsi, portanto acentos da Europa Ocidental são renderizados
|
||||
prontos para uso; o helper mapeia aspas tipográficas e travessões e descarta caracteres que
|
||||
não consegue codificar. Renderizar scripts não-latinos (chinês, árabe, cirílico) significaria que
|
||||
incorporando uma fonte Unicode.
|
||||
</Note>
|
||||
|
||||
Em seguida, carregue-a e armazene a referência no registro. Rotas `uploadFile` bytes
|
||||
para o campo de arquivos de propriedade; o `id` retornado é o que você salva:
|
||||
|
||||
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
|
||||
|
||||
const documentName = `${documentTemplate.name} — ${record.displayName}`;
|
||||
const bytes = await generateDocumentPdf(documentName, content);
|
||||
const fileName = 'proposal.pdf';
|
||||
|
||||
const uploaded = await new MetadataApiClient().uploadFile(
|
||||
Buffer.from(bytes),
|
||||
fileName,
|
||||
'application/pdf',
|
||||
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
await client.mutation({
|
||||
updateDocument: {
|
||||
__args: {
|
||||
id: documentId,
|
||||
data: { file: [{ fileId: uploaded.id, label: fileName }] },
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
O documento gerado agora carrega um PDF:
|
||||
|
||||
<Frame caption="O PDF gerado, armazenado no campo Arquivo do documento.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Um registro de documento com um arquivo PDF gerado" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
`uploadFile` destina-se apenas a campos de arquivos **propriedade do aplicativo** (então o upload sempre requer um aplicativo
|
||||
que possui o campo, mais o sinalizador de papéis `UPLOAD_FILE`). É por isso que o PDF
|
||||
fica no campo `file` do próprio registro - o mesmo padrão que o
|
||||
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
|
||||
usa para gravações.
|
||||
</Note>
|
||||
|
||||
**Após este passo:** cada documento gerado possui um PDF real e para download. Mas
|
||||
nada pode *chamar* o gerador a partir da interface de usuário — por isso precisamos de uma rota HTTP.
|
||||
|
||||
<Card title="Próximo: Rotas HTTP →" icon="globo" href="/desenvolvedores/extend/apps/tutorials/document-gerator/http-routes">
|
||||
Servir a função sobre HTTP e renderizar documentos como páginas da web.
|
||||
</Card>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: 3. Rotas HTTP
|
||||
icon: globe
|
||||
description: Acionar a função sobre HTTP e renderizar documentos como páginas da web.
|
||||
---
|
||||
|
||||
O mesmo manipulador também pode responder solicitações HTTP. Vamos adicionar duas rotas:
|
||||
|
||||
* um terminal **POST** aponta as chamadas da UI para gerar um documento e
|
||||
* um endpoint de **GET** público que renderiza um documento como uma página web impressa.
|
||||
|
||||
Ambos usam `httpRouteTriggerSettings`. As rotas de aplicativos são servidas em `/s` no seu servidor
|
||||
Vinte (por exemplo, `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
## Rota POST - gerar sob demanda
|
||||
|
||||
Isso reusa `generateDocumentHandler`, então não há lógica para repetir — apenas um adaptador
|
||||
fino que lê o corpo da solicitação.
|
||||
|
||||
```ts filename="src/logic-functions/generate-document-route.ts"
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
|
||||
|
||||
const handler = async (event: RoutePayload): Promise<Response> => {
|
||||
const body = event.body as Record<string, unknown> | null;
|
||||
|
||||
const result = await generateDocumentHandler({
|
||||
templateId: (body?.templateId as string) ?? '',
|
||||
recordId: (body?.recordId as string) ?? '',
|
||||
});
|
||||
|
||||
// Map the handler's failure reason onto a real HTTP status (400/404/500)
|
||||
// instead of always returning 200.
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: result.success ? 200 : (result.status ?? 400),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'generate-document-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/documents/generate',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
O manipulador compartilhado retorna um 'status' sugerido durante a falha, então a rota pode
|
||||
responder com um código `4xx`/`5xx` apropriado. `isAuthRequired: true` significa que o chamador
|
||||
deve apresentar um token válido — o componente frontal no próximo capítulo passa o token de acesso do usuário
|
||||
automaticamente.
|
||||
|
||||
## Via GET - renderizar como uma página da web
|
||||
|
||||
Para retornar HTML em vez de JSON, encapsule o corpo em um `Response` com um cabeçalho
|
||||
`Content-Type`. Esta rota é pública (`isAuthRequired: false`) para que um documento
|
||||
gerado possa ser compartilhado como um link.
|
||||
|
||||
```ts filename="src/logic-functions/view-document.ts"
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { documentHtmlPage } from 'src/utils/render-document';
|
||||
|
||||
const htmlResponse = (html: string, status = 200): Response =>
|
||||
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||
|
||||
const handler = async (event: RoutePayload): Promise<Response> => {
|
||||
const documentId = event.queryStringParameters?.id;
|
||||
|
||||
if (!documentId) {
|
||||
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
|
||||
}
|
||||
|
||||
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
|
||||
const { documents } = await new CoreApiClient().query({
|
||||
documents: {
|
||||
__args: { filter: { id: { eq: documentId } }, first: 1 },
|
||||
edges: { node: { id: true, name: true, content: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const document = documents?.edges?.[0]?.node;
|
||||
if (!document?.id) {
|
||||
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
|
||||
}
|
||||
|
||||
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'view-document',
|
||||
timeoutSeconds: 15,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/documents/view',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`documentHtmlPage` renderiza o corpo do Markdown para HTML (com [marked](https://marked.js.org/),
|
||||
sanitizado) e o coloca em um limpo, página imprimível que mostra apenas o conteúdo do modelo- a mesma aparência que o PDF e a visualização no aplicativo.
|
||||
[Veja o ajudante](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
|
||||
|
||||
## Experimente isso
|
||||
|
||||
Com um modelo e uma pessoa em seu espaço de trabalho, chame a rota (pegue um token do
|
||||
**Settings → APIs & Webhooks**):
|
||||
|
||||
```bash filename="Terminal"
|
||||
curl -X POST http://localhost:2020/s/documents/generate \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
|
||||
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
|
||||
```
|
||||
|
||||
Abra o documento retornado no seu navegador:
|
||||
|
||||
```
|
||||
http://localhost:2020/s/documents/view?id=<documentId>
|
||||
```
|
||||
|
||||
<Frame caption="A rota GET pública renderiza o documento como uma página impressa.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Uma página renderizada do documento web" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
Você também pode transmitir logs de uma função enquanto está testando com
|
||||
`yarn twenty dev:function:logs`, ou invocá-los diretamente com
|
||||
`yarn twenty dev:function:exec`.
|
||||
</Tip>
|
||||
|
||||
**Após esse passo:** o aplicativo pode gerar documentos via HTTP e servi-los como páginas
|
||||
web. Agora vamos torná-lo utilizável sem `curl`.
|
||||
|
||||
<Card title="Próximo: Construindo a interface do usuário →" icon="table-columns" href="/desenvolvedores/extend/apps/tutorials/document-generator/building-the-ui">
|
||||
Exibir, navegação, um comando e um componente inicial.
|
||||
</Card>
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: "Tutorial: Gerador de Documentos"
|
||||
icon: wand-magic-sparkles
|
||||
description: Crie um aplicativo Twenty real que gera documentos personalizados a partir dos seus dados de CRM.
|
||||
---
|
||||
|
||||
Neste tutorial, você vai criar o **Gerador de Documentos** — um aplicativo que transforma modelos reutilizáveis
|
||||
em documentos personalizados usando os dados que você já tem no seu CRM.
|
||||
|
||||
Escreva um modelo uma vez com `{{placeholders}}` e, em seguida, gere um documento preenchido
|
||||
para qualquer Pessoa ou Empresa com um clique — a partir do menu de comando, de um
|
||||
agente de IA ou de um workflow.
|
||||
|
||||
<Frame caption="Um modelo, gerado para uma pessoa específica, aberto como uma página para impressão.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Um documento de proposta de vendas gerado" />
|
||||
</Frame>
|
||||
|
||||
## O que você vai aprender
|
||||
|
||||
Cada capítulo adiciona uma capacidade. Ao final, você terá usado a maior parte do SDK.
|
||||
|
||||
| Capítulo | Capacidade | Referência |
|
||||
| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| [1. Modelo de Dados](/l/pt/developers/extend/apps/tutorials/document-generator/data-model) | Objetos, campos e uma relação | [Dados](/l/pt/developers/extend/apps/data/overview) |
|
||||
| [2. Geração de documentos](/l/pt/developers/extend/apps/tutorials/document-generator/generating-documents) | Uma função lógica (ferramenta de IA + ação de workflow) que preenche um modelo Markdown e anexa um PDF finalizado | [Funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) |
|
||||
| [3. Rotas HTTP](/l/pt/developers/extend/apps/tutorials/document-generator/http-routes) | Servindo JSON e uma página HTML compartilhável a partir de rotas | [Funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) |
|
||||
| [4. Criando a UI](/l/pt/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vistas, navegação, menu de comandos e componentes de front-end que preveem um documento e editam um modelo | [Layout](/l/pt/developers/extend/apps/layout/overview) |
|
||||
| [5. Um agente de IA](/l/pt/developers/extend/apps/tutorials/document-generator/ai-agent) | Agente + habilidade | [Habilidades e Agentes](/l/pt/developers/extend/apps/logic/skills-and-agents) |
|
||||
| [6. Publicação](/l/pt/developers/extend/apps/tutorials/document-generator/publishing) | Envie-o para o marketplace | [Publicação](/l/pt/developers/extend/apps/operations/publishing) |
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
Você deve ter concluído o [Quick Start](/l/pt/developers/extend/apps/getting-started/quick-start):
|
||||
um servidor Twenty local em execução na porta `2020` e o CLI autenticado nele.
|
||||
|
||||
Se não, gere a estrutura e inicie um agora:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest document-generator
|
||||
cd document-generator
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
<Note>
|
||||
Prefere ler o código finalizado? O aplicativo completo está em
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
Cada trecho abaixo foi copiado de lá.
|
||||
</Note>
|
||||
|
||||
## Como o aplicativo se encaixa
|
||||
|
||||
<Frame>
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Um modelo com placeholders é transformado em um documento finalizado com um PDF, acionado a partir do menu de comandos, de um agente de IA, de um workflow ou de um link compartilhável" />
|
||||
</Frame>
|
||||
|
||||
Você escreve um **modelo** uma vez em um editor de rich text, com `{{placeholders}}`. Escolher um
|
||||
modelo e um registro de CRM preenche os placeholders e armazena um
|
||||
**documento** finalizado (com um arquivo PDF). Todo o resto — o menu de comandos, o agente de IA,
|
||||
o passo de workflow, o link compartilhável — é apenas uma forma diferente de acionar aquele
|
||||
único gerador.
|
||||
|
||||
## Mantenha este ciclo em execução
|
||||
|
||||
Deixe `yarn twenty dev` rodando em um terminal durante todo o tutorial. Toda vez que
|
||||
você adicionar ou editar um arquivo em `src/`, ele será ressíncronizado com o seu servidor em alguns
|
||||
segundos, para que você possa ver cada capacidade aparecer na UI conforme a constrói.
|
||||
|
||||
<Card title="Comece a construir →" icon="database" href="/l/pt/developers/extend/apps/tutorials/document-generator/data-model">
|
||||
Capítulo 1: modele documentos e modelos.
|
||||
</Card>
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: 6. Publicação
|
||||
icon: rocket
|
||||
description: Adicione metadados da loja e publique seu aplicativo.
|
||||
---
|
||||
|
||||
Seu aplicativo funciona. O último passo é descrevê-lo para o mercado e publicar.
|
||||
|
||||
## Adicionar metadados de mercado
|
||||
|
||||
A [aplicação config](/l/pt/developers/extend/apps/config/application) transporta a identidade
|
||||
que aparece no mercado: autor, categoria, logotipo e suporte links
|
||||
. Coloque um logotipo em `public/` e referencie-o com `logoUrl`.
|
||||
|
||||
```ts filename="src/application-config.ts"
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Document Generator',
|
||||
description:
|
||||
'Create reusable document templates and generate personalized documents from your CRM records.',
|
||||
logoUrl: 'public/document-generator.svg',
|
||||
author: 'Twenty',
|
||||
category: 'Productivity',
|
||||
websiteUrl: 'https://docs.twenty.com/l/pt/developers/extend/apps',
|
||||
termsUrl: 'https://www.twenty.com/terms',
|
||||
emailSupport: 'contact@twenty.com',
|
||||
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
A função padrão é declarada com `defineApplicationRole()` em seu próprio arquivo — você
|
||||
não passa mais `defaultRoleUniversalIdentifier` aqui.
|
||||
</Tip>
|
||||
|
||||
Também adicione a palavra-chave `vinte app` ao `package.json` para que o aplicativo seja detectável:
|
||||
|
||||
```json filename="package.json"
|
||||
{ "keywords": ["twenty-app"] }
|
||||
```
|
||||
|
||||
## Adicionar capturas de tela da galeria
|
||||
|
||||
Uma lista de mercado vende a si mesma com screenshots. Colocar alguns PNGs em
|
||||
`public/gallery/` e referenciá-los com `screenshots` — eles renderizam como galeria
|
||||
na página de listagem.
|
||||
|
||||
```ts filename="src/application-config.ts"
|
||||
export default defineApplication({
|
||||
// ...identity from above
|
||||
screenshots: [
|
||||
'public/gallery/01-generated-document.png',
|
||||
'public/gallery/02-command-menu.png',
|
||||
'public/gallery/03-template-editor.png',
|
||||
'public/gallery/04-documents.png',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Conduzir com o pagamento: fazer a primeira captura de tela o resultado finalizado (um documento
|
||||
gerado), depois mostrar como é acionado e autorado. Use capturas
|
||||
crocantes e de alta resolução — elas são a primeira coisa que um usuário vê.
|
||||
</Tip>
|
||||
|
||||
Dê ao `README.md` o mesmo tratamento — é a página inicial em npm e GitHub.
|
||||
Abra com a proposição de valor e uma captura de tela, liste os recursos do título,
|
||||
e então mantenha os detalhes de compilação abaixo da dobra.
|
||||
|
||||
## Verifique antes de enviar
|
||||
|
||||
Executar os mesmos portões CI do portão:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
```
|
||||
|
||||
A corrida seca imprime exatamente o que mudaria no servidor sem aplicá-lo —
|
||||
uma boa verificação de sanidade final. Veja
|
||||
[Testing](/l/pt/developers/extend/apps/operations/testing) e
|
||||
[Sincronização e recuperação](/l/pt/developers/extend/apps/operations/sync-and-recovery).
|
||||
|
||||
## Publicar
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Public app → npm (default)
|
||||
yarn twenty app:publish
|
||||
|
||||
# Or deploy privately to a specific server's registry
|
||||
yarn twenty app:publish --private -r <remote>
|
||||
```
|
||||
|
||||
`app:publish` compila e publica para o npm por padrão; `--private` envia uma tarball
|
||||
para um registro privado de vinte servidores. Para superfíciar um aplicativo publicado
|
||||
no mercado de uma instância, acione uma sincronização de catálogo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync -r <remote>
|
||||
```
|
||||
|
||||
Detalhes completos e o checklist de lançamento:
|
||||
[Publishing](/l/pt/developers/extend/apps/operations/publishing).
|
||||
|
||||
## Você construiu um aplicativo 🎉
|
||||
|
||||
Em seis capítulos você usou a maior parte da superfície SDK:
|
||||
|
||||
* **Objetos, campos e uma relação** para modelar os dados
|
||||
* Uma **função lógica** exposta como uma **ferramenta de Inteligência**, uma **ação de fluxo de trabalho** e **rotas HTTP**
|
||||
* **Exibir, navegação, um comando e um componente frontal** para a interface do usuário
|
||||
* Um **agente + habilidade** para a geração de idioma natural
|
||||
* **Metadados do Marketplace** e o fluxo de publicação
|
||||
|
||||
O aplicativo finalizado está em
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
|
||||
## Para onde ir
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Referência de dados" icon="database" href="/l/pt/developers/extend/apps/data/overview">
|
||||
Todos os tipos de campo, relação e opção de índice.
|
||||
</Card>
|
||||
<Card title="Referência lógica" icon="bolt" href="/l/pt/developers/extend/apps/logic/overview">
|
||||
Gatilhos Cron e database-event trigers, a loja chave-valor, conexões OAuth
|
||||
</Card>
|
||||
<Card title="Referência do layout" icon="table-columns" href="/l/pt/developers/extend/apps/layout/overview">
|
||||
Layouts da página, widgets do painel e mais superfícies da UI.
|
||||
</Card>
|
||||
<Card title="Operações" icon="rocket" href="/l/pt/developers/extend/apps/operations/overview">
|
||||
CLI, testes, controles remotos e CI.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user