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: Permiteți unui agent să genereze documente dintr-o conversație, utilizând instrumentul dvs.
|
||||
---
|
||||
|
||||
Pentru că `generate-document` este expus ca **unealtă**, un agent AI îl poate chema.
|
||||
Hai să adăugăm un agent și o abilitate astfel încât utilizatorii să poată spune *"generează o propunere pentru
|
||||
Jeffery Griffin"*.
|
||||
|
||||
## Abilitatea
|
||||
|
||||
Un [skill](/l/ro/developers/extend/apps/logic/skills-and-agents) este reutilizabil
|
||||
instrucțiuni - cunoștințele atașate la agenți. Noi învață modelul cum să folosești
|
||||
unealta.
|
||||
|
||||
```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'),
|
||||
});
|
||||
```
|
||||
|
||||
## Agentul
|
||||
|
||||
Un [agent](/l/ro/developers/extend/apps/logic/skills-and-agents) împerechează o recomandare cu un model
|
||||
. Setați în mod explicit `responseFormat` pentru a evita un avertisment de construire.
|
||||
|
||||
```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>
|
||||
Agentul poate apela instrumentul doar dacă rolul său îi permite acest lucru. Am setat deja
|
||||
`canAccessAllTools: true` și `canBeAssignedToAgents: true` pe rolul aplicației în
|
||||
[capitolul 2](/l/ro/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
|
||||
</Note>
|
||||
|
||||
## Încearcă-l
|
||||
|
||||
Deschideți o conversație cu **Asistentul pentru documente** și cereți-i să elaboreze un document pentru o persoană
|
||||
din CRM. Găsește înregistrarea, apelează `generate-document`, și raportează
|
||||
înapoi documentul pe care l-a creat — care apare acum în **documentele**,
|
||||
exact ca meniul de comenzi și căile fluxului de lucru.
|
||||
|
||||
Aceasta este rezultatul prezentării logicii ca unealtă: **o funcție, multe uși din față** -
|
||||
meniul de comandă, HTTP, pasul fluxului de lucru, și acum limbajul natural.
|
||||
|
||||
**După acest pas:** aplicația este completă și cu adevărat utilă. Timpul până la expedierea lui
|
||||
.
|
||||
|
||||
<Card title="Următorul: publicare →" icon="rocket" href="/dezvoltatori/extindere/aplicații/tutoriale/generator documente/publicare">
|
||||
Adăugați metadate de bazar și publicați.
|
||||
</Card>
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
---
|
||||
title: 4. Construirea interfeței
|
||||
icon: table-columns
|
||||
description: Vizualizări, navigare bară laterală, o comandă și componente frontale.
|
||||
---
|
||||
|
||||
În acest moment, obiectele sunt accesibile doar prin Setări. Hai să oferim aplicației o prezență reală
|
||||
în UI: vizualizare listă, intrări sidebar, un singur click
|
||||
**Generează document** comandă, o filă de înregistrare a componentei frontale la **previzualizarea** un document
|
||||
şi o filă nativă bog-text **editor** pentru şabloane.
|
||||
|
||||
## Vizualizări și navigare
|
||||
|
||||
Un [view](/l/ro/developers/extend/apps/layout/views) este o listă salvată a unui obiect dat.
|
||||
Un [element meniu de navigare](/l/ro/developers/extend/apps/layout/navigation-menu-items)
|
||||
introduce această vizualizare în bara 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,
|
||||
});
|
||||
```
|
||||
|
||||
Adaugă aceeași pereche pentru șabloane. Ambele sunt prezentate acum pe partea laterală:
|
||||
|
||||
<Frame caption="Documente şi şabloane din bara laterală, documentul generat fiind enumerat.">
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/04-documents-view.png" alt="Vizualizare documente cu un document generat" />
|
||||
</Frame>
|
||||
|
||||
## O componentă față
|
||||
|
||||
O [componentă frontală](/l/ro/developers/extend/apps/layout/front-components) este o componentă React
|
||||
inserată în Twenty. Ours citeşte înregistrarea selectată, încarcă şabloanele pentru persoane
|
||||
prin `CoreApiClient`, şi POST-uri pe ruta de la ultimul capitol
|
||||
.
|
||||
|
||||
```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>
|
||||
Stilul cu variabilele CSS înline (`var(--t-color-blue)`), nu valorile importate din
|
||||
`douăzeci enty-ui`. SDK se dublează că pachetul a fost construit, astfel încât importurile constantelor de temă
|
||||
la nivel de modul ar fi `nedefinită`. Vezi
|
||||
[componenta completă](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
|
||||
</Warning>
|
||||
|
||||
## O comandă pentru a o deschide
|
||||
|
||||
Un [element meniu de comandă](/l/ro/developers/extend/apps/layout/command-menu-items) cu
|
||||
\`availabilityType: 'RECORD_SELECTION'' apare atunci când o Persoană este selectată, și
|
||||
deschide componenta în panoul 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,
|
||||
});
|
||||
```
|
||||
|
||||
## Încercați întregul flux
|
||||
|
||||
Deschideți **oameni**, bifați o persoană și apăsați <kbd>£K</kbd> / <kbd>Ctrl K</kbd>.
|
||||
"Generează document" apare, etichetat cu aplicația ta:
|
||||
|
||||
<Frame caption="Comanda apare atunci când o persoană este selectată.">
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/06-command-menu.png" alt="Meniul de comenzi cu Generarea documentului" />
|
||||
</Frame>
|
||||
|
||||
Rulează — componenta ta se deschide în panoul lateral. Alege un șablon, fă clic pe
|
||||
**Generează** și pe un nou teren de înregistrări în **documente**.
|
||||
|
||||
<Frame caption="Componenta frontală, șabloanele de încărcare și generarea unui clic.">
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/06b-front-componente.png" alt="Generare panou de document" />
|
||||
</Frame>
|
||||
|
||||
Fiecare document generat înregistrează aplicația dvs. ca autor:
|
||||
|
||||
<Frame caption="Creat de Generatorul de Document Stare generată.">
|
||||
<img src="/imagini/documente/dezvoltatori/extensii/aplicații/generator/document-document-record.png" alt="Înregistrare de documente generată" />
|
||||
</Frame>
|
||||
|
||||
## Previzualizați un document pe pagina sa de înregistrare
|
||||
|
||||
O componentă frontală nu este doar pentru meniurile de comenzi — poți canta una ca \*\*tab pe pagina de înregistrare
|
||||
\*\*. Hai să adăugăm o filă *Preview* la înregistrarea de document care face corpul
|
||||
Markdown ca o pagină șlefuită.
|
||||
|
||||
Componenta citește id-ul de înregistrare curent din contextul de execuție, încarcă documentul
|
||||
și îl redă. Componentele frontului rulează într-un **sandbox** care permite doar o albire
|
||||
a etichetelor HTML - injectarea brută HTML (`dangerouslySetInnerHTML`) și
|
||||
`\<style>` sunt blocate - așa că am redat Markdown ca elemente React cu linia
|
||||
stiluri mici [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
|
||||
hel.
|
||||
|
||||
```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ţi cu un [aspect de pagină](/l/ro/developers/extend/apps/layout/page-layouts). Un layout
|
||||
`RECORD_PAGE` adaugă file la vizualizarea unui obiect de înregistrare; un widget `FRONT_COMPONENT`
|
||||
într-o filă `CANVAS` găzduieşte componenta:
|
||||
|
||||
```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,
|
||||
},
|
||||
}],
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
Deschide orice document — o filă **Preview** o face frumoasă, cu link-uri către pagina web partajabilă
|
||||
și PDF:
|
||||
|
||||
<Frame caption="Tab-ul de Previzualizare face documentul cu stiluri inline, plus link-uri rapide.">
|
||||
<img src="/imagini/documente/dezvoltatori/extensii/aplicații/generator/09-document-viewer.png" alt="Vizualizator document componenta frontală într-o filă de înregistrare pagină" />
|
||||
</Frame>
|
||||
|
||||
## Editați un șablon cu editorul de text bogat
|
||||
|
||||
Șabloanele nu au nevoie deloc de o componentă personalizată. Deoarece `body` este un câmp
|
||||
`RICH_TEXT`, Douăzeci de ani oferă deja un editor complet de text pentru el — același
|
||||
cu Nota standard și obiectul de activitate. Îl înfățișăm doar pe pagina de înregistrare a șablonului
|
||||
|
||||
Adăugați o filă cu un widget `FIELD` în modul de afișare `EDITOR`, îndreptat în câmpul `body`
|
||||
prin `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 câmp `RICH_TEXT` stochează atât blocul editorului JSON cât și un proiecție Markdown
|
||||
. Conducta de generare citeşte că Markdown projection, deci
|
||||
placeholders, PDF, și pagina web partajabilă continuă să funcționeze neschimbat —
|
||||
vezi tot
|
||||
[`template-record. layout-ul vârstei`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
|
||||
Editorii scriu șabloane într-un adevărat editor de text bogat:
|
||||
|
||||
<Frame caption="Tab-ul modelului: Editorul nativ de text bogat al Twenty's legat de câmpul corpului.">
|
||||
<img src="/images/documente/dezvoltatori/extends/apps/document-generator/10-template-editor.png" alt="Șablon de înregistrare cu fila nativă de editor de text bogat" />
|
||||
</Frame>
|
||||
|
||||
**După acest pas:** documentele previzualizează frumos și șabloanele sunt editabile
|
||||
în aplicație. Apoi, lasă un agent AI să le genereze dintr-o conversație.
|
||||
|
||||
<Card title="Următorul: un agent AI →" icon="robot" href="/dezvoltator/extindere/aplicații/tutoriale/document-generator/ai-agent">
|
||||
Adăugați un agent și o abilitate care vă sună unelta.
|
||||
</Card>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: 1. Model de date
|
||||
icon: database
|
||||
description: Modelul de documente și șabloane cu obiecte, câmpuri și o relație.
|
||||
---
|
||||
|
||||
Aplicația noastră are nevoie de două obiecte personalizate: **șabloane de documente** (ce să scrie) și
|
||||
**documente** (rezultatul generat). Să le definim.
|
||||
|
||||
Scaffold fiecare fișier de entitate cu CLI - generează un UUID valid și dosarul corect
|
||||
pentru tine:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
```
|
||||
|
||||
Mai jos arătăm fișierele terminate.
|
||||
|
||||
<Note>
|
||||
Fiecare "\*_UNIVERSAL_IDENTIFIER" trăieşte în
|
||||
`src/constants/universal-identificfiers.ts` şi este importat unde este folosit. Fragmentele
|
||||
de mai jos omit acele importuri pentru încurcare - le păstrează-le în propriile fișiere.
|
||||
</Note>
|
||||
|
||||
## Obiectul şablonului
|
||||
|
||||
Un şablon are un `name`, un `body` cu `{{placeholders}}`, şi un `target` care spune
|
||||
dacă este scris pentru o Persoană sau o companie. `body` este un câmp
|
||||
`RICH_TEXT`, deci douăzeci îi dă un editor complet bogat.
|
||||
|
||||
```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>
|
||||
Opţiunea `SELECT` **valori** trebuie să fie `UPPER_CASE` (`PERSON", nu `persoană`), şi
|
||||
`valoarea implicită`este împachetată în ghilimele suplimentare: `` 'PERSON''.`label\` este ceea ce văd utilizatorii
|
||||
.
|
||||
</Warning>
|
||||
|
||||
## Obiectul documentului
|
||||
|
||||
Documentul generat stochează `conținutul` și `status`. Definiţi-l
|
||||
în acelaşi mod, cu `status` selectează din `DRAFT` / `GENERATED`. Fişier complet:
|
||||
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
|
||||
|
||||
## Conectarea lor cu o relație
|
||||
|
||||
Fiecare document ar trebui să indice modelul de la care a venit. Relațiile sunt
|
||||
**bidirecționale** — definiți ambele părți, fiecare în propriul fișier de câmp.
|
||||
|
||||
```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',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Cealaltă parte (`template-documents-relation.field.ts`) este un câmp
|
||||
`RelationType.ONE_TO_MANY` numit `documents` care arată invers.
|
||||
Vezi [Relations](/l/ro/developers/extend/apps/data/relations) pentru întregul model.
|
||||
|
||||
## Vezi în Douăzeci
|
||||
|
||||
Cu `yarn douăzeci de` rulează, deschide **Setări → Modelul de date**. Ambele obiecte
|
||||
apar, etichetate cu aplicația ta.
|
||||
|
||||
<Frame caption="Ambele obiecte personalizate, deținute de aplicația Document Generator.">
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/01-data-model.png" alt="Setările modelului de date care afișează documente și șabloane de documente" />
|
||||
</Frame>
|
||||
|
||||
Creați un șablon pentru a testa cu - denumiți *Propunere de vânzări*, setați **Ținta**
|
||||
*Person*, și lipiți un corp cu câțiva substituenți:
|
||||
|
||||
```text
|
||||
Dear {{name.firstName}} {{name.lastName}},
|
||||
|
||||
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
|
||||
|
||||
Best,
|
||||
The Team
|
||||
```
|
||||
|
||||
<Frame caption="O înregistrare șablon. Organismul îşi păstrează substituenţii până când se generează un document.">
|
||||
<img src="/imagini/documente/dezvoltatori/extensii/aplicații/generator document/03-template-record.png" alt="Un model de înregistrare a propunerii de vânzare cu organismul substituent" />
|
||||
</Frame>
|
||||
|
||||
**După acest pas:** aveţi obiecte `documentTemplate` şi `document`, legate de
|
||||
o relaţie, şi un şablon din care să se genereze. Apoi, logica care o umple.
|
||||
|
||||
<Card title="Următorul: generarea de documente →" icon="bolt" href="/dezvoltator/extindere/aplicații/tutoriale/generator documentare/generare-documente">
|
||||
Scrieți funcția logică care completează șablonul.
|
||||
</Card>
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
---
|
||||
title: 2. Generarea documentelor
|
||||
icon: bolt
|
||||
description: O funcţie logică, expusă ca instrument AI şi acţiune a fluxului de lucru.
|
||||
---
|
||||
|
||||
Acum, nucleul: o [funcţie logică](/l/ro/developers/extend/apps/logic/logic-functions)
|
||||
care încarcă un şablon şi o înregistrare, completează substituenţii şi salvează un nou document
|
||||
.
|
||||
|
||||
Vom scrie logica de afaceri o dată ca **manipulator**, apoi o vom expune prin
|
||||
mai mulţi declanşatori. Acest capitol le leagă pe două - un instrument **IA** și un
|
||||
**acțiune flux de lucru**.
|
||||
|
||||
## Ajutor de redare
|
||||
|
||||
Păstrați logica pură în propriul fișier, astfel încât să fie ușor de testat unitar. Acest lucru nivelează o înregistrare
|
||||
în `{{dot.path}}` îi jefuiește și le înlocuiește.
|
||||
|
||||
```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>
|
||||
Deoarece acest fișier nu are reacții adverse, îl puteți acoperi cu teste rapide de unitate
|
||||
('test yarn:unit'). Vezi [Testing](/l/ro/developers/extend/apps/operations/testing).
|
||||
</Tip>
|
||||
|
||||
## Gestionarul
|
||||
|
||||
Gestionarul folosește [`CoreApiClient`](/l/ro/developers/extend/apps/logic/logic-functions)
|
||||
pentru a citi și scrie date CRM. Încarcă șablonul, încarcă înregistrarea țintă, umple numărul
|
||||
și creează 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` rulează o interogare diferită pentru o Persoană vs. o companie și aplatizează
|
||||
rezultatul - vezi
|
||||
[`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).
|
||||
|
||||
## Expuneți-l ca instrument și acțiune de flux de lucru
|
||||
|
||||
Un singur `defineLogicFunction` poate purta mai multe declanşatoare. Aici, `toolTriggerSettings`
|
||||
îl face apelabil de către agenții AI, iar `workflowActionTriggerSettings` îl transformă într-un pas
|
||||
în constructorul fluxului de lucru vizual. Amândoi descriu datele lor cu ajutorul unei scheme 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,
|
||||
});
|
||||
```
|
||||
|
||||
Schema de introducere este o schemă JSON simplă care descrie `templateId` și `recordId` —
|
||||
vezi [`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).
|
||||
|
||||
## Acordă acces
|
||||
|
||||
Funcțiile logice funcționează ca rolul aplicației. Trebuie să citească șabloane și înregistrări
|
||||
și să creeze documente, așa că permiteți acest lucru în `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` lasă funcţia să încarce PDF-ul generat în secţiunea următoare.
|
||||
Vezi [Roles](/l/ro/developers/extend/apps/config/roles) pentru permisiunile cu bobul superior.
|
||||
|
||||
## Atașează un fișier PDF real
|
||||
|
||||
Un câmp text redat este util, dar utilizatorii doresc un document adevărat. Hai să generăm un
|
||||
**PDF** şi să-l stocăm în înregistrare ca un fişier ce poate fi descărcat.
|
||||
|
||||
În primul rând, daţi "document" obiectului `FILES` un câmp pentru a ţine fișierul PDF. Aplicațiile încarcă
|
||||
în câmpurile lor **proprii** de fișiere, astfel încât acest câmp este ruta încărcării:
|
||||
|
||||
```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 },
|
||||
}
|
||||
```
|
||||
|
||||
Acum redă acel PDF. O aplicație este un proiect node real, astfel încât puteți adăuga pachetul npm
|
||||
de care aveți nevoie și îl puteți importa ca oriunde altundeva. Folosim **[pdf-lib](https://pdf-lib.js.org/)**
|
||||
pentru a desena PDF şi **[marked](https://marked.js.org/)** pentru a analiza corpul Markdown- CLI le instalează în rularea funcţiei:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add pdf-lib marked
|
||||
```
|
||||
|
||||
Ajutorul complet este
|
||||
[`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).
|
||||
Analizează Markdown-ul în tokeni cu `marked.lexer`, apoi îl aranjează cu
|
||||
pdf-lib: titluri reale, pasaje **bold**/*italic*, liste cu buline și liste numerotate,
|
||||
blocuri de citat și linii — o redare A4 finisată, pe mai multe pagini, a șablonului
|
||||
în sine, mai degrabă decât un zid de text.
|
||||
|
||||
<Frame caption="PDF-ul generat: tipografie reală şi formatare Markdown, redând corpul şablonului.">
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/07b-generat-pdf.png" alt="Un PDF generat lustruit si vanzabil" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
fonturile încorporate ale pdf-lib-ului folosesc codificarea WinAnsi, astfel încât accentele europene occidentale dau
|
||||
din cutie; Hărțile ajutătoare nu pot codifica ghilimele, ghilimele și scadea cu caracterele
|
||||
. Redarea scripturilor non-latine (chineză, arabă, chirilică) ar însemna
|
||||
încorporarea unui font Unicode.
|
||||
</Note>
|
||||
|
||||
Apoi încărcați-l și stocați referința în înregistrare. `uploadFile` trasee bytes
|
||||
către câmpul de fișiere deținute de aplicație; `id` returnat este ceea ce salvați:
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Documentul generat poartă acum un PDF care poate fi descărcat:
|
||||
|
||||
<Frame caption="Fișierul PDF generat, stocat pe câmpul Fișier al documentului.">
|
||||
<img src="/imagini/documente/dezvoltatori/extensii/aplicații/document-generator/08-document-with-pdf.png" alt="O înregistrare document cu un fișier PDF generat" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
`uploadFile` țintește doar câmpurile fișierelor **app-owned** (astfel încât încărcările necesită întotdeauna o aplicație
|
||||
care deține câmpul, plus steagul de rol `UPLOAD_FILE`). Acesta este motivul pentru care PDF
|
||||
aterizează pe câmpul `fișier` al înregistrării - același model
|
||||
[aplicația pentru înregistrare](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
|
||||
folosit pentru înregistrări.
|
||||
</Note>
|
||||
|
||||
**După acest pas:** fiecare document generat are un PDF real, descărcabil. Dar
|
||||
nimic nu poate *apela* generatorul din interfață — pentru asta avem nevoie de un traseu HTTP.
|
||||
|
||||
<Card title="Următorul: Rute HTTP →" icon="glob" href="/dezvoltatori/extindere/aplicații/tutoriale/document-generator/http-routes">
|
||||
Serviți funcția de HTTP și redați documentele ca pagini web.
|
||||
</Card>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: 3. Rute HTTP
|
||||
icon: globe
|
||||
description: Declanșați funcția de HTTP și redați documentele ca pagini web.
|
||||
---
|
||||
|
||||
Același gestionar poate răspunde și la solicitările HTTP. Vom adăuga două rute:
|
||||
|
||||
* a **POST** final apeluri interfață pentru a genera un document, și
|
||||
* un obiectiv public **GET** care face un document ca o pagină web printabilă.
|
||||
|
||||
Ambele folosesc `httpRouteTriggerSettings`. Rutele aplicațiilor sunt servite sub `/s` pe
|
||||
Douăzeci de servere (ex. `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
## Ruta POST – generarea la cerere
|
||||
|
||||
Aceasta refolosește `generateDocumentHandler`, deci nu există logică de repetat — doar un adaptor subțire
|
||||
care citește corpul cererii.
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Managerul partajat returnează un cod `status` sugerat la eşec, astfel încât ruta poate să răspundă
|
||||
cu codul `4xx`/`5xx`. `isAuthRequired: true` înseamnă că apelantul
|
||||
trebuie să prezinte un token valid - componenta frontală din capitolul următor trece automat indicativul de acces al utilizatorului
|
||||
.
|
||||
|
||||
## Ruta GET - redare ca pagină web
|
||||
|
||||
Pentru a returna HTML în loc de JSON, înfăşuraţi corpul într-un `Răspuns` cu headerul
|
||||
`Content-Type`. Această rută este publică (`isAuthRequired: false`) astfel încât un document generat
|
||||
poate fi partajat ca un 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` redă corpul Markdown în HTML (cu [marked](https://marked.js.org/),
|
||||
sanitizat) şi îl introduce într-o curată, pagina printabilă care afișează doar conținutul șablonului- aceeași imagine ca PDF și previzualizarea in-app.
|
||||
[Vezi ajutorul](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
|
||||
|
||||
## Încearcă-l
|
||||
|
||||
Cu un șablon și o Persoană în spațiul de lucru, sunați ruta (apucați un token de la
|
||||
**Setări → API-uri & 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, ..."}
|
||||
```
|
||||
|
||||
Deschide documentul returnat în browser-ul tău:
|
||||
|
||||
```
|
||||
http://localhost:2020/s/documents/view?id=<documentId>
|
||||
```
|
||||
|
||||
<Frame caption="Ruta publică GET face ca documentul să fie o pagină imprimabilă.">
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/07-rendered-document.png" alt="O pagină web de document redată" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
De asemenea, poți viziona jurnalele unei funcții în timp ce testezi cu
|
||||
`yarn douăzeci dev:function:logs`, sau să o invoci direct cu
|
||||
`yarn douăzeci dev:function:exec`.
|
||||
</Tip>
|
||||
|
||||
**După acest pas:** aplicația poate genera documente prin HTTP și le poate servi ca
|
||||
pagini web. Acum hai să îl facem utilizabil fără 'curl'.
|
||||
|
||||
<Card title="Următorul: construirea interfeței →" icon="table-columns" href="/dezvoltatori/extindere/aplicații/tutoriale/document-generator/building-the-ui">
|
||||
Vizualizări, navigare, o comandă și o componentă frontală.
|
||||
</Card>
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: "Tutorial: Generator de documente"
|
||||
icon: wand-magic-sparkles
|
||||
description: Construiește o aplicație Twenty reală care generează documente personalizate din datele tale CRM.
|
||||
---
|
||||
|
||||
În acest tutorial vei construi **Document Generator** — o aplicație care transformă șabloanele reutilizabile
|
||||
în documente personalizate folosind datele deja existente în CRM-ul tău.
|
||||
|
||||
Scrie un șablon o singură dată cu `{{placeholders}}`, apoi generează un document completat
|
||||
pentru orice Persoană sau Companie cu un singur clic — din meniul de comenzi, de la un
|
||||
agent AI sau dintr-un flux de lucru.
|
||||
|
||||
<Frame caption="Un șablon, generat pentru o anumită persoană, deschis ca o pagină imprimabilă.">
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/07-rendered-document.png" alt="Un document al propunerii de vanzari generat" />
|
||||
</Frame>
|
||||
|
||||
## Ce vei învăța
|
||||
|
||||
Fiecare capitol adaugă o capacitate. Până la sfârşit veţi fi atins cea mai mare parte a SDK.
|
||||
|
||||
| Capitol | Capabilitate | Referință |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
|
||||
| [1. Model dată](/l/ro/developers/extend/apps/tutorials/document-generator/data-model) | Obiecte, câmpuri și o relație | [Data](/l/ro/developers/extend/apps/data/overview) |
|
||||
| [2. Generare documente](/l/ro/developers/extend/apps/tutorials/document-generator/generating-documents) | O funcție logică (unealta AI + acțiunea fluxului de lucru) care completează un șablon Markdown și atașează un PDF șablon șters | [Funcții logice](/l/ro/developers/extend/apps/logic/logic-functions) |
|
||||
| [3. Rute HTTP](/l/ro/developers/extend/apps/tutorials/document-generator/http-routes) | Se servește JSON și o pagină HTML partajabilă din rute | [Funcții logice](/l/ro/developers/extend/apps/logic/logic-functions) |
|
||||
| [4. Construirea UI](/l/ro/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vizualizări, navigare, meniu, și componentele frontale care previzualizează un document și editează un șablon | [Aspect](/l/ro/developers/extend/apps/layout/overview) |
|
||||
| [5. An AI agent](/l/ro/developers/extend/apps/tutorials/document-generator/ai-agent) | Agent + abilitate | [Competențe & agenții](/l/ro/developers/extend/apps/logic/skills-and-agents) |
|
||||
| [6. Publicare](/l/ro/developers/extend/apps/tutorials/document-generator/publishing) | Trimiteți-l la piață | [Publicare](/l/ro/developers/extend/apps/operations/publishing) |
|
||||
|
||||
## Cerințe
|
||||
|
||||
Trebuie să fi terminat [Quick Start](/l/ro/developers/extend/apps/getting-started/quick-start):
|
||||
un server local de douăzeci care rulează pe portul `2020` şi CLI autentificat la acesta.
|
||||
|
||||
Dacă nu, schildează şi începe acum unul:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest document-generator
|
||||
cd document-generator
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
<Note>
|
||||
Preferă să citești codul finalizat? Aplicația completă trăiește în
|
||||
[`pachete/douăzeci de aplicații/exemple/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
Fiecare snippet de mai jos este copiat din el.
|
||||
</Note>
|
||||
|
||||
## Cum se potrivește aplicația împreună
|
||||
|
||||
<Frame>
|
||||
<img src="/imagini/documente/dezvoltatori/extinderi/aplicații/generator/how-it-fits.svg" alt="Un model cu substituenți este generat într-un document șters cu un PDF, declanșat din meniul de comandă, un agent AI, un flux de lucru, sau un link partajabil" />
|
||||
</Frame>
|
||||
|
||||
Scrii un **șablon** o dată într-un editor bogat cu `{{placeholders}}`. Alegerea unui șablon
|
||||
și a unei înregistrări CRM completează locațiile și stochează un
|
||||
**document** (cu un fișier PDF). Toate celelalte - meniul de comandă, agentul AI,
|
||||
pasul fluxului de lucru, link-ul partajabil - este doar o modalitate diferită de a declanșa acel generator
|
||||
.
|
||||
|
||||
## Păstrează această buclă rulantă
|
||||
|
||||
Lăsați `yarn douăzeci de` să ruleze într-un terminal pentru întregul tutorial. De fiecare dată când
|
||||
adaugi sau editezi un fişier sub `src/`, el re-sincronizează pe serverul tău în câteva
|
||||
secunde, pentru ca fiecare capabilitate să apară în interfață pe măsură ce o construiești.
|
||||
|
||||
<Card title="Începe construirea →" icon="database" href="/dezvoltator/extensie/aplicații/tutoriale/document-generator/model de date">
|
||||
Capitolul 1: modele de documente și modele.
|
||||
</Card>
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: 6. Publicare
|
||||
icon: rocket
|
||||
description: Adăugați metadate de bazar și publicați aplicația dvs.
|
||||
---
|
||||
|
||||
Aplicația dvs. funcționează. Ultimul pas este să îl descriem pentru piață și publicare.
|
||||
|
||||
## Adaugă metadate marketplace
|
||||
|
||||
[application config](/l/ro/developers/extend/apps/config/application) posedă identitatea
|
||||
care apare în bazar: author, categorie, logo, and support
|
||||
link-uri. Puneți un logo în `public/` și trimiteți-l cu `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/ro/developers/extend/apps',
|
||||
termsUrl: 'https://www.twenty.com/terms',
|
||||
emailSupport: 'contact@twenty.com',
|
||||
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Rolul implicit este declarat cu `defineApplicationRole()` în propriul său fișier —
|
||||
nu mai pasa `defaultRoleUniversalIdentifier`.
|
||||
</Tip>
|
||||
|
||||
Adăugați de asemenea cuvântul cheie `douăzeci și douăzeci de aplicații` în `package.json` astfel încât aplicația să poată fi descoperită:
|
||||
|
||||
```json filename="package.json"
|
||||
{ "keywords": ["twenty-app"] }
|
||||
```
|
||||
|
||||
## Adaugă capturi de ecran galerie
|
||||
|
||||
O listă de marketplace se vinde cu capturi de ecran. Plasați câteva PNG-uri în
|
||||
`public/gallery/` și trimiteți-le cu `capturi de ecran` - ele se redau ca galerie
|
||||
pe pagina de listare.
|
||||
|
||||
```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>
|
||||
Plumb cu plata: fă prima captură de ecran rezultatul final (un document
|
||||
generat), apoi arată cum este declanșat și autentic. Utilizează capturi crisp, de înaltă rezoluție- ele sunt primul lucru pe care îl vede utilizatorul.
|
||||
</Tip>
|
||||
|
||||
Dă același tratament `README.md` — este prima pagină pe npm și GitHub.
|
||||
Deschideți cu o propunere de valoare și o captură de ecran, listați funcțiile titlului,
|
||||
și păstrați detaliile de construcție sub îndoit.
|
||||
|
||||
## Verifică înainte de livrare
|
||||
|
||||
Rulează aceleași porți CI face:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
```
|
||||
|
||||
Derularea uscată tipărește exact ce s-ar schimba pe server fără a o aplica —
|
||||
un bun control sanitar final. Vezi
|
||||
[Testing](/l/ro/developers/extend/apps/operations/testing) şi
|
||||
[Sincronizare şi Recuperare](/l/ro/developers/extend/apps/operations/sync-and-recovery).
|
||||
|
||||
## Publicare
|
||||
|
||||
```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` construiește și publică în npm în mod implicit; `--private` încarcă în schimb o tarball
|
||||
către un registru privat de douăzeci de servere. Pentru a întinde o aplicație publicată
|
||||
într-o piață de instanță, activați o sincronizare de catalog:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync -r <remote>
|
||||
```
|
||||
|
||||
Detalii complete şi lista de verificare a versiunii:
|
||||
[Publishing](/l/ro/developers/extend/apps/operations/publishing).
|
||||
|
||||
## Ai construit o aplicație 🎉
|
||||
|
||||
În şase capitole aţi folosit cea mai mare parte a suprafeţei SDK:
|
||||
|
||||
* **Obiecte, câmpuri și o relație** pentru a modela datele
|
||||
* O **funcţie logică** expusă ca o unealtă \*\*IA, o **acţiune de flux de lucru**, şi **rute HTTP**
|
||||
* **Vezi, navigare, o comandă și o componentă față** pentru UI
|
||||
* Un **agent + abilitate** pentru generarea limbajului natural
|
||||
* \*\*metadate Marketplace \*\* și fluxul de publicare
|
||||
|
||||
Aplicația finalizată este la
|
||||
[`pachete/douăzeci de aplicații/exemple/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
|
||||
## Unde să mergi mai departe
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Referință date" icon="database" href="/l/ro/developers/extend/apps/data/overview">
|
||||
Fiecare tip de câmp, relatie si optiune indice.
|
||||
</Card>
|
||||
<Card title="Referință logică" icon="bolt" href="/l/ro/developers/extend/apps/logic/overview">
|
||||
Cron and database-event triggers, the key-value store, OAuth connections.
|
||||
</Card>
|
||||
<Card title="Referință schemă" icon="table-columns" href="/l/ro/developers/extend/apps/layout/overview">
|
||||
Aspectele paginii, widget-urile tabloului de bord și mai multe suprafețe ale interfeței.
|
||||
</Card>
|
||||
<Card title="Operațiuni" icon="rocket" href="/l/ro/developers/extend/apps/operations/overview">
|
||||
CLI, testare, îndepărtări și CI.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user