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:
github-actions[bot]
2026-07-07 11:54:54 +02:00
committed by GitHub
parent 07a921f8ca
commit 18ca89bcdd
97 changed files with 13247 additions and 0 deletions
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>