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: Deja que un agente genere documentos de un chat, usando tu herramienta.
|
||||
---
|
||||
|
||||
Debido a que `generate-document` está expuesto como una **herramienta**, un agente de IA puede llamarlo.
|
||||
Añadamos un agente y una habilidad para que los usuarios puedan decir *"generar una propuesta para
|
||||
Grifo de Jeffery"*.
|
||||
|
||||
## La habilidad
|
||||
|
||||
Una [skill](/l/es/developers/extend/apps/logic/skills-and-agents) es reutilizable
|
||||
instrucciones: conocimiento que adjuntas a los agentes. La nuestra enseña al modelo cómo usar
|
||||
la herramienta.
|
||||
|
||||
```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'),
|
||||
});
|
||||
```
|
||||
|
||||
## El agente
|
||||
|
||||
Un [agent](/l/es/developers/extend/apps/logic/skills-and-agents) empareja un símbolo de espera de órdenes con un modelo
|
||||
. Establece `responseFormat` explícitamente para evitar una advertencia de compilación.
|
||||
|
||||
```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>
|
||||
El agente sólo puede llamar a la herramienta si su papel lo permite. Ya establecemos
|
||||
`canAccessAllTools: true` y `canBeAssignedToAgents: true` en el rol de la aplicación en
|
||||
[Capítulo 2](/l/es/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
|
||||
</Note>
|
||||
|
||||
## Pruébalo
|
||||
|
||||
Abre un chat con **Asistente de documentos** y pídele que elabore un documento para una personaformat@@0
|
||||
en tu CRM. Encuentra el registro, llama a `generate-document` e informa
|
||||
el documento que creó, el cual ahora aparece en tu vista de **Documents**,
|
||||
exactamente igual que las rutas del menú de comandos y del flujo de trabajo.
|
||||
|
||||
Esa es la ganancia de exponer la lógica como una herramienta: **una función, muchas puertas delanteras** —
|
||||
menú de comandos, HTTP, paso de flujo de trabajo y ahora lenguaje natural.
|
||||
|
||||
**Después de este paso:** la aplicación es completa y genuinamente útil. Es hora de
|
||||
enviarlo.
|
||||
|
||||
<Card title="Siguiente: publicando →" icon="cohete" href="/Developopers/extend/apps/tutorials/document-generator/publishing">
|
||||
Añadir metadatos de mercado y publicar.
|
||||
</Card>
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
---
|
||||
title: 4. Construyendo la interfaz de usuario
|
||||
icon: table-columns
|
||||
description: Visualizaciones, navegación en la barra lateral, un comando y componentes frontales.
|
||||
---
|
||||
|
||||
Ahora mismo los objetos sólo son accesibles a través de Configuración. Vamos a dar a la aplicación una presencia
|
||||
real en la interfaz de usuario: vistas de lista, entradas de la barra lateral, un comando de
|
||||
**Generar documento**, un componente frontal de página de registro para **previsualizar** un documento
|
||||
y una pestaña de **editor** de texto nativo para plantillas.
|
||||
|
||||
## Vistas y navegación
|
||||
|
||||
Una [view](/l/es/developers/extend/apps/layout/views) es una lista guardada de un objeto dado.
|
||||
Un [elemento del menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items)
|
||||
pone esa vista en la 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,
|
||||
});
|
||||
```
|
||||
|
||||
Añadir el mismo par para las plantillas. Ambos ahora se muestran en la barra lateral:
|
||||
|
||||
<Frame caption="Documentos y plantillas en la barra lateral con el documento generado listado.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/04-documents-view.png" alt="Vista de documentos con un documento generado" />
|
||||
</Frame>
|
||||
|
||||
## Un componente frontal
|
||||
|
||||
Un [componente delantero](/l/es/developers/extend/apps/layout/front-components) es un componente de React
|
||||
en el interior de Twenty. Nuestra nuestra lee el registro seleccionado, carga las plantillas
|
||||
persona a través de `CoreApiClient`, y POSTs a la ruta desde el ú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 con variables CSS en línea (`var(--t-color-blue)`), no valores importados de
|
||||
`twenty-ui`. El SDK simula ese paquete durante la compilación, por lo que las importaciones a nivel de módulo de
|
||||
constantes de tema serían `undefined`. Ver el
|
||||
[componente completo](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
|
||||
</Warning>
|
||||
|
||||
## Un comando para abrirlo
|
||||
|
||||
Un [elemento del menú de comandos](/l/es/developers/extend/apps/layout/command-menu-items) con
|
||||
`availabilityType: 'RECORD_SELECTION'` aparece cuando se selecciona una Persona, y
|
||||
abre el componente en el panel 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,
|
||||
});
|
||||
```
|
||||
|
||||
## Prueba todo el flujo
|
||||
|
||||
Abre **Personas**, marca a una persona y pulsa <kbd>mañK</kbd> / <kbd>Ctrl K</kbd>.
|
||||
"Generar documento" aparece, etiquetado con tu aplicación:
|
||||
|
||||
<Frame caption="El comando se muestra cuando se selecciona una Persona.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/06-command-menu.png" alt="Menú de comandos con Generar documento" />
|
||||
</Frame>
|
||||
|
||||
Ejecutarlo — su componente se abre en el panel lateral. Elige una plantilla, haz clic en
|
||||
**Generar**, y un nuevo registro de tierras en **Documentos**.
|
||||
|
||||
<Frame caption="El componente frontal, cargando plantillas y generando al hacer clic.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/06b-front-component.png" alt="Generar panel lateral del documento" />
|
||||
</Frame>
|
||||
|
||||
Cada documento generado registra tu aplicación como su autor:
|
||||
|
||||
<Frame caption="Creado por Document Generator, estado Generado.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/05-document-record.png" alt="Un registro de documento generado" />
|
||||
</Frame>
|
||||
|
||||
## Vista previa de un documento en su página de registro
|
||||
|
||||
Un componente frontal no solo para los menús de comandos: puedes montar uno como una \*\*pestaña en una página de registro
|
||||
. Añadamos una pestaña de *Vista previa* al registro de documentos que renderiza el cuerpo de Markdown
|
||||
como una página pulida e imprimible.
|
||||
|
||||
El componente lee el id de registro actual de su contexto de ejecución, carga el documento
|
||||
y lo renderiza. Los componentes de Front se ejecutan en una **sandbox** que solo permite una
|
||||
lista blanca de etiquetas HTML: la inyección de HTML sin procesar (`dangerouslySetInnerHTML`) y
|
||||
`\<style>` están bloqueados, por lo que representamos el Markdown como elementos de React con estilos
|
||||
inline mediante un pequeño helper [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx).
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Montarlo con un [diseño de página](/l/es/developers/extend/apps/layout/page-layouts). Un diseño
|
||||
`RECORD_PAGE` añade pestañas a la vista de registro de un objeto; un widget `FRONT_COMPONENT`
|
||||
en una pestaña `CANVAS` aloja el 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,
|
||||
},
|
||||
}],
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
Abre cualquier documento — una pestaña de **Vista previa** lo renderiza hermosamente, con enlaces a la página web compartible
|
||||
y el PDF:
|
||||
|
||||
<Frame caption="La pestaña Vista previa muestra el documento con estilos en línea, además de enlaces rápidos.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/09-document-viewer.png" alt="Componente frontal del visor de documentos en una pestaña de página de registro" />
|
||||
</Frame>
|
||||
|
||||
## Editar una plantilla con el editor de texto
|
||||
|
||||
Las plantillas no necesitan ningún componente personalizado. Porque el `body` es un campo
|
||||
`RICH_TEXT`, Veinte ya proporciona un editor de texto completo para él — el mismo
|
||||
que los objetos estándar de Nota y Task usan. Acabamos de superarlo en la página de registro
|
||||
plantilla.
|
||||
|
||||
Añade una pestaña con un widget `FIELD` en el modo de visualización `EDITOR`, apuntando al campo `body`
|
||||
a travé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',
|
||||
},
|
||||
}],
|
||||
}
|
||||
```
|
||||
|
||||
Un campo `RICH_TEXT` almacena tanto el bloque JSON del editor como una proyección
|
||||
de Markdown. La generación de pipeline lee que Markdown proyecta, así que
|
||||
marcadores de posición, el PDF, y la página web compartible siguen funcionando sin cambios —
|
||||
vea el
|
||||
completo [`template-record. age-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
|
||||
Ahora editores escriben plantillas en un editor de texto rich:
|
||||
|
||||
<Frame caption="La pestaña Plantilla: Editor nativo de texto de texto de 20 años vinculado al campo del cuerpo.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/10-template-editor.png" alt="Registro de plantillas con la pestaña nativa del editor de texto" />
|
||||
</Frame>
|
||||
|
||||
**Después de este paso:** vista previa de documentos hermosamente y plantillas son editables
|
||||
en la aplicación. A continuación, deja que un agente de IA los genere a partir de un chat.
|
||||
|
||||
<Card title="Siguiente: un agente de IA →" icon="robot" href="/Developopers/extend/apps/tutorials/document-generator/ai-agent">
|
||||
Añade un agente y una habilidad que llame a tu herramienta.
|
||||
</Card>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: 1. Modelo de datos
|
||||
icon: database
|
||||
description: Modelo de documentos y plantillas con objetos, campos y una relación.
|
||||
---
|
||||
|
||||
Nuestra aplicación necesita dos objetos personalizados: **plantillas de documentos** (qué escribir) y
|
||||
**documentos** (el resultado generado). Vamos a definirlos.
|
||||
|
||||
Escaffold cada archivo de entidad con la CLI — genera un UUID válido y la carpeta
|
||||
correcta para ti:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
```
|
||||
|
||||
A continuación mostramos los archivos terminados.
|
||||
|
||||
<Note>
|
||||
Cada `*_UNIVERSAL_IDENTIFIER` vida constante en
|
||||
`src/constants/universal-identifiers.ts` y es importado donde se utiliza. Los fragmentos
|
||||
a continuación omiten las importaciones por brevedad — manténgalos en tus propios archivos.
|
||||
</Note>
|
||||
|
||||
## El objeto de plantilla
|
||||
|
||||
Una plantilla tiene un `nombre`, un `cuerpo` con `{{placeholders}}`, y un `target` que
|
||||
dice si está escrito para una persona o una empresa. El `body` es un campo
|
||||
`RICH_TEXT`, así que Veinte le da un editor completo de texto.
|
||||
|
||||
```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` option **values** debe ser `UPPER_CASE` (`PERSON`, no `person`), y el
|
||||
`defaultValue` está envuelto en comillas adicionales: `` `'PERSON'` ``. La `label` es lo que
|
||||
los usuarios ven.
|
||||
</Warning>
|
||||
|
||||
## El objeto del documento
|
||||
|
||||
El documento generado almacena el `contenido` renderizado y un `status`. Definirlo
|
||||
de la misma manera, con un `status` seleccionado de `DRAFT` / `GENERATED`. Archivo completo:
|
||||
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
|
||||
|
||||
## Vinculándolas con una relación
|
||||
|
||||
Cada documento debe apuntar de nuevo a la plantilla de la que proviene. Las relaciones son
|
||||
**bidireccionales** — defines ambos lados, cada uno en su propio archivo 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',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
El otro lado (`template-documents-relation.field.ts`) es un campo
|
||||
`RelationType.ONE_TO_MANY` llamado `documents` que apunta a la manera opuesta.
|
||||
Ver [Relations](/l/es/developers/extend/apps/data/relations) para ver el patrón completo.
|
||||
|
||||
## Ver en Veinte
|
||||
|
||||
Con `yarn veinte dev` en ejecución, abre **Ajustes → Modelo de datos**. Ambos objetos
|
||||
aparecen, etiquetados con tu aplicación.
|
||||
|
||||
<Frame caption="Ambos objetos personalizados, propiedad del generador de documentos.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/01-data-model.png" alt="Configuración del modelo de datos mostrando plantillas de documentos y documentos" />
|
||||
</Frame>
|
||||
|
||||
Crea una plantilla para probar con — nombra la *propuesta de ventas*, establece el **objetivo** a
|
||||
*Persona*, y pega un cuerpo con unos pocos marcadores de posición:
|
||||
|
||||
```text
|
||||
Dear {{name.firstName}} {{name.lastName}},
|
||||
|
||||
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
|
||||
|
||||
Best,
|
||||
The Team
|
||||
```
|
||||
|
||||
<Frame caption="Un registro de plantilla. El cuerpo mantiene sus marcadores de posición hasta que se genere un documento.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/03-template-record.png" alt="Un registro de plantilla de propuesta de ventas con el cuerpo del marcador de posición" />
|
||||
</Frame>
|
||||
|
||||
**Después de este paso:** tienes los objetos `documentTemplate` y `document`, enlazados por
|
||||
una relación, y una plantilla para generar. Después, la lógica que lo llena.
|
||||
|
||||
<Card title="Siguiente: generando documentos →" icon="bolt" href="/developopers/extend/apps/tutorials/document-generator/generating-documents">
|
||||
Escriba la función lógica que llena la plantilla.
|
||||
</Card>
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: 2. Generando documentos
|
||||
icon: bolt
|
||||
description: Una función lógica, expuesta como una herramienta de IA y una acción de flujo de trabajo.
|
||||
---
|
||||
|
||||
Ahora el núcleo: una [función lógica](/l/es/developers/extend/apps/logic/logic-functions)
|
||||
que carga una plantilla y un registro, rellena los marcadores de posición y guarda un nuevo documento
|
||||
.
|
||||
|
||||
Escribiremos la lógica de negocio una sola vez como un **handler**, y luego la expondremos mediante
|
||||
diversos desencadenadores. Este capítulo conecta dos de ellos: una **herramienta de IA** y una
|
||||
**acción de flujo de trabajo**.
|
||||
|
||||
## El ayudante de renderizado
|
||||
|
||||
Mantenga la lógica pura en su propio archivo para que sea fácil de unir-test. Esto arrastra un registro
|
||||
en `{{dot.path}}` tokens y los sustituye.
|
||||
|
||||
```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>
|
||||
Debido a que este archivo no tiene efectos secundarios, puedes cubrirlo con pruebas unitarias rápidas
|
||||
(`yarn test:unit`). Ver [Testing](/l/es/developers/extend/apps/operations/testing).
|
||||
</Tip>
|
||||
|
||||
## El manejador
|
||||
|
||||
El manejador utiliza el [`CoreApiClient`](/l/es/developers/extend/apps/logic/logic-functions)
|
||||
generado para leer y escribir datos CRM. Carga la plantilla, carga el registro de destino, rellena
|
||||
el cuerpo y crea un `document`.
|
||||
|
||||
```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` ejecuta una consulta diferente para una Persona vs. una Empresa y aplana
|
||||
el resultado — vea
|
||||
[`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).
|
||||
|
||||
## Exponerlo como una herramienta y una acción de flujo de trabajo
|
||||
|
||||
Un solo `defineLogicFunction` puede llevar varios disparadores. Aquí, `toolTriggerSettings`
|
||||
hace que sea llamable por agentes IA, y `workflowActionTriggerSettings` lo convierte en un paso
|
||||
en el constructor de flujo de trabajo visual. Ambos describen su entrada con un 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,
|
||||
});
|
||||
```
|
||||
|
||||
El esquema de entrada es un esquema JSON simple que describe `templateId` y `recordId` —
|
||||
see [`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).
|
||||
|
||||
## Conceder acceso
|
||||
|
||||
Funciones lógicas ejecutadas como el rol de la aplicación. Necesita leer plantillas y registrar
|
||||
y crear documentos, así que permita eso en `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 a la función cargar el PDF generado en la siguiente sección.
|
||||
Ver [Roles](/l/es/developers/extend/apps/config/roles) para obtener permisos más finos.
|
||||
|
||||
## Adjuntar un archivo PDF real
|
||||
|
||||
Un campo de texto renderizado es útil, pero los usuarios quieren un documento real. Vamos a generar un
|
||||
**PDF** y almacenarlo en el registro como un archivo descargable.
|
||||
|
||||
Primero, da al objeto `document` un campo `ARCHIVOS` para mantener el PDF. Las aplicaciones suben
|
||||
a sus campos de archivos **propios**, así que este campo es qué rutas cargar:
|
||||
|
||||
```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 },
|
||||
}
|
||||
```
|
||||
|
||||
Ahora renderice ese PDF. Una aplicación es un proyecto de nodo real, así que puedes añadir cualquier paquete npm
|
||||
que necesites e importarlo como en cualquier otro lugar. Utilizamos **[pdf-lib](https://pdf-lib.js.org/)**
|
||||
para dibujar el PDF y **[marked](https://marked.js.org/)** para analizar el cuerpo de Markdown
|
||||
— el CLI los instala en el tiempo de ejecución de la función para ti:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add pdf-lib marked
|
||||
```
|
||||
|
||||
El ayudante completo es
|
||||
[`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).
|
||||
Analiza el Markdown en tokens con `marked.lexer`, y luego los dispone con
|
||||
pdf-lib: encabezados reales, texto en **negrita**/*cursiva*, listas con viñetas y numeradas,
|
||||
citas en bloque y líneas de separación: una representación A4 pulida y de varias páginas de la propia plantilla,
|
||||
en lugar de un bloque de texto.
|
||||
|
||||
<Frame caption="El PDF generado: tipografía real y formato Markdown, renderizando el cuerpo de la plantilla.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/07b-generated-pdf.png" alt="Un PDF pulido y generado por marcadores" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
Las fuentes incorporadas de pdf-lib's usan la codificación WinAnsi, por lo que los acentos de WesternEuropean renderizan
|
||||
fuera de la caja el ayudante mapea comillas inteligentes y guiones y deja caer caracteres que
|
||||
no puede codificar. Renderizar escrituras no latinas (chino, árabe, cirílico) implicaría
|
||||
incrustar una fuente Unicode.
|
||||
</Note>
|
||||
|
||||
A continuación, suba y almacene la referencia en el registro. `uploadFile` rutas bytes
|
||||
a tu campo de archivos propiedad de la aplicación; el `id` devuelto es lo que guardas:
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
El documento generado ahora contiene un PDF descargable:
|
||||
|
||||
<Frame caption="El PDF generado, almacenado en el campo Archivo del documento.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/08-document-with-pdf.png" alt="Un registro de documento con un archivo PDF generado" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
`uploadFile` solo se dirige a campos de archivos **propiedad de la app** (por lo que las cargas siempre requieren una
|
||||
app que sea propietaria del campo, además del indicador de rol `UPLOAD_FILE`). Por eso el PDF
|
||||
aterriza en el propio campo `file` del registro — el mismo patrón que el
|
||||
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
|
||||
usa para grabaciones.
|
||||
</Note>
|
||||
|
||||
**Después de este paso:** cada documento generado tiene un PDF real y descargable. Pero
|
||||
nada puede *llamar* al generador de la interfaz de usuario aún — para eso necesitamos una ruta HTTP.
|
||||
|
||||
<Card title="Siguiente: rutas HTTP →" icon="globo" href="/Developopers/extend/apps/tutorials/document-generator/http-routes">
|
||||
Servir la función sobre HTTP y representar documentos como páginas web.
|
||||
</Card>
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: 3. Rutas HTTP
|
||||
icon: globe
|
||||
description: Activa la función sobre HTTP y renderiza documentos como páginas web.
|
||||
---
|
||||
|
||||
El mismo manejador también puede responder a peticiones HTTP. Añadiremos dos rutas:
|
||||
|
||||
* un endpoint **POST** para generar un documento, y
|
||||
* un endpoint público **GET** que renderiza un documento como una página web imprimible.
|
||||
|
||||
Ambos usan `httpRouteTriggerSettings`. Las rutas de la aplicación se sirven bajo `/s` en tu servidor
|
||||
Veenty (por ejemplo, `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
## Ruta POST — generar bajo demanda
|
||||
|
||||
Esto reutiliza `generateDocumentHandler`, así que no hay lógica para repetir: solo un adaptador
|
||||
fino que lee el cuerpo de la solicitud.
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
El manejador compartido devuelve un `status` sugerido en caso de fallo, así que la ruta puede
|
||||
responder con un código apropiado `4xx`/`5xx`. `isAuthRequired: true` significa que la persona que llama
|
||||
debe presentar un token válido — el componente frontal en el siguiente capítulo pasa el token de acceso del usuario
|
||||
automáticamente.
|
||||
|
||||
## Ruta GET — renderizar como una página web
|
||||
|
||||
Para devolver HTML en lugar de JSON, envuelve el cuerpo en un encabezado `Response` con una cabecera
|
||||
`Content-Type`. Esta ruta es pública (`isAuthRequired: false`) por lo que un documento generado
|
||||
puede ser compartido como un enlace.
|
||||
|
||||
```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 el cuerpo de Markdown a HTML (con [marked](https://marked.js.org/),
|
||||
saneado) y lo deja caer en una limpieza, página imprimible que muestra sólo el contenido
|
||||
plantilla — el mismo aspecto que el PDF y la vista previa dentro de la aplicación.
|
||||
[Ver el ayudante](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
|
||||
|
||||
## Pruébalo
|
||||
|
||||
Con una plantilla y una persona en tu espacio de trabajo, llama a la ruta (toma un token desde
|
||||
**Ajustes → 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 el documento devuelto en su navegador:
|
||||
|
||||
```
|
||||
http://localhost:2020/s/documents/view?id=<documentId>
|
||||
```
|
||||
|
||||
<Frame caption="La ruta pública GET renderiza el documento como una página imprimible.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/07-rendered-document.png" alt="Una página web de documentos procesados" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
También puedes transmitir los registros de una función mientras pruebas con
|
||||
`yarn twenty dev:function:logs`, o invocarlo directamente con
|
||||
`yarn twenty dev:function:exec`.
|
||||
</Tip>
|
||||
|
||||
**Después de este paso:** la aplicación puede generar documentos a través de HTTP y servirlos como
|
||||
páginas web. Ahora hagámoslo utilizable sin `curl`.
|
||||
|
||||
<Card title="Siguiente: construyendo la interfaz de usuario →" icon="table-columns" href="/Developopers/extend/apps/tutorials/document-generator/building-the-ui">
|
||||
Vistas, navegación, un comando y un componente frontal.
|
||||
</Card>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "Tutorial: Generador de documentos"
|
||||
icon: wand-magic-sparkles
|
||||
description: Crea una aplicación real de Twenty que genere documentos personalizados a partir de los datos de tu CRM.
|
||||
---
|
||||
|
||||
En este tutorial crearás **Document Generator**, una aplicación que convierte plantillas reutilizables en documentos personalizados usando los datos que ya tienes en tu CRM.
|
||||
|
||||
Escribe una plantilla una vez con `{{placeholders}}`, luego genera un documento completado para cualquier Persona o Empresa con un solo clic, desde el menú de comandos, desde un agente de IA o desde un flujo de trabajo.
|
||||
|
||||
<Frame caption="Una plantilla, generada para una persona específica, abierta como una página imprimible.">
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/07-rendered-document.png" alt="Un documento generado de propuesta de ventas" />
|
||||
</Frame>
|
||||
|
||||
## Lo que aprenderás
|
||||
|
||||
Cada capítulo añade una capacidad. Al final habrás tocado la mayoría del SDK.
|
||||
|
||||
| Capítulo | Capacidad | Referencia |
|
||||
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| [1. Modelo de datos](/l/es/developers/extend/apps/tutorials/document-generator/data-model) | Objetos, campos y una relación | [Data](/l/es/developers/extend/apps/data/overview) |
|
||||
| [2. Generando documentos](/l/es/developers/extend/apps/tutorials/document-generator/generating-documents) | Una función lógica (herramienta (AI + acción de flujo de trabajo) que rellena una plantilla de Markdown y adjunta un PDF pulido | [Funciones lógicas](/l/es/developers/extend/apps/logic/logic-functions) |
|
||||
| [3. Rutas HTTP](/l/es/developers/extend/apps/tutorials/document-generator/http-routes) | Ejecutando JSON y una página HTML compartible desde rutas | [Funciones lógicas](/l/es/developers/extend/apps/logic/logic-functions) |
|
||||
| [4. Construir la interfaz de usuario](/l/es/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vistas, navegación, menú de comandos y componentes frontales que previsualizan un documento y editan una plantilla | [Diseño](/l/es/developers/extend/apps/layout/overview) |
|
||||
| [5. An AI agent](/l/es/developers/extend/apps/tutorials/document-generator/ai-agent) | Agente + habilidad | [Habilidades y agentes](/l/es/developers/extend/apps/logic/skills-and-agents) |
|
||||
| [6. Publicación](/l/es/developers/extend/apps/tutorials/document-generator/publishing) | Envíalo al mercado | [Publicación](/l/es/developers/extend/apps/operations/publishing) |
|
||||
|
||||
## Prerrequisitos
|
||||
|
||||
Deberías haber terminado el [Inicio rápido](/l/es/developers/extend/apps/getting-started/quick-start):
|
||||
un servidor Veinte local corriendo en el puerto `2020` y el CLI autenticado en él.
|
||||
|
||||
Si no, andamio e inicia uno ahora:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest document-generator
|
||||
cd document-generator
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
<Note>
|
||||
¿Prefieres leer el código terminado? La aplicación completa vive en
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
Cada fragmento de abajo es copiado de él.
|
||||
</Note>
|
||||
|
||||
## Cómo encaja la aplicación
|
||||
|
||||
<Frame>
|
||||
<img src="/images/docs/desarrolladores/extends/apps/document-generator/how-it-fits.svg" alt="Una plantilla con marcadores de posición se genera en un documento pulido con un PDF, activado desde el menú de comandos, un agente IA, un flujo de trabajo o un enlace compartible" />
|
||||
</Frame>
|
||||
|
||||
Escribes una **plantilla** una vez en un editor de texto rich, con `{{placeholders}}`. Al elegir una plantilla
|
||||
y un registro CRM rellenan los marcadores de posición y almacenan un **documento** pulido
|
||||
(con un archivo PDF). Todo lo demás — el menú de comandos, el agente IA,
|
||||
el paso del flujo de trabajo, el enlace compartible — es sólo una manera diferente de activar que
|
||||
un generador.
|
||||
|
||||
## Mantener este ciclo en ejecución
|
||||
|
||||
Deja `yarn twenty dev` corriendo en un terminal para el tutorial completo. Cada vez que
|
||||
agregas o editas un archivo en `src/`, se vuelve a sincronizar con tu servidor en unos
|
||||
segundos, para que puedas ver cómo cada capacidad aparece en la interfaz de usuario a medida que la construyes.
|
||||
|
||||
<Card title="Comenzar construcción →" icon="database" href="/Developopers/extend/apps/tutorials/document-generator/data-model">
|
||||
Capítulo 1: documentos de modelo y plantillas.
|
||||
</Card>
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: 6. Publicación
|
||||
icon: rocket
|
||||
description: Añade metadatos de mercado y publica tu aplicación.
|
||||
---
|
||||
|
||||
Tu app funciona. El último paso es describirlo para el mercado y publicarlo.
|
||||
|
||||
## Añadir metadatos de mercado
|
||||
|
||||
La [configuración de la aplicación](/l/es/developers/extend/apps/config/application) lleva los enlaces
|
||||
que se muestran en el mercado: autor, categoría, logotipo y soporte
|
||||
. Pon un logotipo en `public/` y referencialo con `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/es/developers/extend/apps',
|
||||
termsUrl: 'https://www.twenty.com/terms',
|
||||
emailSupport: 'contact@twenty.com',
|
||||
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
El rol por defecto se declara con `defineApplicationRole()` en su propio archivo — usted
|
||||
ya no pase `defaultRoleUniversalIdentifier` aquí.
|
||||
</Tip>
|
||||
|
||||
También añade la palabra clave `twenty-app` a `package.json` para que la aplicación sea detectable:
|
||||
|
||||
```json filename="package.json"
|
||||
{ "keywords": ["twenty-app"] }
|
||||
```
|
||||
|
||||
## Añadir capturas de pantalla de galería
|
||||
|
||||
Un listado de mercado se vende a sí mismo con capturas de pantalla. Soltar unos pocos PNGs en
|
||||
`public/gallery/` y referenciarlos con `capturas de pantalla` — se renderizan como una galería
|
||||
en la página de listado.
|
||||
|
||||
```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>
|
||||
Lance con la ganancia: haga la primera captura de pantalla el resultado terminado (un documento
|
||||
generado), luego muestre cómo se activa y se autoriza. Usa capturas
|
||||
nítidas y de alta resolución, son lo primero que un usuario ve.
|
||||
</Tip>
|
||||
|
||||
Dale a `README.md` el mismo tratamiento: es la página principal de npm y GitHub.
|
||||
Abre con la proposición de valor y una captura de pantalla, muestra las características de la línea de cabecera,
|
||||
y mantén los detalles de construcción debajo del plano.
|
||||
|
||||
## Comprobar antes de enviar
|
||||
|
||||
Ejecuta las mismas puertas que CI hace:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
```
|
||||
|
||||
La ejecución seca imprime exactamente lo que cambiaría en el servidor sin aplicarlo —
|
||||
una buena comprobación final de sanidad. Ver
|
||||
[Testing](/l/es/developers/extend/apps/operations/testing) y
|
||||
[Sincronizando y recuperando](/l/es/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` construye y publica a npm por defecto; `--private` sube un tarball
|
||||
a un registro privado del servidor Veinte en su lugar. Para superficiar una aplicación publicada
|
||||
en el mercado de una instancia, dispara una sincronización de catálogo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync -r <remote>
|
||||
```
|
||||
|
||||
Detalles completos y la lista de verificación de lanzamiento:
|
||||
[Publishing](/l/es/developers/extend/apps/operations/publishing).
|
||||
|
||||
## Construiste una aplicación 🎉
|
||||
|
||||
En seis capítulos utilizaste la mayor parte de la superficie SDK:
|
||||
|
||||
* **Objetos, campos y una relación** para modelar los datos
|
||||
* Una **función lógica** expuesta como una **herramienta de IA**, una **acción de flujo de trabajo**, y **rutas HTTP**
|
||||
* **Vistas, navegación, un comando y un componente frontal** para la interfaz de usuario
|
||||
* Un **agente + habilidad** para generación natural
|
||||
* **Metadatos del mercado** y el flujo de publicación
|
||||
|
||||
La aplicación terminada está en
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
|
||||
## A dónde ir siguiente
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Referencia de datos" icon="database" href="/l/es/developers/extend/apps/data/overview">
|
||||
Cada tipo de campo, relación y opción de índice.
|
||||
</Card>
|
||||
<Card title="Referencia lógica" icon="bolt" href="/l/es/developers/extend/apps/logic/overview">
|
||||
Activadores de eventos Cron y base de datos, el almacén clave-valor, conexiones OAuth.
|
||||
</Card>
|
||||
<Card title="Referencia del diseño" icon="table-columns" href="/l/es/developers/extend/apps/layout/overview">
|
||||
Diseños de páginas, widgets de tablero y más superficies de interfaz.
|
||||
</Card>
|
||||
<Card title="Operaciones" icon="rocket" href="/l/es/developers/extend/apps/operations/overview">
|
||||
CLI, prueba, control remoto, y CI.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user