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: Lascia che un agente generi documenti da una chat, usando il tuo strumento.
|
||||
---
|
||||
|
||||
Poiché `generate-document` è esposto come **strumento**, un agente AI può chiamarlo.
|
||||
Aggiungiamo un agente e un'abilità in modo che gli utenti possano solo dire *"generare una proposta per
|
||||
Jeffery Griffin"*.
|
||||
|
||||
## L'abilità
|
||||
|
||||
A [skill](/l/it/developers/extend/apps/logic/skills-and-agents) è riutilizzabile
|
||||
istruzioni — conoscenza che si lega agli agenti. Il nostro insegna al modello come usare
|
||||
lo strumento.
|
||||
|
||||
```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'),
|
||||
});
|
||||
```
|
||||
|
||||
## L'agente
|
||||
|
||||
Un [agent](/l/it/developers/extend/apps/logic/skills-and-agents) coppia un prompt con un modello
|
||||
. Imposta esplicitamente `responseFormat` per evitare un avviso di generazione.
|
||||
|
||||
```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>
|
||||
L'agente può chiamare lo strumento solo se il suo ruolo lo permette. Abbiamo già impostato
|
||||
`canAccessAllTools: true` e `canBeAssignedToAgents: true` sul ruolo dell'app in
|
||||
[Capitolo 2](/l/it/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
|
||||
</Note>
|
||||
|
||||
## Provalo
|
||||
|
||||
Apri una chat con **Document Assistant** e chiedi di redigere un documento per una persona
|
||||
nel tuo CRM. Trova il record, chiama `documento generato`, e riporta
|
||||
indietro il documento che ha creato — che ora appare nella tua vista **Documenti**,
|
||||
esattamente come il menu di comando e i percorsi del flusso di lavoro.
|
||||
|
||||
Questo è il payoff della logica di esposizione come uno strumento: **una funzione, molte porte d'ingresso** —
|
||||
menu di comando, HTTP, passo del flusso di lavoro, e ora il linguaggio naturale.
|
||||
|
||||
**Dopo questo passaggio:** l'app è completa e genuinamente utile. Tempo per
|
||||
spedirlo.
|
||||
|
||||
<Card title="Successivo: pubblicazione →" icon="rocket" href="/l/it/developers/extend/apps/tutorials/document-generator/publishing">
|
||||
Aggiungi metadati di mercato e pubblica.
|
||||
</Card>
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: 4. Costruire l'interfaccia utente
|
||||
icon: table-columns
|
||||
description: Viste, navigazione della barra laterale, un comando e componenti anteriori.
|
||||
---
|
||||
|
||||
In questo momento gli oggetti sono raggiungibili solo attraverso le Impostazioni. Diamo all'app una presenza
|
||||
reale nell'UI: viste elenco, voci sidebar, un comando con un solo clic
|
||||
**Genera documento** un componente frontale della record-page per **anteprima** un documento
|
||||
e una scheda **editor** di testo ricco nativo per i modelli.
|
||||
|
||||
## Visualizzazioni e navigazione
|
||||
|
||||
Un [view](/l/it/developers/extend/apps/layout/views) è una lista salvata di un dato oggetto.
|
||||
Una [voce del menu di navigazione](/l/it/developers/extend/apps/layout/navigation-menu-items)
|
||||
mette quella vista nella barra laterale.
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Aggiungi la stessa coppia per i modelli. Entrambi ora mostrano nella barra laterale:
|
||||
|
||||
<Frame caption="Documenti e Modelli nella barra laterale, con il documento generato elencato.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Vista documenti con un documento generato" />
|
||||
</Frame>
|
||||
|
||||
## Un componente anteriore
|
||||
|
||||
Un [componente anteriore](/l/it/developers/extend/apps/layout/front-components) è un componente React
|
||||
sabbiato all'interno di Twenty. La nostra legge il record selezionato, carica i modelli di persona
|
||||
tramite `CoreApiClient`, e POSTs sul percorso dall'ultimo capitolo
|
||||
.
|
||||
|
||||
```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>
|
||||
Stile con variabili CSS in linea (`var(--t-color-blue)`), non valori importati da
|
||||
`twenty-ui`. Gli SDK mocks che il pacchetto durante la build, quindi le importazioni a livello di modulo di costanti del tema
|
||||
sarebbero `indefinite`. Vedi il [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 per aprirlo
|
||||
|
||||
Una [voce del menu di comando](/l/it/developers/extend/apps/layout/command-menu-items) con
|
||||
`disponibilitàTipo: 'RECORD_SELECTION'` viene visualizzata quando una persona è selezionata, e
|
||||
apre il componente nel pannello laterale.
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
## Prova l'intero flusso
|
||||
|
||||
Aprire **Persone**, spuntare una persona e premere <kbd>국K</kbd> / <kbd>Ctrl K</kbd>.
|
||||
"Genera documento" appare, taggato con la tua app:
|
||||
|
||||
<Frame caption="Il comando viene visualizzato quando una persona è selezionata.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Menu comandi con Genera documento" />
|
||||
</Frame>
|
||||
|
||||
Eseguire — il componente si apre nel pannello laterale. Scegli un modello, fai clic su
|
||||
**Genera**, e un nuovo record atterra in **Documenti**.
|
||||
|
||||
<Frame caption="Il componente anteriore, il caricamento dei modelli e la generazione al clic.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Genera pannello laterale documento" />
|
||||
</Frame>
|
||||
|
||||
Ogni documento generato registra la tua app come suo autore:
|
||||
|
||||
<Frame caption="Creato da Generatore di documenti, stato generato.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Un documento generato" />
|
||||
</Frame>
|
||||
|
||||
## Anteprima di un documento nella sua pagina di record
|
||||
|
||||
Un componente frontale non è solo per i menu di comando: puoi montarne uno come \*\*scheda su una pagina di record
|
||||
\*\*. Aggiungiamo una scheda *Anteprima* al record di documenti che rende il corpo
|
||||
Markdown come una pagina lucida e stampabile.
|
||||
|
||||
Il componente legge l'id del record corrente dal suo contesto di esecuzione, carica il documento
|
||||
e lo rende. I componenti anteriori vengono eseguiti in una **sandbox** che permette solo una whitelist
|
||||
di tag HTML — iniezione HTML grezza (`dangerouslySetInnerHTML`) e
|
||||
`\<style>` sono bloccati — quindi rendiamo Markdown come elementi React con stili inline
|
||||
tramite un piccolo [`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,
|
||||
});
|
||||
```
|
||||
|
||||
Monta con un [layout pagina](/l/it/developers/extend/apps/layout/page-layouts). Un layout
|
||||
`RECORD_PAGE` aggiunge schede alla vista record di un oggetto; un widget `FRONT_COMPONENT`
|
||||
in una scheda `CANVAS` ospita il 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,
|
||||
},
|
||||
}],
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
Apri qualsiasi documento — una scheda **Anteprima** lo rende splendido, con link alla pagina web
|
||||
condivisibile e il PDF:
|
||||
|
||||
<Frame caption="La scheda Anteprima rende il documento con stili in linea, più collegamenti rapidi.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Componente frontale del visualizzatore documenti in una scheda record-page" />
|
||||
</Frame>
|
||||
|
||||
## Modifica un modello con l'editor di testo
|
||||
|
||||
I modelli non hanno bisogno di un componente personalizzato. Perché il `body` è un campo
|
||||
`RICH_TEXT`, Venti fornisce già un editor di testo completo per esso — l'
|
||||
stesso degli oggetti Note e Attività standard utilizzati. Lo superficiamo solo sulla pagina di record di modello
|
||||
.
|
||||
|
||||
Aggiungi una scheda con un widget `FIELD` in modalità di visualizzazione `EDITOR`, puntando sul campo `body`
|
||||
tramite `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` memorizza sia il blocco dell'editor JSON che una proiezione Markdown
|
||||
. La pipeline di generazione legge che Markdown proiezione, quindi
|
||||
segnaposti, il PDF, e la pagina web condivisibile tutti continuano a lavorare invariato —
|
||||
vedere il 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).
|
||||
Ora gli editori scrivono i modelli in un corretto editor di testo ricco:
|
||||
|
||||
<Frame caption="La scheda Modello: Editor nativo di testo ricco di venti legato al campo corpo.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Template record con la scheda nativa editor di testo ricco" />
|
||||
</Frame>
|
||||
|
||||
**Dopo questo passaggio:** i documenti in anteprima magnificamente e i modelli sono modificabili
|
||||
in-app. Poi, lasciare che un agente AI li generi da una chat.
|
||||
|
||||
<Card title="Il prossimo: un agente AI →" icon="robot" href="/l/it/developers/extend/apps/tutorials/document-generator/ai-agent">
|
||||
Aggiungi un agente e un'abilità che chiama il tuo strumento.
|
||||
</Card>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: 1. Modello dati
|
||||
icon: database
|
||||
description: Modelli di documenti e modelli con oggetti, campi e una relazione.
|
||||
---
|
||||
|
||||
La nostra app ha bisogno di due oggetti personalizzati: **modelli di documenti** (cosa scrivere) e
|
||||
**documenti** (il risultato generato). Le definiamo.
|
||||
|
||||
Scaffold each entity file with the CLI — genera un UUID valido e la cartella
|
||||
giusta per te:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
```
|
||||
|
||||
Di seguito mostriamo i file finiti.
|
||||
|
||||
<Note>
|
||||
Ogni costante `*_UNIVERSAL_IDENTIFIER` vive in
|
||||
`src/constants/universal-identifiers.ts` ed è importata dove utilizzata. Gli snippet
|
||||
qui sotto omettono quelle importazioni per brevità — tenerli nei propri file.
|
||||
</Note>
|
||||
|
||||
## L'oggetto del modello
|
||||
|
||||
Un modello ha un `name`, un `body` con `{{placeholders}}`, e un `target` che
|
||||
dice se è scritto per una persona o un'azienda. Il `body` è un campo
|
||||
`RICH_TEXT`, quindi Twenty gli dà un editor completo di testo ricco.
|
||||
|
||||
```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>
|
||||
L'opzione `SELECT` **values** deve essere `UPPER_CASE` (`PERSON`, non `person`), e il `defaultValue`
|
||||
è racchiuso in virgolette extra: `` `'PERSON'` ``. Il `label` è quello che vedono gli utenti
|
||||
.
|
||||
</Warning>
|
||||
|
||||
## L'oggetto del documento
|
||||
|
||||
Il documento generato memorizza il `content` renderizzato e un `status`. Definisci
|
||||
allo stesso modo, con una selezione `status` di `DRAFT` / `GENERATED`. File completo:
|
||||
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
|
||||
|
||||
## Collegarli con una relazione
|
||||
|
||||
Ogni documento deve riportare il modello da cui proveniva. Le relazioni sono
|
||||
**bidirezionali** — si definiscono entrambi i lati, ciascuno nel proprio file di 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',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
L'altro lato (`template-documents-relation.field.ts`) è un campo
|
||||
`RelationType.ONE_TO_MANY` chiamato `documents` che punta il senso opposto.
|
||||
Vedi [Relations](/l/it/developers/extend/apps/data/relations) per il modello completo.
|
||||
|
||||
## Vedere in venti
|
||||
|
||||
Con `yarn venti dev` in esecuzione, apri **Impostazioni → Modello dati**. Entrambi gli oggetti
|
||||
appariranno, etichettati con la tua app.
|
||||
|
||||
<Frame caption="Entrambi gli oggetti personalizzati, di proprietà dell'app Generatore di documenti.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Impostazioni del modello di dati che mostrano i modelli di documenti e documenti" />
|
||||
</Frame>
|
||||
|
||||
Crea un modello da testare con — chiamalo *Proposta vendite*, imposta **Obiettivo** su
|
||||
*Persona*, e incolla un corpo con alcuni segnaposti:
|
||||
|
||||
```text
|
||||
Dear {{name.firstName}} {{name.lastName}},
|
||||
|
||||
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
|
||||
|
||||
Best,
|
||||
The Team
|
||||
```
|
||||
|
||||
<Frame caption="Un record di modello. Il corpo mantiene i segnaposto finché non viene generato un documento.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Un record modello di proposta di vendita con corpo segnaposto" />
|
||||
</Frame>
|
||||
|
||||
**Dopo questo passaggio:** hai oggetti `documentTemplate` e `document`, collegati da
|
||||
una relazione e un modello da cui generare. Successivamente, la logica che lo riempie.
|
||||
|
||||
<Card title="Successivo: generare documenti →" icon="bolt" href="/l/it/developers/extend/apps/tutorials/document-generator/generating-documents">
|
||||
Scrivi la funzione logica che riempie il modello.
|
||||
</Card>
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: 2. Generazione documenti
|
||||
icon: bolt
|
||||
description: Una funzione logica, esposta come strumento AI e un'azione di flusso di lavoro.
|
||||
---
|
||||
|
||||
Ora il core: una [funzione logica](/l/it/developers/extend/apps/logic/logic-functions)
|
||||
che carica un modello e un record, riempie i segnaposti, e salva un nuovo documento
|
||||
.
|
||||
|
||||
Scriveremo la logica aziendale una volta come **handler**, quindi esponendola attraverso
|
||||
diversi trigger. Questo capitolo ne collega due — uno **strumento di intelligenza artificiale** e un'azione di workflow
|
||||
\*\*.
|
||||
|
||||
## L'helper del rendering
|
||||
|
||||
Mantenere la logica pura nel proprio file in modo che sia facile da unit-test. Questo appiattisce un record
|
||||
in `{{dot.path}}` token e li sostituisce.
|
||||
|
||||
```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>
|
||||
Poiché questo file non ha effetti collaterali, puoi coprirlo con i test veloci di unità
|
||||
(`yarn test:unit`). Vedi [Testing](/l/it/developers/extend/apps/operations/testing).
|
||||
</Tip>
|
||||
|
||||
## Il gestore
|
||||
|
||||
Il gestore utilizza il [`CoreApiClient`](/l/it/developers/extend/apps/logic/logic-functions)
|
||||
generato per leggere e scrivere i dati CRM. Carica il modello, carica il record di destinazione, riempie
|
||||
il corpo e crea un `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` esegue una query diversa per una Persona contro una Società e appiattisce
|
||||
il risultato — vedi
|
||||
[`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).
|
||||
|
||||
## Esporre come strumento e come azione del flusso di lavoro
|
||||
|
||||
Un singolo `defineLogicFunction` può portare diversi trigger. Qui, `toolTriggerSettings`
|
||||
lo rende chiamabile da agenti AI, e `workflowActionTriggerSettings` lo trasforma in un passaggio
|
||||
nel costruttore di flussi di lavoro visivi. Entrambi descrivono il loro input con uno schema 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,
|
||||
});
|
||||
```
|
||||
|
||||
Lo schema di input è uno schema JSON semplice che descrive `templateId` e `recordId` —
|
||||
vedi [`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).
|
||||
|
||||
## Concedi l'accesso
|
||||
|
||||
Le funzioni Logica vengono eseguite come ruolo dell'app. Ha bisogno di leggere i modelli e registra
|
||||
e creare documenti, in modo da consentire che in `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` permette alla funzione di caricare il PDF generato nella prossima sezione.
|
||||
Vedi [Roles](/l/it/developers/extend/apps/config/roles) per i permessi a grana più fine.
|
||||
|
||||
## Allega un file PDF reale
|
||||
|
||||
Un campo di testo renderizzato è utile, ma gli utenti vogliono un documento reale. Generiamo un file
|
||||
**PDF** e memorizzalo sul record come file scaricabile.
|
||||
|
||||
Innanzitutto, dai al `document` object un campo `FILES` per tenere il PDF. Le app caricano
|
||||
nei loro **propri** campi di file, quindi questo campo è quello che il caricamento:
|
||||
|
||||
```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 },
|
||||
}
|
||||
```
|
||||
|
||||
Ora renderizza quel PDF. Un'app è un vero progetto Node, quindi puoi aggiungere qualsiasi pacchetto npm
|
||||
che ti serve e importarlo come altrove. Usiamo **[pdf-lib](https://pdf-lib.js.org/)**
|
||||
per disegnare il PDF e **[marked](https://marked.js.org/)** per analizzare il corpo Markdown
|
||||
— il CLI li installa nel runtime della funzione per te:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add pdf-lib marked
|
||||
```
|
||||
|
||||
L'helper 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).
|
||||
Analizza il Markdown in token con `marked. exer`, poi li mette fuori con
|
||||
pdf-lib: intestazioni reali, **bold**/*italic* runs, proiettile e liste numerate,
|
||||
blockquotes e regole — un rendering A4 lucido e multi-pagina del modello
|
||||
stesso, piuttosto che un muro di testo.
|
||||
|
||||
<Frame caption="Il PDF generato: la tipografia reale e la formattazione Markdown, rendendo il corpo del modello.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Un PDF generato lucido e commerciabile" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
pdf-lib's built-in font utilizzare WinAnsi encoding, così gli accenti occidentali europei rendono
|
||||
fuori dalla scatola; le mappe helper citazioni intelligenti e trattini e lascia i caratteri
|
||||
non possono codificare. Rendering non latino script (Cinese, Arabo, Cirillico) significherebbe
|
||||
incorporare un carattere Unicode.
|
||||
</Note>
|
||||
|
||||
Quindi caricarlo e memorizzare il riferimento sul record. `uploadFile` percorre bytes
|
||||
nel campo dei file di proprietà dell'app; il `id` restituito è quello che salvi:
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Il documento generato ora contiene un PDF scaricabile:
|
||||
|
||||
<Frame caption="Il PDF generato, memorizzato nel campo File del documento.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Un record di documento con un file PDF generato" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
`uploadFile` si rivolge solo ai campi di file **app-owned** (quindi i caricamenti richiedono sempre un'app
|
||||
che possiede il campo, più il flag ruolo `UPLOAD_FILE`). Ecco perché il PDF
|
||||
atterra sul campo `file` del record - lo stesso modello utilizzato dall'app
|
||||
[call-recorder](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
|
||||
per le registrazioni.
|
||||
</Note>
|
||||
|
||||
**Dopo questo passaggio:** ogni documento generato ha un PDF reale e scaricabile. Ma
|
||||
niente può *chiamare* il generatore dall'interfaccia utente ancora — per questo abbiamo bisogno di un percorso HTTP.
|
||||
|
||||
<Card title="Il prossimo: percorsi HTTP →" icon="globo" href="/l/it/developers/extend/apps/tutorials/document-generator/http-route">
|
||||
Servire la funzione su HTTP e rendere i documenti come pagine web.
|
||||
</Card>
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: 3. Percorsi HTTP
|
||||
icon: globe
|
||||
description: Attivare la funzione su HTTP e rendere i documenti come pagine web.
|
||||
---
|
||||
|
||||
Lo stesso gestore può anche rispondere alle richieste HTTP. Aggiungeremo due percorsi:
|
||||
|
||||
* un endpoint **POST** per generare un documento, e
|
||||
* un endpoint pubblico **GET** che rende un documento come una pagina web stampabile.
|
||||
|
||||
Entrambi usano `httpRouteTriggerSettings`. Gli itinerari delle app sono serviti sotto `/s` sul tuo server
|
||||
Twenty (es. `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
## Percorso POST — generare su richiesta
|
||||
|
||||
Questo riutilizza `generateDocumentHandler`, quindi non c'è alcuna logica da ripetere — solo un sottile adattatore
|
||||
che legge il corpo della richiesta.
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Il gestore condiviso restituisce un suggerito `status` al fallimento, quindi il percorso può rispondere
|
||||
con un corretto codice `4xx`/`5xx`. `isAuthRequired: true` significa che il chiamante
|
||||
deve presentare un token valido — il componente anteriore nel capitolo successivo passa automaticamente il token di accesso dell'utente
|
||||
.
|
||||
|
||||
## OTTIENI il percorso — renderizza come pagina web
|
||||
|
||||
Per restituire HTML invece di JSON, avvolgi il corpo in un `Response` con un'intestazione
|
||||
`Content-Type`. Questo percorso è pubblico (`isAuthRequired: false`) così un documento generato
|
||||
può essere condiviso come 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` rende il corpo Markdown in HTML (con [marked](https://marked.js.org/),
|
||||
sanitizzato) e lo lascia in un pulito, pagina stampabile che mostra solo il contenuto del template
|
||||
— lo stesso aspetto del PDF e l'anteprima in-app.
|
||||
[Vedi l'helper](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
|
||||
|
||||
## Provalo
|
||||
|
||||
Con un modello e una persona nel tuo workspace, chiama il percorso (prendi un token da
|
||||
**Impostazioni → API & 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, ..."}
|
||||
```
|
||||
|
||||
Apri il documento restituito nel tuo browser:
|
||||
|
||||
```
|
||||
http://localhost:2020/s/documents/view?id=<documentId>
|
||||
```
|
||||
|
||||
<Frame caption="Il percorso GET pubblico rende il documento come una pagina stampabile.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Una pagina web di documento renderizzato" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
Puoi anche trasmettere i log di una funzione durante il test con
|
||||
`yarn venti dev:function:logs`, o invocarlo direttamente con
|
||||
`yarn venti dev:function:exec`.
|
||||
</Tip>
|
||||
|
||||
**Dopo questo passaggio:** l'app può generare documenti su HTTP e servirli come pagine web
|
||||
. Ora rendiamo utilizzabile senza `curl`.
|
||||
|
||||
<Card title="Il prossimo: costruire l'interfaccia utente →" icon="table-columns" href="/l/it/developers/extend/apps/tutorials/document-generator/building-the-ui">
|
||||
Viste, navigazione, un comando e un componente anteriore.
|
||||
</Card>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "Tutorial: Generatore di documenti"
|
||||
icon: wand-magic-sparkles
|
||||
description: Crea una vera app Twenty che genera documenti personalizzati a partire dai dati del tuo CRM.
|
||||
---
|
||||
|
||||
In questo tutorial creerai **Document Generator**, un'app che trasforma modelli riutilizzabili in documenti personalizzati usando i dati già presenti nel tuo CRM.
|
||||
|
||||
Scrivi una volta un modello con `{{placeholders}}`, quindi genera un documento compilato per qualsiasi Persona o Azienda con un solo clic, dal command menu, da un agente AI o da un workflow.
|
||||
|
||||
<Frame caption="Un modello, generato per una persona specifica, aperto come pagina stampabile.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Un documento di proposta di vendita generato" />
|
||||
</Frame>
|
||||
|
||||
## Cosa imparerai
|
||||
|
||||
Ogni capitolo aggiunge una funzionalità. Alla fine avrai toccato la maggior parte dell’SDK.
|
||||
|
||||
| Capitolo | Funzionalità | Riferimento |
|
||||
| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| [1. Modello dati](/l/it/developers/extend/apps/tutorials/document-generator/data-model) | Oggetti, campi e una relazione | [Dati](/l/it/developers/extend/apps/data/overview) |
|
||||
| [2. Generare documenti](/l/it/developers/extend/apps/tutorials/document-generator/generating-documents) | Una funzione logica (strumento IA + azione del workflow) che compila un modello Markdown e allega un PDF rifinito | [Funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions) |
|
||||
| [3. Route HTTP](/l/it/developers/extend/apps/tutorials/document-generator/http-routes) | Servire JSON e una pagina HTML condivisibile dalle route | [Funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions) |
|
||||
| [4. Creare l’interfaccia utente](/l/it/developers/extend/apps/tutorials/document-generator/building-the-ui) | Viste, navigazione, menu dei comandi e componenti front-end che mostrano l’anteprima di un documento e modificano un modello | [Layout](/l/it/developers/extend/apps/layout/overview) |
|
||||
| [5. Un agente IA](/l/it/developers/extend/apps/tutorials/document-generator/ai-agent) | Agente + abilità | [Abilità e agenti](/l/it/developers/extend/apps/logic/skills-and-agents) |
|
||||
| [6. Pubblicazione](/l/it/developers/extend/apps/tutorials/document-generator/publishing) | Pubblicala nel marketplace | [Pubblicazione](/l/it/developers/extend/apps/operations/publishing) |
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
Dovresti aver completato il [Quick Start](/l/it/developers/extend/apps/getting-started/quick-start):
|
||||
un server Twenty locale in esecuzione sulla porta `2020` e la CLI autenticata ad esso.
|
||||
|
||||
In caso contrario, esegui ora lo scaffold e avvialo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest document-generator
|
||||
cd document-generator
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
<Note>
|
||||
Preferisci leggere il codice finito? L’app completa si trova in
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
Ogni snippet qui sotto è copiato da lì.
|
||||
</Note>
|
||||
|
||||
## Come si integra l’app
|
||||
|
||||
<Frame>
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Un modello con segnaposto viene trasformato in un documento rifinito con un PDF, attivato dal menu dei comandi, da un agente IA, da un workflow o da un link condivisibile" />
|
||||
</Frame>
|
||||
|
||||
Scrivi una **template** una sola volta in un editor rich text, con `{{placeholders}}`. Scegliere una
|
||||
template e un record CRM compila i segnaposto e memorizza un
|
||||
**documento** rifinito (con un file PDF). Tutto il resto — il menu dei comandi, l’agente IA,
|
||||
lo step del workflow, il link condivisibile — è solo un modo diverso di attivare quell’unico generatore.
|
||||
|
||||
## Mantieni attivo questo ciclo
|
||||
|
||||
Lascia `yarn twenty dev` in esecuzione in un terminale per l’intero tutorial. Ogni volta che
|
||||
aggiungi o modifichi un file sotto `src/`, viene nuovamente sincronizzato con il tuo server entro pochi
|
||||
secondi, così puoi vedere ogni funzionalità comparire nell’interfaccia utente mentre la costruisci.
|
||||
|
||||
<Card title="Inizia a sviluppare →" icon="database" href="/l/it/developers/extend/apps/tutorials/document-generator/data-model">
|
||||
Capitolo 1: modella documenti e template.
|
||||
</Card>
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: 6. Pubblicazione
|
||||
icon: rocket
|
||||
description: Aggiungi metadati di mercato e pubblica la tua app.
|
||||
---
|
||||
|
||||
La tua app funziona. L'ultimo passo è descriverlo per il mercato e pubblicarlo.
|
||||
|
||||
## Aggiungi metadati di mercato
|
||||
|
||||
La [application config](/l/it/developers/extend/apps/config/application) contiene l'identità
|
||||
che appare nel marketplace: autore, categoria, logo e supporta i link
|
||||
. Metti un logo in `public/` e fai riferimento 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/it/developers/extend/apps',
|
||||
termsUrl: 'https://www.twenty.com/terms',
|
||||
emailSupport: 'contact@twenty.com',
|
||||
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Il ruolo predefinito viene dichiarato con `defineApplicationRole()` nel proprio file — non passi più `defaultRoleUniversalIdentifier` qui.
|
||||
</Tip>
|
||||
|
||||
Aggiungi anche la parola chiave `twenty-app` a `package.json` così l'app è scopribile:
|
||||
|
||||
```json filename="package.json"
|
||||
{ "keywords": ["twenty-app"] }
|
||||
```
|
||||
|
||||
## Aggiungi screenshot galleria
|
||||
|
||||
Una quotazione di mercato si vende con screenshot. Trascina alcuni PNG nella `public/gallery/` di
|
||||
e li referenzia con `screenshots` — sono una galleria
|
||||
nella pagina di inserimento.
|
||||
|
||||
```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>
|
||||
Lead with the payoff: make the first screenshot the finished result (un generated
|
||||
document), then show how it's triggered and authored. Usa cattura
|
||||
croccanti e ad alta risoluzione — sono la prima cosa che un utente vede.
|
||||
</Tip>
|
||||
|
||||
Dare `README.md` lo stesso trattamento — è la prima pagina su npm e GitHub.
|
||||
Apri con la proposizione del valore e uno screenshot, elenca le caratteristiche del titolo,
|
||||
quindi tieni i dettagli della build sotto la piega.
|
||||
|
||||
## Controllare prima di spedire
|
||||
|
||||
Eseguire lo stesso cancelli CI fa:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
```
|
||||
|
||||
L'esecuzione a secco stampa esattamente quello che cambierebbe sul server senza applicarlo —
|
||||
un buon controllo finale di sanità. Vedi
|
||||
[Testing](/l/it/developers/extend/apps/operations/testing) e
|
||||
[Sincronizzazione & recupero](/l/it/developers/extend/apps/operations/sync-and-recovery).
|
||||
|
||||
## Pubblica
|
||||
|
||||
```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` costruisce e pubblica a npm per impostazione predefinita; `--private` carica un tarball
|
||||
su un registro privato di Twenty server. Per superficiare un'app pubblicata
|
||||
nel marketplace di un'istanza, attiva una sincronizzazione del catalogo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync -r <remote>
|
||||
```
|
||||
|
||||
Dettagli completi e la lista di controllo rilascio:
|
||||
[Publishing](/l/it/developers/extend/apps/operations/publishing).
|
||||
|
||||
## Hai costruito un'app 🎉
|
||||
|
||||
In sei capitoli hai usato la maggior parte della superficie SDK:
|
||||
|
||||
* **Oggetti, campi e una relazione** per modellare i dati
|
||||
* Una **funzione logica** esposta come **strumento di intelligenza artificiale**, un **workflow action**, e **HTTP routes**
|
||||
* **Viste, navigazione, un comando e un componente frontale** per l'interfaccia utente
|
||||
* Un **agente + abilità** per la generazione di linguaggio naturale
|
||||
* **Marketplace metadata** e il flusso di pubblicazione
|
||||
|
||||
L'app completata è su
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
|
||||
## Dove andare avanti
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Riferimento dei dati" icon="database" href="/l/it/developers/extend/apps/data/overview">
|
||||
Ogni tipo di campo, relazione e opzione indice.
|
||||
</Card>
|
||||
<Card title="Riferimento logico" icon="bolt" href="/l/it/developers/extend/apps/logic/overview">
|
||||
Cron e database-event triggers, il key-value store, connessioni OAuth.
|
||||
</Card>
|
||||
<Card title="Riferimento del layout" icon="table-columns" href="/l/it/developers/extend/apps/layout/overview">
|
||||
Layout di pagina, widget dashboard e più superfici dell'interfaccia utente.
|
||||
</Card>
|
||||
<Card title="Operazioni" icon="rocket" href="/l/it/developers/extend/apps/operations/overview">
|
||||
CLI, prove, telecomandi e IC.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user