snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
---
title: Configurația aplicației
description: Declarați identitatea aplicației, rolul implicit, variabilele și metadatele din marketplace cu `defineApplication`.
icon: rocket
---
Fiecare aplicație trebuie să aibă exact un apel `defineApplication`. Acesta declară:
* **Identitate** — identificator universal, nume de afișare, descriere.
* **Permisiuni** — sub ce rol rulează funcțiile logice și componentele front-end ale acesteia.
* **Variabile** *(opțional)* — perechi cheie–valoare expuse codului dvs. ca variabile de mediu.
* **Hook-uri de pre-instalare / post-instalare / dezinstalare** *(opțional)* — vedeți [Funcții logice](/l/ro/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
Notițe:
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți. Generați-le o singură dată și mențineți-le stabile între sincronizări.
* `applicationVariables` devin variabile de mediu pentru funcțiile și componentele front-end. În funcțiile de logică (server-side), acestea sunt disponibile ca `process.env.VARIABLE_NAME`. În componentele front-end, folosește `getApplicationVariable('VARIABLE_NAME')` din `twenty-sdk/front-component`. Variabilele marcate cu `isSecret: true` sunt injectate doar în funcțiile de logică. Componentele front-end primesc doar variabile non-secrete.
* Rolul implicit este detectat automat din fișierul de rol marcat cu [`defineApplicationRole()`](/l/ro/developers/extend/apps/config/roles) — nu este necesar să faci referire la el în `defineApplication()`.
* Funcțiile de pre-instalare, post-instalare și dezinstalare sunt detectate automat în timpul construirii manifestului — nu este nevoie să faceți referire la ele în `defineApplication()`.
* Transmiterea explicită a `defaultRoleUniversalIdentifier` este în continuare acceptată pentru compatibilitate retroactivă, dar este considerată învechită în favoarea `defineApplicationRole()`.
* `serverVariables` sunt configurări și secrete la nivel de instanță (de ex. chei API). Spre deosebire de `applicationVariables`, ele nu declară nicio valoare în manifest — operatorul spațiului de lucru le completează din setările aplicației și sunt injectate în funcțiile de logică doar după ce au fost setate.
* Ambele tipuri de variabile acceptă `isDeprecated: true`. Folosește-l pentru a retrage o variabilă în loc să o ștergi: păstrarea cheii declarate conservă valoarea stocată (ștergerea ei distruge valoarea introdusă de operator), iar variabila este în continuare injectată, astfel încât codul tău poate reveni la ea — `process.env.NEW_API_KEY ?? process.env.API_KEY`. O variabilă învechită dispare din setările aplicației odată ce nu mai are nicio valoare și nu este niciodată luată în calcul la verificarea configurației aplicației, astfel încât `isDeprecated` are prioritate față de `isRequired`.
* Pentru a afișa o interfață de configurare personalizată în fila **Settings** a aplicației (în locul secțiunii implicite de configurare a variabilelor), declară un front component cu [`defineSettingsFrontComponent()`](/l/ro/developers/extend/apps/layout/front-components#custom-settings-component) într-un fișier separat. Este permis doar unul per aplicație. Secțiunile gestionate de sistem (auto-upgrade, App URL, connections) rămân întotdeauna vizibile.
## Tipuri de variabile
Atât `applicationVariables`, cât și `serverVariables` acceptă un câmp opțional `type` (și, pentru `SELECT` / `MULTI_SELECT`, o listă `options`). Tipuri acceptate: `TEXT` (implicit), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
Proprietatea `type` afectează doar **prezentarea și validarea** — selectează câmpul de intrare corespunzător în interfața de setări a spațiului de lucru (un comutator, câmp numeric, listă derulantă, selector de dată, editor JSON, …) și permite build‑ului să valideze configurația (de exemplu, `SELECT` / `MULTI_SELECT` trebuie să declare o listă `options` negoală). Nu modifică **deloc** modul în care valoarea ajunge în codul tău.
Valorile sunt **întotdeauna injectate ca stringuri** — acest lucru este inerent pentru variabilele de mediu (`process.env.*` este doar string). Când rulează funcția ta de logică, executorul serializează fiecare valoare în funcție de `type`‑ul declarat în timp ce construiește `process.env`, astfel încât formatul stringului este consecvent indiferent de modul în care a fost setată valoarea (valoare implicită din manifest, interfața de setări sau o versiune anterioară):
| Tip | string `process.env` |
| ------------------------------------- | --------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | valoarea brută (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | string zecimal (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | array JSON (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | obiect JSON (`'{"retries":3}'`) |
Parsează stringul înapoi în tipul pe care îl aștepți:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
Același lucru se aplică și componentelor de interfață care citesc valori prin `getApplicationVariable('VARIABLE_NAME')` — valoarea returnată este un string; parseaz-o după cum este necesar.
## Rol implicit pentru funcții
Rolul declarat cu [`defineApplicationRole()`](/l/ro/developers/extend/apps/config/roles) controlează la ce pot avea acces funcțiile logice și componentele de interfață ale aplicației:
* Tokenurile de runtime injectate în funcțiile dvs. de logică sunt derivate din acest rol. Un apel care acționează ca o persoană este restrâns suplimentar la ceea ce poate face acea persoană, astfel încât nu poate depăși niciodată niciuna dintre cele două limite. Consultați [Ce acces utilizează un apel](/l/ro/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* Clientul API tipizat este restricționat la permisiunile acordate acelui rol.
* Respectați principiul celui mai mic privilegiu: declarați doar permisiunile de care au nevoie funcțiile.
Când generați o aplicație nouă, CLI creează un fișier de rol de pornire la `src/roles/default-role.ts`. Consultați [Roluri și permisiuni](/l/ro/developers/extend/apps/config/roles) pentru referința completă.
## Metadate pentru marketplace
Dacă intenționați să [publicați aplicația](/l/ro/developers/extend/apps/operations/publishing), aceste câmpuri opționale controlează modul în care apare în marketplace:
| Câmp | Descriere |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `author` | Numele autorului sau al companiei |
| `category` | Categoria aplicației pentru filtrarea în marketplace |
| `logo` | Calea către logo-ul aplicației tale inclus în `public/` (de ex., `public/logo.png`) |
| `galleryImages` | Array de căi către imaginile din galerie incluse în `public/` (de ex., `public/screenshot-1.png`) |
| `aboutDescription` | Descriere markdown mai lungă pentru fila "About". Dacă este omis, marketplace-ul folosește `README.md` al pachetului de pe npm |
| `websiteUrl` | Link către site-ul dvs. |
| `termsUrl` | Link către termenii de serviciu |
| `emailSupport` | Adresă de e-mail pentru suport |
| `issueReportUrl` | Link către sistemul de urmărire a problemelor |
<Note>
`logoUrl` și `screenshots` sunt aliasuri învechite pentru `logo` și `galleryImages`. URL-urile absolute externe (`http://` sau `https://`) nu sunt acceptate pentru aceste câmpuri: ele sunt eliminate cu un avertisment în timpul build-ului. În schimb, include imaginile în folderul `public/` al aplicației tale.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Hook-uri de instalare
description: Rulați logică în timpul ciclului de viață de instalare, actualizare sau dezinstalare — populați date, faceți backup pentru înregistrări, validați actualizarea, curățați resursele externe.
icon: wrench
---
Hook-urile de instalare sunt funcții logice speciale care rulează în timpul ciclului de viață de instalare, actualizare sau dezinstalare. Acestea folosesc același runtime de handler ca și [funcțiile logice](/l/ro/developers/extend/apps/logic/logic-functions) obișnuite, dar sunt declarate cu propriile lor funcții de definire și există în afara modelului obișnuit de declanșatori (HTTP, cron, evenimente de bază de date). Hook-urile de instalare primesc un `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` este `undefined` la o instalare nouă); hook-ul de dezinstalare primește un `UninstallPayload` (`{ version?: string }` — versiunea care este eliminată).
Fiecare aplicație poate defini **cel mult unul** din fiecare hook (pre-instalare, post-instalare, dezinstalare). Construirea manifestului va genera o eroare dacă se detectează mai mult de unul de orice tip.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## Dintr-o privire
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ---------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Rulări | Înainte de migrarea metadatelor — schema și datele **anterioare** sunt încă intacte | După migrare și generarea SDK — schema **nouă** este aplicată |
| Execuție | Întotdeauna sincronă; blochează instalarea | Async în mod implicit (pus în coadă, 3 reîncercări); execuție sincronă opțională prin `shouldRunSynchronously: true` |
| La eșec | Instalarea este **întreruptă** înainte de orice modificare a schemei | Async: reîncercată de până la 3 ori. Sync: apelantul primește `POST_INSTALL_ERROR` (modificările de schemă **nu** sunt anulate) |
| Utilizare tipică | Faceți backup sau reparați date pe care o migrare le-ar pierde; refuzați un upgrade riscant aruncând o eroare | Populați date implicite, configurați workspace-ul, înregistrați resurse externe |
**Regulă generală:** folosiți implicit post-install. Apelați la pre-install doar când migrarea în sine este distructivă și trebuie să interceptați starea anterioară înainte să dispară.
| Doriți să... | Folosiți |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Populați date, configurați workspace-ul, înregistrați resurse externe | `post-install` |
| Muncă de durată care nu ar trebui să blocheze răspunsul la instalare | `post-install` (mod async implicit, cu reîncercări ale workerului) |
| Configurare rapidă de care apelantul are nevoie imediat după ce instalarea se încheie | `post-install` cu `shouldRunSynchronously: true` |
| Citiți sau faceți backup datelor pe care migrarea iminentă le-ar pierde | `pre-install` |
| Respingeți o actualizare care ar corupe datele existente | `pre-install` (aruncați din handler) |
| Reconciliere la fiecare upgrade | Oricare hook cu `shouldRunOnVersionUpgrade: true` |
## Comportament partajat de ambele hook-uri
* Configurația este o configurație `defineLogicFunction` minus setările de declanșare, plus `shouldRunOnVersionUpgrade`.
* **Când rulează**: doar la instalări noi, în mod implicit. Setați `shouldRunOnVersionUpgrade: true` pentru a rula și la upgrade-uri. Folosiți `previousVersion` / `newVersion` pentru a ramifica în funcție de calea de upgrade.
* **Idempotența contează**: post-install asincron poate fi reîncercat, iar oricare hook rulează din nou la upgrade-uri când `shouldRunOnVersionUpgrade` este activat.
* Mediul obișnuit al funcțiilor logice (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) este injectat, astfel încât puteți apela API-ul Twenty cu tokenul aplicației voastre.
* Hook-ul este atașat automat la manifestul aplicației la build (`preInstallLogicFunction` / `postInstallLogicFunction`) — nu este nevoie să fie referențiat în [`defineApplication()`](/l/ro/developers/extend/apps/config/application).
* Valoarea implicită pentru `timeoutSeconds` este 300 pentru a permite sarcini de configurare mai lungi, cum ar fi popularea datelor.
* **Nu este executat în modul dev**: `yarn twenty dev` sare peste fluxul de instalare și sincronizează fișierele direct, astfel încât hook-urile nu rulează acolo. Declanșați-le manual în schimb:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="Rulează după ce migrarea metadatelor workspace-ului este aplicată">
Rulează după ce aplicația voastră a terminat instalarea: metadate sincronizate, clientul SDK generat, noua schemă poate fi interogată. Exemplu — populează o înregistrare implicită la instalări noi:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Flag-ul `shouldRunSynchronously` controlează modelul de execuție:
* `false` *(implicit)* — pus în coada de mesaje (`retryLimit: 3`) și rulat de un worker. Răspunsul la instalare este returnat imediat ce jobul este pus în coadă. **Folosiți pentru muncă de durată** — popularea unor seturi mari de date, API-uri lente ale terților.
* `true` — executat inline în timpul fluxului de instalare. Cererea de instalare este blocată până când handlerul se termină; o eroare aruncată este expusă apelantului ca `POST_INSTALL_ERROR` (fără reîncercări). **Folosiți pentru muncă rapidă, care trebuie să fie finalizată înainte de răspuns.** Migrarea a fost deja aplicată în acest punct, astfel încât un eșec nu anulează modificările de schemă — doar expune eroarea.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Rulează înainte ca migrarea metadatelor workspace-ului să fie aplicată">
Rulează înainte de migrarea metadatelor, pe schema **anterioară** — locul potrivit pentru a face backup datelor pe care o migrare le-ar pierde sau pentru a refuza un upgrade riscant. Înainte de execuție, serverul rulează un „sync redus”, pur aditiv, care înregistrează doar funcția de pre-instalare a versiunii noi; tot restul — obiectele, câmpurile și datele versiunii anterioare — rămâne neatins atunci când rulează handlerul.
Pre-install este întotdeauna **sincron** și blochează instalarea. Dacă handlerul aruncă o eroare, instalarea este întreruptă înainte de orice modificare a schemei — workspace-ul rămâne la versiunea anterioară într-o stare consistentă. Acest lucru este intenționat: pre-install este ultima dvs. șansă de a refuza o actualizare riscantă.
Exemplu — copiați valorile unui câmp vechi înainte ca migrarea să îl elimine:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## Hook de dezinstalare
`defineUninstallLogicFunction` declară un hook care rulează atunci când un utilizator dezinstalează aplicația ta. Acesta este executat **înainte** ca metadatele, datele și codul aplicației să fie eliminate — odată ce migrarea de ștergere rulează, nu mai rămâne nimic de executat — astfel încât handler-ul tău poate încă interoga obiectele și înregistrările aplicației. Folosește-l pentru curățarea resurselor externe: deprovisionarea resurselor API, ștergerea boților rămași, revocarea webhook-urilor.
Notițe:
* Hook-ul funcționează după principiul „best-effort“: rulează sincron, dar o eroare este înregistrată și **nu blochează niciodată dezinstalarea** — curățarea nu trebuie să facă imposibilă eliminarea unei aplicații.
* Acesta primește `UninstallPayload` (`{ version?: string }` — versiunea care este eliminată).
* Nu rulează atunci când o instalare nouă eșuată este anulată — aplicația nu a terminat niciodată instalarea.
* Hook-ul nu poate rula după ce aplicația a dispărut, astfel încât curățarea externă care depinde de datele aplicației (de ex. ID-uri de boți stocate în înregistrări) trebuie făcută aici, nu într-un job programat extern.
* La fel ca hook-urile de instalare, **nu este executat în modul de dezvoltare (dev mode)** — declanșează-l manual în schimb:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: Prezentare generală
description: Configurează însăși aplicația — identitatea ei, permisiunile implicite și ceea ce rulează în momentul instalării.
icon: screwdriver-wrench
---
**Stratul de configurare** al unei aplicații Twenty este cel care descrie aplicația *pentru platformă* — identitatea ei, permisiunile pe care le deține și codul care rulează în timpul instalării sau actualizării. Aceste declarații nu adaugă noi structuri de date sau comportamente la rulare; ele îi spun lui Twenty *cine este aplicația* și *cum să fie configurată*.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## În această secțiune
<CardGroup cols={2}>
<Card title="Configurația aplicației" icon="rocket" href="/l/ro/developers/extend/apps/config/application">
`defineApplication` — identitate, rol implicit, variabile, metadate pentru marketplace.
</Card>
<Card title="Roluri și permisiuni" icon="shield-halved" href="/l/ro/developers/extend/apps/config/roles">
`defineRole` — declară ce pot citi și scrie funcțiile de logică ale aplicației dvs.
</Card>
<Card title="Hook-uri de instalare" icon="wrench" href="/l/ro/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction` și `defineUninstallLogicFunction` — fac backup la date, introduc valori implicite, validează actualizările și curăță la eliminare.
</Card>
</CardGroup>
## Cum se leagă componentele între ele
* **Aplicația** este punctul de intrare. Fiecare aplicație are exact un apel `defineApplication()`, iar acesta indică un singur **Rol** ca implicit.
* **Rolul** controlează ce pot citi și scrie funcțiile de logică și componentele front-end ale aplicației. Respectați principiul celui mai mic privilegiu: acordați doar permisiunile de care codul dvs. are cu adevărat nevoie.
* **Hook-urile de instalare** rulează în timpul instalării sau actualizării — pre-install înainte de migrarea metadatelor (astfel încât să poată refuza o actualizare riscantă), post-install după migrare (astfel încât să poată introduce date implicite în noua schemă). Hook-ul de dezinstalare rulează chiar înainte ca aplicația să fie eliminată, astfel încât poate curăța resursele externe cât timp datele aplicației sunt încă lizibile.
<Note>
Hook-urile de instalare împart același runtime cu [funcțiile de logică](/l/ro/developers/extend/apps/logic/logic-functions) — aceeași semnătură a handlerului, aceleași variabile de mediu, același client API tipizat — dar sunt declarate cu propriile lor funcții `define` și există în afara modelului obișnuit de declanșare (HTTP, cron, evenimente de bază de date).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: Resurse publice
description: Livrați fișiere statice — imagini, pictograme, fonturi — împreună cu aplicația dvs. prin folderul public/.
icon: folder-open
---
Folderul `public/` din rădăcina aplicației conține fișiere statice — imagini, pictograme, fonturi sau orice alte resurse de care are nevoie aplicația la rulare. Aceste fișiere sunt incluse automat în build-uri, sincronizate în timpul modului de dezvoltare și încărcate pe server.
Fișierele plasate în `public/` sunt:
* **Accesibile public** — odată sincronizate pe server, resursele sunt servite la un URL public. Nu este necesară autentificarea pentru a le accesa.
* **Disponibile în componentele frontend** — folosiți URL-urile resurselor pentru a afișa imagini, pictograme sau orice media în componentele React.
* **Disponibile în funcțiile logice** — referiți URL-urile resurselor în e-mailuri, răspunsuri API sau orice logică pe server.
* **Utilizate pentru metadatele marketplace-ului** — câmpurile `logo` și `galleryImages` din `defineApplication()` fac referire la fișiere din acest folder (de ex., `public/logo.png`). Acestea sunt afișate în marketplace când aplicația este publicată. URL-urile absolute externe sunt ignorate pentru aceste câmpuri — încorporează imaginile în `public/` în schimb. Fiecare imagine nu trebuie să depășească 10 MB.
* **Sincronizate automat în modul de dezvoltare** — când adăugați, actualizați sau ștergeți un fișier în `public/`, acesta este sincronizat automat cu serverul. Nu este nevoie de repornire.
* **Incluse în build-uri** — `yarn twenty dev:build` împachetează toate resursele publice în outputul de distribuție.
## Accesarea resurselor publice cu `getPublicAssetUrl`
Utilizați helperul `getPublicAssetUrl` din `twenty-sdk` pentru a obține URL-ul complet al unui fișier din directorul `public/`. Funcționează atât în funcții logice, cât și în componente frontend.
**Într-o funcție logică:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**Într-o componentă frontend:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
Argumentul `path` este relativ la folderul `public/` al aplicației. Atât `getPublicAssetUrl('logo.png')`, cât și `getPublicAssetUrl('public/logo.png')` se rezolvă la același URL — prefixul `public/` este eliminat automat dacă este prezent.

View File

@@ -0,0 +1,212 @@
---
title: Roluri și permisiuni
description: Declarați ce obiecte și câmpuri pot citi și scrie funcțiile de logică și componentele front-end ale aplicației dvs.
icon: shield-halved
---
Un **rol** este un set de permisiuni: ce obiecte poate citi sau scrie o aplicație, ce câmpuri poate vedea și ce capabilități la nivel de platformă poate folosi. Funcțiile de logică și componentele front-end ale fiecărei aplicații moștenesc permisiunile rolului marcat cu `defineApplicationRole()` (consultați [Rolul implicit pentru funcții](#the-default-function-role) mai jos).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## Securitate la nivel de rând
Permisiunile pentru obiecte și câmpuri decid *ce obiecte și câmpuri* poate accesa un rol. **Predicate de
permisiuni la nivel de rând** merg mai departe și decid *ce înregistrări* poate vedea și asupra cărora poate acționa un rol — de
exemplu, un rol de tip autoservire în care fiecare utilizator extern își vede doar propriile înregistrări.
Declară predicate cu `rowLevelPermissionPredicates` în rol. La fel ca restul manifestului,
fiecare predicat are propriul `universalIdentifier` și face referire la un obiect și un câmp prin
`universalIdentifier`, un `operand` și (opțional) un câmp workspaceMember a cărui valoare este injectată
în momentul interogării — astfel încât poți exprima „relația de proprietar a înregistrării **este** membrul curent al spațiului de lucru”:
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
Deoarece predicatele sunt livrate în manifest, ele sunt create, actualizate și eliminate împreună cu
rolul la fiecare instalare și actualizare — nu există un pas separat post-instalare pentru a le menține sincronizate.
### Combinarea predicatelor cu grupuri
În mod implicit, predicatele unui rol sunt combinate cu `AND`. Pentru a combina unele dintre ele cu `OR` (sau pentru a
imbrica logica), declară o intrare `rowLevelPermissionPredicateGroups` și asociază fiecare predicat la aceasta prin
`predicateGroupUniversalIdentifier`. Acest rol permite unui partener să vadă o oportunitate pe care fie o **deține**,
fie pentru care este **punctul de contact**:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
Notițe:
* Oferă fiecărui predicat și fiecărui grup un `universalIdentifier` stabil (orice uuid) — acesta este cheia entității
de-a lungul actualizărilor, iar predicatele fac referire la grupuri prin el.
* Predicatele pot face referire la obiecte și câmpuri deținute de aplicația ta sau de obiectele standard ale Twenty.
* Securitatea la nivel de rând este aplicată pentru spațiile de lucru din planurile care o includ; predicatele se sincronizează în continuare
pe alte planuri, doar că nu sunt aplicate.
## Rolul implicit pentru funcții
Când generați o aplicație nouă, CLI creează un fișier de rol implicit declarat cu `defineApplicationRole()`:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` este un wrapper subțire în jurul `defineRole()` care marchează rolul utilizat ca implicit pentru aplicația dvs. în momentul instalării. Validarea este identică cu `defineRole`, dar pipeline-ul de build conectează automat `universalIdentifier` în `defaultRoleUniversalIdentifier` din manifestul aplicației — astfel încât nu trebuie să îl referiți manual din [`defineApplication`](/l/ro/developers/extend/apps/config/application).
Notițe:
* Exact **un** `defineApplicationRole(...)` este permis per aplicație — build-ul manifestului va eșua dacă găsește mai mult de unul.
* Utilizați `defineRole()` (nu `defineApplicationRole()`) pentru orice roluri **suplimentare** cu care este livrată aplicația.
* Setarea explicită a `defaultRoleUniversalIdentifier` în `defineApplication()` este în continuare acceptată pentru compatibilitate retroactivă, dar este considerată învechită în favoarea `defineApplicationRole()`.
## Cele mai bune practici
* Porniți de la rolul generat de scaffolding, apoi restrângeți-l progresiv — rolul implicit acordă acces larg la citire, ceea ce rareori este de dorit în producție.
* Înlocuiți `objectPermissions` și `fieldPermissions` cu obiectele și câmpurile de care au nevoie efectiv funcțiile dvs.
* `permissionFlagUniversalIdentifiers` controlează accesul la capabilități la nivelul platformei. Mențineți-le la minimum.
* Vedeți un exemplu funcțional: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).

View File

@@ -0,0 +1,52 @@
---
title: Extinderea obiectelor
description: Adăugați câmpuri la obiectele standard Twenty (Person, Company, …) sau la obiecte din alte aplicații folosind defineField.
icon: wand-magic-sparkles
---
Folosiți `defineField()` pentru a adăuga un câmp la un obiect pe care nu îl dețineți — un obiect standard Twenty precum Person sau Company, sau un obiect livrat de o altă aplicație instalată. Spre deosebire de câmpurile inline declarate în interiorul [`defineObject`](/l/ro/developers/extend/apps/data/objects), câmpurile independente necesită un `objectUniversalIdentifier` pentru a specifica obiectul pe care îl extind.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
## Puncte cheie
* `objectUniversalIdentifier` identifică obiectul țintă. Pentru obiectele standard Twenty, importați constanta din `twenty-sdk`:
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* Atunci când definiți câmpuri **inline în `defineObject()`**, **nu** aveți nevoie de `objectUniversalIdentifier` — acesta este moștenit de la obiectul părinte.
* `defineField()` este singura modalitate de a adăuga câmpuri la obiecte pe care nu le-ați creat cu `defineObject()`.
* Locația fișierului depinde de dumneavoastră. Convenția este `src/fields/\<name>.field.ts`, dar SDK-ul detectează câmpuri oriunde în `src/`.
* `writability` poate fi de asemenea setată per câmp: `MetadataWritability.APPLICATION` restricționează scrierile la propriile funcții logice ale aplicației tale, `MetadataWritability.SYSTEM` este rezervat pentru date administrate de platformă. Capacitatea de scriere a unui câmp poate fi doar mai strictă decât nivelul obiectului său, iar citirile nu sunt afectate. Vezi [Objects](/l/ro/developers/extend/apps/data/objects) pentru semantica completă.
* Pentru a adăuga o filă într-un layout standard de pagină (de ex. pagina de detalii pentru Task sau Company), folosește [`definePageLayoutTab`](/l/ro/developers/extend/apps/layout/page-layouts#definepagelayouttab) cu `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` din `twenty-sdk/define`.
## Adăugarea unei relații la un obiect existent
Pentru a adăuga un câmp de tip relație (de ex. pentru a lega obiectul personalizat de un `Person` standard), folosiți `defineField()` cu `FieldType.RELATION`. Modelul este același ca pentru relațiile inline, dar cu `objectUniversalIdentifier` setat explicit. Consultați [Relații](/l/ro/developers/extend/apps/data/relations) pentru modelul bidirecțional.

View File

@@ -0,0 +1,143 @@
---
title: Obiecte
description: Declarați noi tipuri de înregistrări — tabele personalizate cu propriile câmpuri — folosind defineObject.
icon: table
---
**Obiectele** personalizate sunt tipuri noi de înregistrări pe care aplicația dvs. le adaugă într-un spațiu de lucru — carte poștală, factură, abonament, orice este specific domeniului dvs. Fiecare obiect își declară propria schemă (câmpuri, relații, valori implicite) și un identificator universal stabil, care este păstrat între sincronizări și implementări.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
## Puncte cheie
* `universalIdentifier` trebuie să fie unic și stabil între implementări.
* Fiecare câmp necesită un `name`, un `type`, un `label` și propriul `universalIdentifier` stabil.
* Tabloul `fields` este opțional — puteți defini obiecte fără câmpuri personalizate.
* `openRecordIn` stabilește unde se deschid înregistrările acestui obiect când se face clic pe ele: `ObjectOpenRecordIn.USER_CHOICE` (valoarea implicită, care respectă preferința fiecărui membru al spațiului de lucru din Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` sau `ObjectOpenRecordIn.RECORD_PAGE`. Fixează-l la `RECORD_PAGE` pentru înregistrările care au nevoie de o pagină completă pentru a fi utilizabile, așa cum este cazul fluxurilor de lucru și al dashboard-urilor, sau la `SIDE_PANEL` pentru înregistrările care au sens doar ca un panou rapid, așa cum este cazul evenimentelor din calendar.
* `writability` controlează cine poate scrie înregistrări ale obiectului, înainte ca permisiunile pe roluri să se aplice: `MetadataWritability.OPEN` (valoarea implicită — rolurile din spațiul de lucru decid), `MetadataWritability.APPLICATION` (numai funcțiile de logică ale aplicației tale pot crea, actualiza sau șterge înregistrări; folosește acest lucru pentru obiecte de tip configurație ale căror înregistrări determină comportamentul, astfel încât membrii spațiului de lucru cu acces extins la înregistrări să nu le poată edita prin API) sau `MetadataWritability.SYSTEM` (rezervat pentru date administrate de platformă). Operațiunile de citire nu sunt afectate — acest lucru este impus la nivel de server, spre deosebire de `isUIEditable`, care doar ascunde elementele din interfața cu utilizatorul. Se aplică și la nivel de câmp, unde poate fi doar mai restrictivă decât la nivelul obiectului.
* Câmpurile inline definite aici **nu** au nevoie de `objectUniversalIdentifier` — este moștenit de la obiectul părinte. Folosiți [`defineField()`](/l/ro/developers/extend/apps/data/extending-objects) pentru a adăuga câmpuri la obiecte care nu vă aparțin.
* Puteți genera obiecte noi cu `yarn twenty dev:add object`, care vă ghidează prin denumire, câmpuri și relații. Consultați [Arhitectură → Generarea entităților](/l/ro/developers/extend/apps/getting-started/scaffolding).
<Note>
**Câmpurile de bază sunt adăugate automat.** Când definiți un obiect personalizat, Twenty creează pentru dvs. câmpuri standard precum `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` și `deletedAt`. Nu trebuie să le declarați în tabloul `fields` — doar câmpurile dvs. personalizate. Puteți suprascrie un câmp implicit declarând unul cu același nume, dar acest lucru este rareori o idee bună.
</Note>
## Tipuri de câmpuri
Setul complet de valori `FieldType`, exportate din `twenty-sdk/define`:
| Categorie | Tipuri |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Text | `TEXT`, `RICH_TEXT`, `ARRAY` (de șiruri), `RAW_JSON` |
| Numerice | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (precizie arbitrară), `RATING`, `POSITION` |
| Date calendaristice | `DATE`, `DATE_TIME` |
| Alegere | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Compuse | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identificatori și relații | `UUID`, `RELATION`, `MORPH_RELATION` (vezi [Relații](/l/ro/developers/extend/apps/data/relations)) |
| Sistem | `TS_VECTOR` (vector de căutare full-text, gestionat de server) |
Tipurile compuse stochează mai multe sub‑câmpuri (de ex. `FULL_NAME` = prenume + nume de familie; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` și `MULTI_SELECT` necesită un tablou `options`, ca în exemplul de mai sus.
## Valori implicite
Valorile implicite de tip șir literal trebuie să fie încadrate în ghilimele simple **în interiorul** șirului — `defaultValue: "'Draft'"`, nu `defaultValue: "Draft"`. De aceea câmpul `status` de mai sus folosește `` `'${PostCardStatus.DRAFT}'` ``.
Șirurile fără ghilimele sunt rezervate pentru valorile implicite calculate, evaluate atunci când este creată o înregistrare:
* `'uuid'` — generează un UUID (pentru câmpurile `UUID`)
* `'now'` — marcajul temporal curent (pentru câmpurile `DATE_TIME`)
Aceeași convenție se aplică și subcâmpurilor de tip șir de caractere ale valorilor implicite compuse (de ex. `{ source: "'MANUAL'" }` pe un câmp `ACTOR`) și valorilor `SELECT`/`MULTI_SELECT`. O valoare implicită literală de tip șir de caractere lăsată neîncadrată în ghilimele generează un avertisment atunci când aplicația este construită.
## Acceptarea valorii NULL
`isNullable` controlează dacă un câmp acceptă `NULL`. Valoarea sa implicită este `true` — omiteți-l pentru câmpurile opționale. Setați `isNullable: false` pentru a face un câmp obligatoriu la nivel de bază de date.
Modificările aduse proprietății `isNullable` sunt aplicate la fiecare sincronizare, inclusiv la sincronizările care actualizează un câmp existent — astfel încât puteți schimba nullabilitatea unui câmp editând manifestul și resincronizând.
<Note>
**Transformarea unui câmp existent în non-nullable necesită o valoare implicită.** Atunci când modificați un câmp la `isNullable: false`, trebuie să furnizați și un `defaultValue` nenul. Valoarea implicită completează retroactiv orice rânduri `NULL` existente înainte ca restricția `NOT NULL` să fie aplicată; fără aceasta, sincronizarea eșuează cu mesajul `Default value cannot be null for non-nullable fields`. Câmpurile de relație și câmpurile `TS_VECTOR` sunt întotdeauna nullable, astfel încât `isNullable` nu are niciun efect asupra lor.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## Ce urmează
* **Conectați acest obiect la altele** — consultați [Relații](/l/ro/developers/extend/apps/data/relations) pentru modelul de relație bidirecțională.
* **Adăugați câmpuri la obiecte din alte aplicații** — consultați [Extinderea obiectelor](/l/ro/developers/extend/apps/data/extending-objects) pentru `defineField()`.
* **Afișează acest obiect în interfața de utilizator** — consultă [Elemente de meniu de navigare](/l/ro/developers/extend/apps/layout/navigation-menu-items) pentru a adăuga un element în bara laterală; consultă [Vizualizări](/l/ro/developers/extend/apps/layout/views) pentru a adăuga configurații personalizate de listă.

View File

@@ -0,0 +1,101 @@
---
title: Prezentare generală
description: Modelați datele pe care aplicația le adaugă într-un spațiu de lucru — obiecte, câmpuri și relații.
icon: database
---
Stratul de **date** al unei aplicații Twenty reprezintă datele pe care aplicația le *adaugă* într-un spațiu de lucru — noile tipuri de înregistrări pe care le declară, coloanele pe care le adaugă la obiectele existente și modul în care aceste înregistrări se conectează între ele.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## În această secțiune
<CardGroup cols={2}>
<Card title="Obiecte" icon="table" href="/l/ro/developers/extend/apps/data/objects">
`defineObject` — declarați noi tipuri de înregistrări cu propriile lor câmpuri.
</Card>
<Card title="Extinderea obiectelor" icon="wand-magic-sparkles" href="/l/ro/developers/extend/apps/data/extending-objects">
`defineField` — adăugați câmpuri la obiecte standard sau la obiectele altor aplicații.
</Card>
<Card title="Relații" icon="diagram-project" href="/l/ro/developers/extend/apps/data/relations">
Conexiuni bidirecționale `MANY_TO_ONE` / `ONE_TO_MANY` între obiecte.
</Card>
<Card title="Tipuri de activități ale cronologiei" icon="clock-rotate-left" href="/l/ro/developers/extend/apps/data/timeline-activity-types">
Definiți evenimente de audit automate și evenimente explicite ale aplicației pentru cronologiile înregistrărilor.
</Card>
</CardGroup>
## Entități dintr-o privire
| Entitate | Scop | Definit cu |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| **Obiect** | Un nou tip de înregistrare personalizat (de ex. PostCard, Invoice) cu propriile sale câmpuri | `defineObject()` |
| **Câmp** | O coloană pe un obiect. Câmpurile independente pot extinde obiecte pe care nu le-ați creat (de ex. adăugați `loyaltyTier` la Company) | `defineField()` |
| **Relație** | O legătură bidirecțională între două obiecte — ambele părți declarate ca câmpuri | `defineField()` cu `FieldType.RELATION` |
| **Indice** | Un indice de bază de date pentru a accelera o interogare recurentă asupra unuia dintre obiectele tale | `defineIndex()` |
| **Tip de activitate a cronologiei** | Un eveniment automat de audit sau un eveniment explicit de domeniu afișat în cronologiile înregistrărilor. | `defineTimelineActivityType()` |
SDK-ul detectează acestea prin analiza AST la momentul build-ului, astfel încât organizarea fișierelor ține de tine — convenția este `src/objects/`, `src/fields/`, `src/indexes/` și `src/timeline-activity-types/`. UUID-urile stabile `universalIdentifier` leagă totul în toate implementările.
## Indici (opțional)
Aplicațiile pot livra indici împreună cu obiectele lor pentru a menține rapide interogările recurente. Cel mai comun caz este o coloană de status sau o coloană cu cheie străină pe care o citești frecvent.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### Indici unici
`defineIndex` acceptă `isUnique: true` atât pentru unicitatea pe o singură coloană, cât și pe mai multe coloane. Aceasta este primitiva recomandată — `defineField({ isUnique: true })` este învechită (deprecated) și va fi eliminată într-o versiune viitoare.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Alte constrângeri
* Clauzele `WHERE` parțiale rămân sub controlul administratorului — aplicațiile nu le pot declara.
* Fiecare obiect este limitat la 10 indici personalizați (indicii proprii ai framework-ului nu se pun la socoteală).
Ordonează array-ul `fields` în modul în care Postgres ar trebui să îl folosească — coloana din stânga prima, ca într-o agendă telefonică. Indicii nu sunt gratuiți: fiecare scriere în tabel îi actualizează. Adaugă unul doar atunci când ai o interogare care are nevoie de el.
<Note>
Căutați **Application Config** sau **Roles & Permissions**? Acestea descriu aplicația în sine, mai degrabă decât datele pe care le adaugă — se află la [Config](/l/ro/developers/extend/apps/config/overview). Căutați **Connections** (Linear, GitHub, Slack OAuth)? Acestea există pentru a fi apelate *din* funcții de logică și se află la [Logic](/l/ro/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Relații
description: Conectați obiectele între ele cu relații bidirecționale MANY_TO_ONE / ONE_TO_MANY.
icon: diagram-project
---
Relațiile conectează două obiecte între ele. În Twenty, relațiile sunt întotdeauna **bidirecționale** — fiecare relație are două părți, iar fiecare parte este declarată ca un câmp ce o referențiază pe cealaltă.
| Tip relație | Descriere | Are cheie străină? |
| ------------- | ---------------------------------------------------------------------------------- | --------------------- |
| `MANY_TO_ONE` | Multe înregistrări ale acestui obiect indică către o singură înregistrare a țintei | Da (`joinColumnName`) |
| `ONE_TO_MANY` | O înregistrare a acestui obiect are multe înregistrări ale țintei | Nu (partea inversă) |
## Cum funcționează relațiile
Fiecare relație necesită **două câmpuri** care se referențiază reciproc:
1. Partea **MANY_TO_ONE** — se află pe obiectul care deține cheia străină.
2. Partea **ONE_TO_MANY** — se află pe obiectul care deține colecția.
Ambele câmpuri folosesc `FieldType.RELATION` și se referențiază încrucișat prin `relationTargetFieldMetadataUniversalIdentifier`.
## Exemplu: Post Card are mulți destinatari
Un `PostCard` poate fi trimis către multe înregistrări `PostCardRecipient`. Fiecare destinatar aparține exact unui Post Card.
**Pasul 1: Definiți partea ONE_TO_MANY pe PostCard** (partea "one"):
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Pasul 2: Definiți partea MANY_TO_ONE pe PostCardRecipient** (partea "many" — deține cheia străină):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Importuri circulare:** ambele câmpuri de relație se referă unul la celălalt prin `universalIdentifier`. Pentru a evita problemele de import circular, exportați ID-urile câmpurilor ca constante denumite din fiecare fișier și importați-le în celălalt. Sistemul de build le rezolvă în timpul compilării.
</Note>
## Relații de joncțiune
Un obiect de joncțiune conectează o înregistrare sursă la una sau mai multe înregistrări-țintă. De exemplu, `PostCardRecipient` poate conține atât o relație `postCard`, cât și o relație `recipient`. În câmpul `ONE_TO_MANY` al obiectului sursă, setează `junctionTargetFieldUniversalIdentifier` la câmpul de relație de pe obiectul de joncțiune care indică spre țintă:
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` trebuie să identifice o relație `MANY_TO_ONE` sau morfă pe obiectul de joncțiune. Acesta indică lui Twenty care înregistrare asociată este ținta atunci când o funcționalitate precum [distribuirea activității din cronologie](/l/ro/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) parcurge joncțiunea.
## Relaționarea cu obiectele standard
Pentru a crea o relație cu un obiect Twenty încorporat (Person, Company etc.), utilizați `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
## Proprietăți ale câmpului de relație
| Proprietate | Obligatoriu | Descriere |
| ---------------------------------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `type` | Da | Trebuie să fie `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Da | `universalIdentifier` al obiectului țintă |
| `relationTargetFieldMetadataUniversalIdentifier` | Da | `universalIdentifier` al câmpului corespunzător de pe obiectul țintă |
| `universalSettings.relationType` | Da | `RelationType.MANY_TO_ONE` sau `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Doar MANY_TO_ONE | Ce se întâmplă atunci când înregistrarea referențiată este ștearsă: `CASCADE`, `SET_NULL`, `RESTRICT` sau `NO_ACTION` |
| `universalSettings.joinColumnName` | Doar MANY_TO_ONE | Numele coloanei din baza de date pentru cheia străină (de ex., `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Doar ONE_TO_MANY de joncțiune | Câmp de relație pe obiectul de joncțiune care indică spre ținta distribuției |
## Câmpuri de relație inline
Puteți, de asemenea, să declarați o relație direct în [`defineObject`](/l/ro/developers/extend/apps/data/objects). Când este inline, omiteți `objectUniversalIdentifier` — este moștenit de la obiectul părinte:
```ts
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// … other fields
],
});
```

View File

@@ -0,0 +1,213 @@
---
title: Țintirea metadatelor de sistem
description: Rezolvă identificatorii universali determinați ai metadatelor pe care Twenty le provizionează automat pe fiecare obiect, astfel încât aplicația ta să le poată referenția fără hardcodare.
icon: gears
---
Fiecare obiect din Twenty vine cu **metadate de sistem** pe care nu le declari niciodată tu însuți, cum ar fi un set de câmpuri și o vizualizare principală de listă cu coloanele sale. Serverul le creează pe toate atunci când obiectul este provizionat, iar setul crește pe măsură ce Twenty evoluează.
Deoarece nu îl declari, nu există nicio constantă `universalIdentifier` pe care să o imporți. În schimb, serverul **derivează** fiecare identificator într-un mod determinist, iar `twenty-sdk` expune aceeași derivare astfel încât manifestul tău să poată rezolva exact valoarea pe care o folosește serverul.
## Câmpuri de sistem
Câmpurile scalare prezente pe fiecare obiect, niciunul dintre ele nu este declarat de tine cu [`defineField()`](/l/ro/developers/extend/apps/data/extending-objects):
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Deci cum faci referire la `createdAt` ca o coloană într-o [vizualizare](/l/ro/developers/extend/apps/layout/views)?
### Problema
Începând cu Twenty 2.19, identificatorul universal al unui câmp de sistem este **derivat deterministic** de server din trei intrări: identificatorul universal al aplicației, identificatorul universal al obiectului și numele câmpului. Inventarea unui id și hardcodarea lui nu va funcționa: acesta nu se potrivește cu nimic de pe server, iar sincronizarea respinge referința rămasă în aer:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### Soluția
<Note>
`getFieldUniversalIdentifier` este disponibil începând cu `twenty-sdk` 2.21.
</Note>
Folosește `getFieldUniversalIdentifier` pentru a obține exact aceeași valoare pe care o folosește serverul. Aceasta primește cele trei intrări și returnează identificatorul universal al câmpului:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` este identificatorul aplicației tale, cel pe care îl transmiți către [`defineApplication()`](/l/ro/developers/extend/apps/config/application).
* `objectUniversalIdentifier` este identificatorul obiectului căruia îi aparține câmpul.
* `name` este numele câmpului de sistem, una dintre valorile listate mai sus.
### Exemplu: o coloană createdAt într-o vizualizare
Cazul tipic este adăugarea unei coloane `createdAt` la o vizualizare a unuia dintre obiectele tale personalizate. Calculează id-ul câmpului și fă referire la el ca la orice alt `fieldMetadataUniversalIdentifier`:
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Același id calculat funcționează oriunde este așteptat un `fieldMetadataUniversalIdentifier`: câmpuri de vizualizare, filtre, sortări, grupări și widget-uri de tip page-layout.
<Note>
Calculează id-ul, nu îl hardcoda. Deoarece serverul derivă valoarea din
id-ul aplicației, id-ul obiectului și numele câmpului, apelarea
`getFieldUniversalIdentifier` menține referința corectă chiar dacă aceste
intrări se schimbă și evită divergența dacă metoda de derivare evoluează vreodată.
</Note>
### Câmpuri de relație de sistem
<Note>
`getSystemRelationFieldUniversalIdentifier` este disponibil începând cu versiunea 2.23 a `twenty-sdk` și necesită un server Twenty pe versiunea 2.23 sau o versiune ulterioară.
</Note>
Pe lângă câmpurile scalare de sistem de mai sus, serverul mai pune la dispoziție patru **câmpuri de relație de sistem** pentru fiecare obiect: `timelineActivities`, `attachments`, `noteTargets` și `taskTargets`, fiecare indicând către obiectul de relație standard corespunzător.
Aceste câmpuri nu sunt rezolvate cu `getFieldUniversalIdentifier`: identificatorul lor este derivat **independent de nume**, din obiectul care găzduiește câmpul și obiectul către care indică acest câmp. Astfel, redenumirea unui obiect nu modifică niciodată identificatorii câmpurilor sale de relație.
Folosește `getSystemRelationFieldUniversalIdentifier` pentru a le rezolva:
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` este obiectul care **găzduiește** câmpul.
* `relationTargetObjectUniversalIdentifier` este obiectul către care **indică** câmpul.
Direcția este codificată prin ordinea argumentelor. Pentru a rezolva partea inversă (de ex. `attachment.targetRocket`, câmpul morfic pe care serverul îl creează pe obiectul de relație standard), inversează-le pe cele două:
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
Ca și în cazul câmpurilor de sistem scalare, id-ul rezolvat funcționează oriunde este așteptat un `fieldMetadataUniversalIdentifier`.
## Vizualizări de sistem
<Note>
`getSystemViewUniversalIdentifier` și `getSystemViewFieldUniversalIdentifier`
sunt disponibile începând cu versiunea 2.26 a `twenty-sdk` și necesită un server Twenty pe
versiunea 2.26 sau o versiune ulterioară.
</Note>
Serverul provizionează, de asemenea, o **vizualizare de sistem** pentru fiecare obiect: vizualizarea principală de listă (`All {objectLabelPlural}`, indexată prin `ViewKey.INDEX` (derivată cu `SYSTEM_VIEW_KEYS.INDEX`)), cu câte o coloană pentru fiecare câmp afișabil. La fel ca în cazul câmpurilor de relație de sistem, identificatorii lor sunt derivați **fără nume**, astfel încât redenumirea unui obiect sau a unui câmp nu îi schimbă niciodată.
Folosește `getSystemViewUniversalIdentifier` pentru a rezolva vizualizarea:
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` este aplicația care deține **obiectul**, ceea ce reprezintă spațiul de nume pentru vizualizare.
* `objectUniversalIdentifier` este obiectul pe care îl listează vizualizarea.
* `viewKey` este discriminatorul de vizualizare de sistem: `SYSTEM_VIEW_KEYS.INDEX` pentru vizualizarea principală de listă, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` pentru vizualizarea widgetului de câmpuri de pe pagina de înregistrare. Acesta stabilește derivarea; numai `INDEX` este, de asemenea, stocat pe rândul vizualizării.
ID-ul rezolvat funcționează oriunde este așteptat un `viewUniversalIdentifier`, cum ar fi o intrare în bara laterală [`NavigationMenuItemType.VIEW`](/l/ro/developers/extend/apps/layout/navigation-menu-items). Pentru a deschide pur și simplu lista principală a unui obiect, preferă `NavigationMenuItemType.OBJECT` cu `targetObjectUniversalIdentifier`: nu are nevoie de derivare.
`getSystemViewFieldUniversalIdentifier` rezolvă o singură **coloană** dintr-o vizualizare de sistem, pornind de la vizualizare și câmpul pe care îl afișează:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Observă primul argument: o coloană este plasată în spațiul de nume al aplicației care deține **câmpul pe care îl afișează**, nu al celei care deține vizualizarea. Un câmp pe care aplicația ta îl adaugă la un obiect standard își obține coloana derivată sub aplicația ta, pe o vizualizare deținută de Twenty.
<Warning>
Vizualizările de sistem și coloanele lor sunt **deținute de server**: rezolvă-le identificatorii
pentru a le referenția, niciodată pentru a le declara. `key` pe
[`defineView()`](/l/ro/developers/extend/apps/layout/views) este depreciat și
ignorat, astfel încât o vizualizare din manifest nu poate revendica niciodată cheia `INDEX`, iar serverul
provizionează deja o coloană pentru fiecare câmp pe care îl adaugi, deci declararea propriului tău
`defineViewField()` pentru același câmp într-o vizualizare de sistem intră în conflict cu aceasta.
</Warning>
## Obiecte standard Twenty
Pentru un obiect Twenty **standard** (Person, Company, Opportunity, …), nu trebuie să derivezi nimic: identificatorii sunt constante pre-calculate pe care le poți importa direct, atât pentru câmpuri, cât și pentru vizualizări.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
Apelează la utilitarele de mai sus atunci când obiectul este unul pe care aplicația ta îl definește cu [`defineObject()`](/l/ro/developers/extend/apps/data/objects), unde nu există o astfel de constantă.
<Note>
`name` este un câmp **implicit**, nu un câmp de sistem. Acesta își păstrează propriul identificator universal
hardcodat și nu este obținut prin
`getFieldUniversalIdentifier`. În obiectele pe care le definești, fă referire la câmpul
`name` prin identificatorul pe care i l-ai dat în `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Tipuri de activitate cronologică
description: Definiți evenimentele de audit automat și evenimentele de aplicație explicite care se execută în calendarele înregistrării.
icon: clock-rotate-left
---
Un tip de activitate cronologică definește vocabularul stabil pentru un eveniment afișat într-un calendar al înregistrării. Obiectele standard și obiectele din aplicație utilizează același contract: un tip are o etichetă și o pictogramă, poate declara opțional când este emis, și pot reda opțional prin una dintre [componentele frontale](/l/ro/developers/extend/apps/layout/front-components).
<Note>
Tipurile de activitate sunt în beta şi vor apărea în curând în 20 2,34. API-ul
poate evolua în timp ce învățăm din cazurile de utilizare a dezvoltatorilor de aplicații.
</Note>
Creați unul cu caseta de teme:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
Sau definește direct:
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## Evenimente automate și explicite
Adăugați `emit` când douăzeci de persoane ar trebui să creeze acest tip automat. `emit.on` suportă `created`, `updated`, `deleted`, `restored`, `linked` şi `unlinked`, în timp ce `emit.objectalIdentifier` identifică obiectul sursă. Doar un singur tip eficient poate gestiona aceeași tastă emit — `on`, obiect și opțional prin intermediul relației — într-un spațiu de lucru.
Fără `emit.through`, evenimentul este scris pe calendarul înregistrărilor sursă. Pentru a o acoperi la înregistrările conexe, setați \`emit.through. elationFieldUniversalIdentifier" fie la o relaţie directă de la un individ la altul, fie la o [relaţie de unu-la-multe joncţiuni ](/l/ro/developers/extend/apps/data/relations#junction-relations) la obiectul sursă. Relaţiile dintre Morph şi fiecare membru al grupului lor, astfel încât o singură declaraţie poate viza mai multe tipuri de obiecte.
Pentru o legătură directă, crearea sau restaurarea înregistrării sursă produce `linked`, ștergându-l produce `unlinked`, și redirecționarea relației produce `unlinked` pe ținta anterioară plus `linked` pe noul țintă. Alte actualizări sursă nu produc evenimente legate de legătură. Acesta este contractul utilizat de ataşamente, a cărui ţintă este o relaţie directă de morfologie.
Pentru o legătură de joncțiune, `universalSettings.junctionTargetFieldUniversalIdentifier` identifică relația de la obiectul de joncțiune la țintă. Crearea sau ştergerea unui rând de joncţiune produce `linked` sau `unlinked`. De asemenea, redirecționarea oricărei părți a unui rând de joncțiune produce un eveniment de legătură; actualizări ale câmpurilor de joncțiune care nu au legătură cu acestea.
Evenimentele `linked` şi `unlinked` necesită `emit.through`, deoarece declanşatorul lor este o schimbare la relaţia directă sau de joncţiune configurată.
De exemplu, acesta este același contract generic utilizat de note, sarcini, mesaje și evenimente calendaristice:
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
Un `updated` prin eveniment este emis implicit pentru fiecare actualizare sursă. Setați `emit.through.triggerFieldUniversalIdentifiers` când numai modificările câmpurilor sursă selectate ar trebui să apară în termenele țintă.
Omiteți `emit` pentru un eveniment de domeniu explicit pe care funcția logică îl creează. Astfel se evită realizarea atât a unui rând automat de audit, cât și a unui rând explicit pentru aceeași operațiune.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
Creați evenimente explicite cu [`createTimelineActivity()`](/l/ro/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). Codul aplicației folosește identificatori universali stabili; Douăzeci rezolvă ID-urile de metadate specifice instalației.
## Redare personalizată
Fără o componentă din față, douăzeci face un rând nativ generic de pe eticheta tipului, pictograma și metadatele obiectelor legate. Aceasta funcționează pentru obiecte standard și personalizate fără un dispozitiv de redare specific obiectului.
Pentru detalii personalizate, setați `frontComponentUniversalIdentifier` la o componentă din față deținută de aceeași aplicație:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Rândul nativ rămâne prezentarea prăbuşită. Douăzeci de montați componenta frontală numai după ce utilizatorul se extinde acea linie, evitând un sandbox și un lucrător pentru fiecare eveniment vizibil. În interiorul componentei, apelați `useTimelineActivityId()` din `douăzeci de elemente-sdk/front-component` pentru a citi ID-ul rândului și a prelua orice date de care aveți nevoie prezentarea. Returnează `null` când componenta este randată în afara liniei cronologice.
## Înlocuirea prezentării altei aplicații
O aplicație deține contracte de cronologie automată pentru propriile obiecte. Pentru a personaliza un eveniment pe un obiect deținut de o altă aplicație, declarați tipul existent în mod explicit cu `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
O suprascriere trebuie să includă `emit`, deoarece înlocuiește un slot automat existent emit mai degrabă decât un tip doar explicit. Tipul de referință trebuie să aparțină aplicației obiectului țintă și să descrie aceeași acțiune și aceeași rută. Eliminarea aplicației suprascriere restaurează tipul de bază; eliminarea tipului de bază dezactivează suprascrierea.
## Suprascrie spațiul de lucru și amuțiri
Administratorii spațiului de lucru pot schimba prezentarea unui tip sau pot bloca evenimentele sale automate și explicite fără a forja aplicația. Folosiți mutația `updateTimelineActivityType` a metadata API cu ID-ul specific instalației:
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label` şi `icon` sunt stocate ca suprascrieri de spaţiu de lucru, astfel încât actualizările ulterioare ale aplicaţiei nu suprascriu opţiunile administratorului. Setați `isActive` la `false` pentru a opri emisia automată și pentru a respinge noi evenimente explicite de acest tip. Rândurile existente rămân vizibile.
Restabiliți setările implicite, inclusiv starea activă, cu:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Câmpuri de configurare
| Câmp | Obligatoriu | Descriere |
| ------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------- |
| `universalIdentifier` | Da | UUID stabil pentru tipul de instalații și modernizări |
| `name` | Da | Nume programatic local aplicației; trebuie să fie unic în cadrul aplicației |
| `label` | Da | Text orientat către utilizator utilizat de către dispozitivul de redare nativă |
| `icon` | Nu | Douăzeci de nume afișate alături de eveniment |
| `emit` | Nu | Declarație de emisii automată; omit pentru tipuri numai explicite |
| `emit.on` | Cu emit | Acțiuni de audit care fac ca acest tip să fie redactat |
| `emit.objectUniversalIdentifier` | Cu emit | Obiect ale cărui înregistrări emit de acest tip |
| `emit.through.relationFieldUniversalIdentifier` | Nu | Relaţia directă sau de joncţiune utilizată pentru ventilator la termenele aferente |
| `emit.through.triggerFieldUniversalIdentifiers` | Nu | Câmpurile sursă care pot declanșa un `updated` prin eveniment |
| `frontComponentUniversalIdentifier` | Nu | Componentă frontală deținută de aplicație montată când rândul este extins |
| `replacesTimelineActivityTypeUniversalIdentifier` | Nu | Tipul de emit existent înlocuit; necesită `emit` |
Douăzeci de instantanee transformă identitatea semantică și prezentarea durabilă atunci când un eveniment este creat. Rândurile istorice păstrează acțiunea originală și semnificația obiectului. În timp ce tipul este instalat, eticheta curentă tradusă, pictograma și componenta frontală redare live; după dezinstalare, instantaneul face rândul să fie lizibil. Rezoluția folosește identificatorul universal, astfel încât prezentarea supraviețuiește și reinstalării aplicației.

View File

@@ -0,0 +1,102 @@
---
title: Concepte
description: Cum funcționează aplicațiile Twenty — modelul de entități, izolarea (sandboxing) și ciclul de viață al instalării.
icon: sitemap
---
Aplicațiile Twenty sunt pachete TypeScript care vă extind spațiul de lucru cu obiecte personalizate, logică, componente UI și capabilități AI. Acestea rulează pe platforma Twenty, cu izolare completă (sandboxing) și controale de permisiuni.
## Cum funcționează aplicațiile
O aplicație este o colecție de **entități** declarate folosind funcțiile `defineEntity()` din pachetul `twenty-sdk`. SDK-ul detectează aceste declarații prin analiză AST în timpul construirii și produce un **manifest** — o descriere completă a ceea ce aplicația dvs. adaugă unui spațiu de lucru. Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**Organizarea fișierelor ține de dvs.** Detectarea entităților este bazată pe AST — SDK-ul găsește apelurile `export default defineEntity(...)` indiferent unde se află fișierul. Structura de foldere de mai sus este o convenție, nu o cerință.
</Note>
## Tipuri de entități
| Entitate | Scop | Documentație |
| --------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| **Aplicație** | Identitatea aplicației, rolul implicit, variabile | [Application Config](/l/ro/developers/extend/apps/config/application) |
| **Rol** | Seturi de permisiuni pentru obiecte și câmpuri | [Roles & Permissions](/l/ro/developers/extend/apps/config/roles) |
| **Obiect** | Tipuri de înregistrări personalizate cu câmpuri | [Objects](/l/ro/developers/extend/apps/data/objects) |
| **Câmp** | Adăugați câmpuri la obiecte din alte aplicații | [Extending Objects](/l/ro/developers/extend/apps/data/extending-objects) |
| **Relație** | Legături bidirecționale între obiecte | [Relations](/l/ro/developers/extend/apps/data/relations) |
| **Funcție logică** | TypeScript pe partea de server cu declanșatoare | [Funcții logice](/l/ro/developers/extend/apps/logic/logic-functions) |
| **Abilitate** | Instrucțiuni reutilizabile pentru agenți AI | [Abilități și agenți](/l/ro/developers/extend/apps/logic/skills-and-agents) |
| **Agent** | Agenți AI cu prompturi personalizate | [Abilități și agenți](/l/ro/developers/extend/apps/logic/skills-and-agents) |
| **Furnizor de conexiune** | Date de autentificare OAuth pentru API-uri terțe | [Connections](/l/ro/developers/extend/apps/logic/connections) |
| **Vizualizare** | Vizualizări preconfigurate ale listelor de înregistrări | [Views](/l/ro/developers/extend/apps/layout/views) |
| **Element de meniu de navigare** | Intrări personalizate în bara laterală | [Navigation Menu Items](/l/ro/developers/extend/apps/layout/navigation-menu-items) |
| **Layout pagină** | File și widgeturi pe pagina de detalii a unei înregistrări | [Page Layouts](/l/ro/developers/extend/apps/layout/page-layouts) |
| **Componentă front-end** | Interfață React izolată (sandboxed) în Twenty | [Componente front-end](/l/ro/developers/extend/apps/layout/front-components) |
| **Dependențe partajate** | Dependențele partajate de componentele front-end ale aplicației, declarate în package.json | [Dependențe partajate](/l/ro/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) |
| **Element din meniul de comenzi** | Acțiuni rapide și intrări Cmd+K | [Command Menu Items](/l/ro/developers/extend/apps/layout/command-menu-items) |
## Izolare (sandboxing)
* **Funcțiile logice** rulează în procese Node.js izolate pe server. Acestea accesează datele numai prin clientul API tipizat, care acționează fie ca persoana care a declanșat execuția, fie ca aplicația în sine. Consultați [Ce acces utilizează un apel](/l/ro/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* **Componentele front-end** rulează în Web Workers folosind Remote DOM — izolate de pagina principală, dar randând elemente DOM native (nu iframes). Acestea comunică cu Twenty printr-un API al gazdei bazat pe transmiterea de mesaje.
* **Permisiunile** sunt aplicate la nivelul API-ului. Sunt injectate două tokenuri de execuție, ambele derivate din rolul declarat cu `defineApplicationRole()`; cel implicit este restrâns suplimentar la ceea ce poate face persoana care a declanșat execuția, ori de câte ori o persoană se află în spatele execuției.
## Ciclul de viață al aplicației
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — monitorizează fișierele sursă și sincronizează în timp real modificările către un server Twenty conectat. Clientul API tipizat este regenerat automat atunci când schema se schimbă.
* **`yarn twenty dev:build`** — compilează TypeScript, împachetează funcțiile logice și componentele front-end cu esbuild și produce un manifest.
* **Hook-uri pre/post-instalare și dezinstalare** — funcții opționale care rulează în timpul instalării sau chiar înainte de eliminare. Vezi [Install Hooks](/l/ro/developers/extend/apps/config/install-hooks) pentru detalii.
## Pașii următori
<CardGroup cols={2}>
<Card title="Configurare" icon="screwdriver-wrench" href="/l/ro/developers/extend/apps/config/overview">
Identitatea aplicației, rolul implicit și hook-urile de instalare și dezinstalare.
</Card>
<Card title="Date" icon="database" href="/l/ro/developers/extend/apps/data/overview">
Obiecte, câmpuri și relații bidirecționale.
</Card>
<Card title="Logică" icon="bolt" href="/l/ro/developers/extend/apps/logic/overview">
Funcții logice, abilități, agenți și conexiuni OAuth.
</Card>
<Card title="Aspect" icon="table-columns" href="/l/ro/developers/extend/apps/layout/overview">
Vizualizări, navigare, layouturi de pagină, componente front-end.
</Card>
<Card title="Operațiuni" icon="rocket" href="/l/ro/developers/extend/apps/operations/overview">
CLI, testare, remote-uri, CI și publicarea aplicației.
</Card>
</CardGroup>

View File

@@ -0,0 +1,87 @@
---
title: Server local
description: Gestionează serverul Twenty Docker local — pornește, oprește, actualizează, rulează o instanță de testare în paralel și configurează manual SDK-ul.
icon: server
---
## Gestionarea serverului local
Folosiți `yarn twenty docker:*` pentru a controla containerul Twenty local:
| Comandă | Ce face |
| -------------------------------------- | ------------------------------------------------------------ |
| `yarn twenty docker:start` | Pornește serverul (descarcă imaginea dacă este necesar) |
| `yarn twenty docker:start 2.2.0` | Pornește o anumită versiune a serverului |
| `yarn twenty docker:start --port 3030` | Pornește pe un port personalizat |
| `yarn twenty docker:stop` | Oprește serverul (păstrează datele) |
| `yarn twenty docker:status` | Afișează URL-ul, versiunea și credențialele de autentificare |
| `yarn twenty docker:logs` | Transmite în flux jurnalele serverului |
| `yarn twenty docker:reset` | Șterge datele și pornește de la zero |
| `yarn twenty docker:upgrade` | Descarcă cea mai recentă imagine `twenty-app-dev` |
| `yarn twenty docker:upgrade 2.2.0` | Actualizează la o versiune specifică |
Datele persistă între reporniri în două volume Docker (`twenty-app-dev-data` pentru PostgreSQL, `twenty-app-dev-storage` pentru fișiere). Folosiți `reset` pentru a șterge totul.
## Fixarea versiunii serverului
Când nu este transmisă nicio versiune, `docker:start` rezolvă versiunea din intervalul `engines.twenty` al aplicației tale din `package.json` — același interval față de care serverul validează când aplicația ta este instalată. Pornește cea mai nouă imagine publicată `twenty-app-dev` care satisface intervalul, revenind la `latest` atunci când câmpul lipsește sau nicio versiune publicată nu se potrivește:
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
Transmite explicit o versiune pentru a suprascrie intervalul pentru o singură rulare: `yarn twenty docker:start 2.3.0`. Dacă un container există deja pe o versiune diferită, `docker:start` îl actualizează pe loc (recreând containerul, dar păstrând volumele tale de date).
## Actualizarea imaginii serverului
`yarn twenty docker:upgrade` descarcă cea mai recentă imagine, compară digest-urile și recreează containerul doar dacă s-a schimbat ceva. Volumele de date sunt păstrate — doar containerul este înlocuit. Dacă a fost descărcată o imagine nouă și containerul rula, actualizarea pornește automat un container nou; rulați apoi `yarn twenty docker:start` pentru a aștepta până când devine funcțional.
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
Puteți verifica versiunea care rulează cu `yarn twenty docker:status` (aceasta afișează `APP_VERSION` încorporat în container).
## Rularea unei instanțe de test în paralel
Adăugați `--test` la orice comandă `docker:*` pentru a gestiona o a doua instanță, complet izolată — utilă pentru teste de integrare sau pentru a experimenta fără a atinge datele principale de dezvoltare:
| Comandă | Ce face |
| ----------------------------------- | --------------------------------------------------- |
| `yarn twenty docker:start --test` | Pornește instanța de test (implicit pe portul 2021) |
| `yarn twenty docker:stop --test` | Oprește-o |
| `yarn twenty docker:status --test` | Afișează-i starea |
| `yarn twenty docker:logs --test` | Transmite în flux jurnalele sale |
| `yarn twenty docker:reset --test` | Șterge-i datele |
| `yarn twenty docker:upgrade --test` | Actualizează-i imaginea |
Instanța de test are propriul container (`twenty-app-dev-test`), propriile volume (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) și propria configurație — rulează alături de instanța principală, fără conflicte. Combinați `--test` cu `--port` pentru a înlocui portul 2021.
## Configurare manuală (fără generator)
Săriți peste generatorul de schelet dacă adăugați SDK-ul într-un proiect existent:
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Adăugați scriptul în `package.json`:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
Acum puteți rula `yarn twenty dev`, `yarn twenty docker:start` și restul.
<Note>
Nu instalați `twenty-sdk` global — fixați-l per proiect astfel încât fiecare aplicație să folosească propria versiune.
</Note>

View File

@@ -0,0 +1,79 @@
---
title: Structura proiectului
description: Ce conține o aplicație Twenty generată — fișiere, foldere și ce face fiecare.
icon: folder-tree
---
O aplicație nouă generată de `npx create-twenty-app` arată astfel:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md
```
## Fișiere cheie
| Fișier / Folder | Scop |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `src/application-config.ts` | **Necesar.** Fișierul principal de configurare pentru aplicație. |
| `src/default-role.ts` | Rol implicit care controlează la ce pot avea acces funcțiile logice. |
| `src/constants/universal-identifiers.ts` | UUID-uri generate automat și metadate (nume afișat, descriere). |
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | O pagină de bun venit inițială: o componentă de interfață randată de un layout de pagină autonom, accesibilă din bara laterală. |
| `src/__tests__/` | Un test unitar plus un test de integrare (cu configurarea sa globală) care sincronizează aplicația cu un server real. |
| `public/` | Resurse statice (imagini, fonturi) servite împreună cu aplicația. |
| `AGENTS.md` / `CLAUDE.md` | Ghid pentru agenții AI de programare care lucrează la aplicație. |
| `CHANGELOG.md` / `SETUP.md` | Jurnal de modificări notabile și instrucțiuni de configurare pentru dezvoltare locală. |
<Note>
**Organizarea fișierelor ține de dvs.** Folderele de mai sus sunt convenții — SDK-ul detectează entitățile prin analiză AST pe apelurile `export default defineEntity(...)`, indiferent unde se află fișierul.
</Note>
## Dependențe
Ambele pachete Twenty SDK trebuie plasate sub `devDependencies`, nu sub `dependencies`:
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "2.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
Generatorul de schelete fixează versiunile `twenty-sdk` și `twenty-client-sdk` la propria sa versiune — păstrează-le sincronizate când faci upgrade.
* **`twenty-sdk`** livrează CLI-ul `twenty` și uneltele de build/scaffolding. Acesta rulează doar în timpul dezvoltării și al build-ului și nu este niciodată importat de runtime-ul aplicației tale publicate.
* **`twenty-client-sdk`** este importat de codul aplicației tale (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), dar Twenty îl furnizează la runtime — funcțiile de logică îl obțin dintr-un strat SDK generat, iar componentele de interfață îl rezolvă din module livrate de server. Copia instalată local este folosită doar pentru verificarea tipurilor și pentru build-ul la momentul de deploy, astfel că nu trebuie niciodată inclusă în bundle-ul livrat.
Păstrarea oricărui pachet sub `dependencies` îl include în bundle-ul de runtime al aplicației instalate, unde reprezintă o încărcătură inutilă. `twenty dev:build` emite un avertisment atunci când oricare dintre ele este încă listat sub `dependencies`.
Adaugă dependențele de runtime proprii ale aplicației tale (bibliotecile pe care funcțiile tale de logică chiar le importă la runtime) sub `dependencies`, ca de obicei. Menține arborele suplu: `dependencies` sunt instalate într-un strat de execuție limitat la 250 MB necomprimat, iar o operațiune de sync sau install eșuează cu o eroare privind dimensiunea dependințelor dacă se depășește această limită — bibliotecile de UI și uneltele de dezvoltare trebuie să fie în `devDependencies`.

View File

@@ -0,0 +1,166 @@
---
title: Pornire rapidă
icon: rocket
description: Creați prima dvs. aplicație Twenty în câteva minute.
---
## Cerințe
* **Node.js 24.5+** — [Descărcați](https://nodejs.org/)
* **Yarn 4** — vine împreună cu Node prin Corepack. Activați-l: `corepack enable`
* **Docker** — [Descărcați](https://www.docker.com/products/docker-desktop/). Necesar pentru a rula un server Twenty local. Omiteți dacă rulați deja Twenty în altă parte.
Crearea unei aplicații Twenty are trei faze. Generatorul le reunește într-o singură comandă pe calea optimă, dar fiecare fază este un concept separat — când ceva eșuează, dacă știți în ce fază sunteți, știți ce trebuie să corectați.
| Fază | Ce faceți | Instrument | Rezultat |
| ----------------------- | ------------------------------------------------ | ----------------------------------- | ------------------------------ |
| **1. Creați scheletul** | Generați codul sursă al aplicației | `npx create-twenty-app` | Un proiect TypeScript pe disc |
| **2. Rulați un server** | Porniți un server Twenty cu care să sincronizați | Docker + `yarn twenty docker:start` | O instanță Twenty care rulează |
| **3. Sincronizați** | Sincronizați în timp real codul cu serverul | `yarn twenty dev` | Modificările apar în UI |
---
## Faza 1 — Creați scheletul proiectului
Creați o nouă aplicație din șablon:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Generatorul este neinteractiv: numele directorului devine numele aplicației. Transmite `--display-name` și `--description` pentru a personaliza metadatele generate (le poți edita și mai târziu în `src/constants/universal-identifiers.ts`). Aceasta generează un proiect TypeScript în `my-twenty-app/` cu un fișier inițial `application-config.ts`, un rol implicit, fluxuri de lucru CI/CD și un test de integrare.
**După această fază:** aveți codul sursă al aplicației pe mașina dvs. Încă nu rulează — aceasta este Faza 2.
---
## Faza 2 — Rulați un server Twenty local
Aplicația are nevoie de un server Twenty cu care să se sincronizeze. Serverul este o instanță Twenty completă — UI, API GraphQL, PostgreSQL — care rulează local în Docker. Codul local încarcă definițiile pe acel server, făcându-le să apară în UI.
Generatorul de proiecte pornește unul pentru tine: cu Docker rulând, descarcă imaginea `twentycrm/twenty-app-dev`, o pornește pe portul `2020` și autentifică CLI-ul față de spațiul de lucru demo preconfigurat (`tim@apple.dev`) — fără a fi necesară autentificarea.
Pentru a te conecta în schimb la un server Twenty existent, treci argumentul `--url \<your-server-url>`. Serverele la distanță se autentifică prin OAuth: se deschide un browser astfel încât să te poți autentifica și să dai clic pe **Authorize**, ceea ce oferă CLI-ului acces la spațiul tău de lucru. (Poți opta pentru OAuth și local cu `--authentication-method oauth` — autentifică-te cu `tim@apple.dev` / `tim@apple.dev`.)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Ecranul de autentificare Twenty" />
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Ecranul de autorizare Twenty CLI" />
</div>
Terminalul va confirma că totul este configurat.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Aplicația a fost creată cu succes" />
</div>
**După această fază:** aveți un server Twenty care rulează la [http://localhost:2020](http://localhost:2020), iar CLI-ul dvs. este autorizat să sincronizeze cu acesta.
<Note>
Dacă Docker nu este instalat sau nu rulează, generatorul de schelet vă va indica comanda corectă de pornire pentru sistemul dvs. de operare. După ce Docker rulează, puteți continua cu `yarn twenty docker:start` — nu este nevoie să recreați scheletul.
</Note>
---
## Faza 3 — Sincronizați modificările
Aceasta este bucla internă în care veți petrece cea mai mare parte a timpului.
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Aceasta monitorizează `src/`, reconstruiește la fiecare modificare și sincronizează rezultatul pe server. Editați un fișier, salvați, iar în câteva secunde serverul reflectă modificarea. Veți vedea în terminal un panou de stare în timp real.
Pentru o ieșire mai detaliată (jurnale de build, cereri de sincronizare, urme ale erorilor), adăugați `--verbose`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Ieșirea terminalului în modul de dezvoltare" />
</div>
Deschideți [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Ar trebui să vedeți aplicația dvs. listată la **Your Apps**.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps care afișează My twenty app" />
</div>
Faceți clic pe **My twenty app** pentru a vedea **înregistrarea aplicației** — o înregistrare la nivel de server care descrie aplicația dvs. (nume, identificator, credențiale OAuth, sursă). O singură înregistrare poate fi instalată în mai multe spații de lucru pe același server.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalii despre înregistrarea aplicației" />
</div>
Faceți clic pe **View installed app** pentru a vedea instalarea în spațiul de lucru. Fila **About** afișează versiunea și opțiunile de administrare.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicație instalată" />
</div>
**După această fază:** aveți o buclă de dezvoltare în timp real. Editați orice fișier în `src/` și acesta apare în UI.
### Sincronizare unică pentru CI și scripturi
Folosește `plan` și `apply` pentru a rula același pipeline o singură dată, fără watcher:
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| Comandă | Comportament | Când se folosește |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Monitorizează și resincronizează la fiecare modificare. Rulează până când îl opriți. | Dezvoltare locală interactivă. |
| `yarn twenty apply` | Un singur build + sync, iese cu `0` la succes, `1` la eșec. Solicită confirmare pentru modificările distructive (treci argumentul `--force` pentru a sări peste aceasta). | CI, hook-uri pre-commit, agenți AI, fluxuri de lucru scriptate. |
| `yarn twenty plan` | Construiește și afișează modificările de metadate **fără a le aplica**. | Inspectarea modificărilor pe care le-ar face o sincronizare înainte de a le confirma. |
Toate modurile necesită o conexiune la distanță autentificată. Vezi [Sincronizare și recuperare](/l/ro/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) pentru mai multe detalii despre `plan`.
<Note>
`yarn twenty dev --once` și `yarn twenty dev --once --dry-run` sunt aliasuri depreciate pentru `yarn twenty apply` și `yarn twenty plan`.
</Note>
### Opțiuni pentru modul de dezvoltare
| Opțiune | Descriere |
| ------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `--force` | Aplică modificările distructive (ștergeri) fără confirmare. |
| `--debounceMs \<ms>` | Setează întârzierea de debounce pentru modificarea fișierului în milisecunde (implicit: `1000`). |
| `--verbose` / `--debug` | Afișează jurnale detaliate de construire, cereri de sincronizare și urme ale erorilor. |
## Ce puteți construi
Aplicațiile sunt compuse din **entități** — fiecare definită într-un fișier TypeScript cu un singur `export default`:
| Entitate | Ce face |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| **Obiecte și câmpuri** | Modele de date personalizate (carte poștală, factură etc.) cu câmpuri tipizate |
| **Funcții logice** | TypeScript pe server declanșat de rute HTTP, programări cron sau evenimente din baza de date |
| **Componente front-end** | Componente React care se afișează în UI-ul Twenty (panou lateral, widgeturi, meniul de comenzi) |
| **Abilități și agenți** | Capabilități AI — instrucțiuni reutilizabile și asistenți autonomi |
| **Vizualizări și navigare** | Vizualizări de listă preconfigurate și elemente de meniu în bara laterală |
| **Layouturi de pagină** | Pagini personalizate de detalii ale înregistrărilor cu file și widgeturi |
Referință completă: [Concepte](/l/ro/developers/extend/apps/getting-started/concepts).
## Pașii următori
<CardGroup cols={2}>
<Card title="Configurare" icon="screwdriver-wrench" href="/l/ro/developers/extend/apps/config/overview">
Identitatea aplicației, rolul implicit, hook-urile de instalare și dezinstalare, resursele publice.
</Card>
<Card title="Date" icon="database" href="/l/ro/developers/extend/apps/data/overview">
Obiecte, câmpuri și relații bidirecționale.
</Card>
<Card title="Logică" icon="bolt" href="/l/ro/developers/extend/apps/logic/overview">
Funcții logice, abilități, agenți și conexiuni OAuth.
</Card>
<Card title="Aspect" icon="table-columns" href="/l/ro/developers/extend/apps/layout/overview">
Vizualizări, navigare, layouturi de pagină, componente front-end.
</Card>
<Card title="Operațiuni" icon="rocket" href="/l/ro/developers/extend/apps/operations/overview">
CLI, testare, remote-uri, CI și publicarea aplicației.
</Card>
</CardGroup>

View File

@@ -0,0 +1,63 @@
---
title: Generare automată
description: Generați fișiere de entități în mod interactiv cu yarn twenty dev:add — obiecte, câmpuri, vizualizări, funcții logice și altele.
icon: wand-magic-sparkles
---
În loc să creați manual fișiere de entități, folosiți generatorul interactiv (scaffolder):
```bash filename="Terminal"
yarn twenty dev:add
```
Vă cere să alegeți un tip de entitate și vă ghidează prin câmpurile necesare, apoi scrie un fișier gata de utilizare cu un `universalIdentifier` stabil și apelul corect `defineEntity()`.
Puteți de asemenea să transmiteți direct tipul de entitate pentru a sări peste primul prompt:
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## Tipuri de entități disponibile
| Tipul entității | Comandă | Fișier generat |
| ------------------------------- | ------------------------------------------ | --------------------------------------------------------- |
| Obiect | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| Câmp | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| Funcție logică | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| Componentă frontend | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| Rol | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| Abilitate | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| Vizualizare | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| Element de meniu de navigare | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Machetă de pagină | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
| Fila "Aspect pagină" | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| Element din meniul de comenzi | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| Câmpul vizualizării | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| Furnizor de conexiune | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| Tip de activitate în cronologie | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<name>.ts` |
## Ce generează scaffolder-ul
Fiecare tip de entitate are propriul său șablon. De exemplu, `yarn twenty dev:add object` solicită:
1. **Nume (singular)** — de ex., `invoice`
2. **Nume (plural)** — de ex., `invoices`
3. **Etichetă (singular)** — completată automat din nume (de ex., `Invoice`)
4. **Etichetă (plural)** — completată automat (de ex., `Invoices`)
5. **Creați o vizualizare și un element de navigare?** — dacă răspundeți afirmativ, scaffolder-ul generează, de asemenea, o vizualizare corespunzătoare și un link în bara laterală pentru noul obiect.
Alte tipuri de entități au prompturi mai simple — majoritatea cer doar un nume.
Tipul de entitate `field` este mai detaliat: solicită numele câmpului, eticheta, tipul (dintr-o listă cu toate tipurile de câmp disponibile precum `TEXT`, `NUMBER`, `SELECT`, `RELATION` etc.) și `universalIdentifier` al obiectului țintă.
## Cale de output personalizată
Utilizați opțiunea `--path` pentru a plasa fișierul generat într-o locație personalizată:
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```

View File

@@ -0,0 +1,14 @@
---
title: Depanare
description: Probleme comune la prima rulare — Docker, versiunea Node, Yarn, dependențe.
icon: wrench
---
* **Erori Docker** — Asigurați-vă că Docker Desktop (sau daemonul) rulează înainte de `yarn twenty docker:start`. Mesajul de eroare va afișa comanda corectă de pornire pentru sistemul dvs. de operare.
* **Versiune Node greșită** — Este nevoie de 24.5+ (`engines.node: ^24.5.0`). Verificați cu `node -v`.
* **Lipsește Yarn 4** — Rulați `corepack enable`.
* **Dependențe nefuncționale** — `rm -rf node_modules && yarn install`.
* **Erori ale `twenty-sdk` după actualizarea la v2.8.0** — A fost mutat din `dependencies` în `devDependencies` în v2.8.0. Vezi [Structura proiectului → Dependințe](/l/ro/developers/extend/apps/getting-started/project-structure#dependencies).
* **`twenty dev:build` afișează un avertisment despre `twenty-client-sdk` aflat în `dependencies`** — Este furnizat în timpul execuției de către Twenty, așa că ar trebui mutat în `devDependencies` alături de `twenty-sdk`. Vezi [Structura proiectului → Dependințe](/l/ro/developers/extend/apps/getting-started/project-structure#dependencies).
Blocat? Întrebați pe [Discordul Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).

View File

@@ -0,0 +1,118 @@
---
title: Elemente din meniul de comenzi
description: Expune componente front-end ca acțiuni rapide și intrări în meniul de comenzi (Cmd+K) folosind defineCommandMenuItem.
icon: terminal
---
Un element din meniul de comenzi este puntea dintre utilizator și un [front component](/l/ro/developers/extend/apps/layout/front-components). Acesta înregistrează componenta în meniul de comenzi Twenty (Cmd+K) și, opțional, ca buton fixat de acțiune rapidă în colțul din dreapta sus al paginii.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Câmpuri de configurare
| Câmp | Obligatoriu | Descriere |
| --------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Da | ID unic stabil pentru comandă |
| `label` | Da | Etichetă completă afișată în meniul de comenzi (Cmd+K) |
| `frontComponentUniversalIdentifier` | Da | `universalIdentifier` al componentei front-end pe care această comandă o deschide |
| `shortLabel` | Nu | Etichetă mai scurtă afișată pe butonul de acțiune rapidă fixat |
| `icon` | Nu | **Învechit** — ignorat în favoarea pictogramei aplicației; build‑ul emite un avertisment dacă este setat |
| `isPinned` | Nu | Când este `true`, afișează comanda ca buton de acțiune rapidă în colțul din dreapta sus al paginii |
| `availabilityType` | Nu | Controlează unde apare comanda: `'GLOBAL'` (mereu disponibilă), `'GLOBAL_OBJECT_CONTEXT'` (doar în paginile cu context de obiect — pagini de index și de înregistrare), `'RECORD_SELECTION'` (doar când sunt selectate înregistrări) sau `'FALLBACK'` (afișată când nicio altă comandă nu se potrivește) |
| `availabilityObjectUniversalIdentifier` | Nu | Restricționați comanda la paginile unui anumit tip de obiect (de ex., doar pe înregistrările Company) |
| `conditionalAvailabilityExpression` | Nu | O expresie booleană care controlează dinamic vizibilitatea (vezi mai jos) |
## Comenzi headless
Un element din meniul de comenzi asociat cu un [headless front component](/l/ro/developers/extend/apps/layout/front-components#headless-vs-non-headless) este modalitatea standard de a oferi o acțiune cu un singur clic — de a rula cod, de a naviga sau de a confirma și executa. Pagina Front Components acoperă [SDK Command components](/l/ro/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) care gestionează modelul acțiune-și-demontare.
Un flux tipic: o componentă headless afișează `<Command execute={...} />` (vezi [exemplul complet](/l/ro/developers/extend/apps/layout/front-components#sdk-command-components)), iar elementul de meniu de comandă îl indică:
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## Expresii de disponibilitate condițională
Câmpul `conditionalAvailabilityExpression` vă permite să controlați când este vizibilă o comandă în funcție de contextul paginii curente. Importați variabile tipizate și operatori din `twenty-sdk` pentru a construi expresii:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` implică deja o selecție care nu este goală — folosește `numberOfSelectedRecords` doar pentru valori specifice (de ex. `>= 2`).
</Note>
### Variabile de context
Acestea reprezintă starea curentă a paginii:
| Variabilă | Tip | Descriere |
| ------------------------------ | --------- | ---------------------------------------------------------------------- |
| `pageType` | `string` | Tipul paginii curente (de ex. `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Dacă componenta este redată într-un panou lateral |
| `numberOfSelectedRecords` | `number` | Numărul de înregistrări selectate în prezent |
| `isSelectAll` | `boolean` | Dacă "select all" este activ |
| `selectedRecords` | `array` | Obiectele înregistrărilor selectate |
| `favoriteRecordIds` | `array` | ID-urile înregistrărilor marcate ca favorite |
| `objectPermissions` | `object` | Permisiuni pentru tipul de obiect curent |
| `targetObjectReadPermissions` | `object` | Permisiuni de citire pentru obiectul țintă |
| `targetObjectWritePermissions` | `object` | Permisiuni de scriere pentru obiectul țintă |
| `featureFlags` | `object` | Steaguri de caracteristici active |
| `objectMetadataItem` | `object` | Metadatele tipului de obiect curent |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Dacă vizualizarea curentă are un filtru soft-delete |
### Operatori
Combinați variabilele în expresii booleene:
| Operator | Descriere |
| ----------------------------------- | ----------------------------------------------------------------------- |
| `isDefined(value)` | `true` dacă valoarea nu este null/undefined |
| `isNonEmptyString(value)` | `true` dacă valoarea este un șir nevid |
| `includes(array, value)` | `true` dacă array-ul conține valoarea |
| `includesEvery(array, prop, value)` | `true` dacă proprietatea fiecărui element include valoarea |
| `every(array, prop)` | `true` dacă proprietatea este truthy pentru fiecare element |
| `everyDefined(array, prop)` | `true` dacă proprietatea este definită pentru fiecare element |
| `everyEquals(array, prop, value)` | `true` dacă proprietatea este egală cu valoarea pentru fiecare element |
| `some(array, prop)` | `true` dacă proprietatea este truthy pe cel puțin un element |
| `someDefined(array, prop)` | `true` dacă proprietatea este definită pe cel puțin un element |
| `someEquals(array, prop, value)` | `true` dacă proprietatea este egală cu valoarea pe cel puțin un element |
| `someNonEmptyString(array, prop)` | `true` dacă proprietatea este un șir nevid pe cel puțin un element |
| `none(array, prop)` | `true` dacă proprietatea este falsy pentru fiecare element |
| `noneDefined(array, prop)` | `true` dacă proprietatea este nedefinită pentru fiecare element |
| `noneEquals(array, prop, value)` | `true` dacă proprietatea nu este egală cu valoarea pe niciun element |

View File

@@ -0,0 +1,849 @@
---
title: Componente front-end
description: Construiți componente React care se afișează în interfața Twenty, cu izolare în sandbox.
icon: window-maximize
---
Componentele front-end sunt componente React care se afișează direct în interfața Twenty. Acestea rulează într-un **Web Worker** izolat folosind Remote DOM — codul se execută într-un iframe cu origine opacă, într-un mediu izolat (sandboxed), însă interfața sa se redă în continuare nativ în pagină, în loc să fie limitată la acel iframe.
<Warning>
Componentele Front sunt încă în curs de dezvoltare activă. Codul tău se execută pe un DOM parțial, nu pe o pagină reală a browserului, astfel încât utilizările avansate pot eșua, adesea în tăcere. Vezi [Limitări actuale](#current-limitations).
</Warning>
## Unde pot fi utilizate componentele front-end
Componentele front-end pot fi afișate în trei locații în cadrul Twenty:
* **Panou lateral** — Componentele front-end care nu sunt headless se deschid în panoul lateral din dreapta. Acesta este comportamentul implicit atunci când o componentă front-end este declanșată din meniul de comenzi.
* **Widgeturi (tablouri de bord și pagini de înregistrare)** — Componentele frontale pot fi încorporate ca widgeturi în [machetele de pagină](/l/ro/developers/extend/apps/layout/page-layouts). La configurarea unui tablou de bord sau a machetei unei pagini de înregistrare, utilizatorii pot adăuga un widget de componentă front-end.
* **Setările aplicației** — Definită cu [`defineSettingsFrontComponent()`](#custom-settings-component), componenta front-end este afișată ca o secțiune în interiorul filei **Settings** a aplicației, în locul interfeței UI implicite de configurare a variabilelor.
O componentă frontală, de una singură, nu este accesibilă din interfața utilizatorului — trebuie să o *expui*. Cele trei moduri de a face asta sunt:
* **Asociază-l cu un [element de meniu de comenzi](/l/ro/developers/extend/apps/layout/command-menu-items)** — îl înregistrează în meniul de comenzi (Cmd+K) și, opțional, ca acțiune rapidă fixată.
* **Încorporează-l ca widget într-o [machetă de pagină](/l/ro/developers/extend/apps/layout/page-layouts)** — îl plasează pe pagina de detalii a unei înregistrări sau pe un tablou de bord.
* **Definește-o cu [`defineSettingsFrontComponent()`](#custom-settings-component)** — o afișează ca o secțiune în interiorul filei **Settings** a aplicației, în locul interfeței UI implicite de configurare a variabilelor.
## Exemplu de bază
Cel mai rapid mod de a vedea o componentă frontală în acțiune este să o asociezi cu un [`defineCommandMenuItem`](/l/ro/developers/extend/apps/layout/command-menu-items), astfel încât să apară ca un buton de acțiune rapidă în colțul din dreapta sus al paginii:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello from my app!</h1>
<p>This component renders inside Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
După sincronizarea cu `yarn twenty dev` (sau prin rularea o singură dată a comenzii `yarn twenty apply`), acțiunea rapidă apare în colțul din dreapta sus al paginii:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Buton de acțiune rapidă în colțul din dreapta sus" />
</div>
Faceți clic pe el pentru a afișa componenta inline.
## Câmpuri de configurare
| Câmp | Obligatoriu | Descriere |
| --------------------- | ----------- | --------------------------------------------------------------------------- |
| `universalIdentifier` | Da | ID unic stabil pentru această componentă |
| `component` | Da | O funcție de componentă React |
| `name` | Nu | Nume afișat |
| `description` | Nu | Descriere a ceea ce face componenta |
| `isHeadless` | Nu | Setați la `true` dacă componenta nu are interfață vizibilă (vedeți mai jos) |
## Plasarea unei componente front-end pe o pagină
Dincolo de comenzi, puteți încorpora o componentă front-end direct într-o pagină de înregistrare adăugând-o ca widget într-un **layout de pagină**. Vezi [Machete de pagină](/l/ro/developers/extend/apps/layout/page-layouts) pentru detalii.
## Componentă de setări personalizată
Pentru a înlocui interfața UI de configurare a variabilelor generată automat din fila **Settings** a aplicației cu propria ta componentă, definește-o cu `defineSettingsFrontComponent` în loc de `defineFrontComponent`. Acesta folosește aceleași [câmpuri de configurare](#configuration-fields) (cu excepția lui `isHeadless`, care nu este acceptat deoarece o componentă de setări afișează întotdeauna o interfață vizibilă) și, în plus, marchează componenta ca interfața de setări a aplicației.
Componenta este afișată ca o secțiune în interiorul filei Settings, nu ca un înlocuitor pentru întreaga filă. Secțiunile gestionate de sistem ale Twenty — actualizare automată, URL aplicație și conexiuni — sunt întotdeauna afișate deasupra și nu pot fi suprascrise de aplicație.
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
Este permisă o singură componentă front de setări pentru fiecare aplicație; declararea a mai mult de una duce la eșecul build-ului. Atunci când este prezentă, fila **Settings** a aplicației afișează această componentă în locul interfeței implicite de configurare a variabilelor.
## Headless vs non-headless
Componentele front-end au două moduri de randare controlate de opțiunea `isHeadless`:
**Non-headless (implicit)** — Componenta afișează o interfață vizibilă. Când este declanșată din meniul de comenzi, se deschide în panoul lateral. Acesta este comportamentul implicit când `isHeadless` este `false` sau omis.
**Headless (`isHeadless: true`)** — Componenta se montează invizibil în fundal. Nu deschide panoul lateral. Componentele headless sunt concepute pentru acțiuni care execută logică și apoi se demontează — de exemplu, rularea unei sarcini asincrone, navigarea la o pagină sau afișarea unui modal de confirmare. Se potrivesc în mod natural cu componentele Command din SDK descrise mai jos.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
useEffect(() => {
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-tracker',
description: 'Tracks record views silently',
isHeadless: true,
component: SyncTracker,
});
```
Deoarece componenta returnează `null`, Twenty omite redarea unui container pentru ea — nu apare spațiu gol în layout. Componenta are în continuare acces la toate hook-urile și la API-ul de comunicare cu gazda.
## Componentele Command din SDK
Pachetul `twenty-sdk` oferă patru componente ajutătoare Command, concepute pentru componente front-end headless. Fiecare componentă execută o acțiune la montare, gestionează erorile afișând o notificare snackbar și demontează automat componenta front-end la final.
Importați-le din `twenty-sdk/front-component`:
* **`Command`** — Rulează un callback asincron prin prop-ul `execute`.
* **`CommandLink`** — Navighează către o rută a aplicației. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Deschide un modal de confirmare. Dacă utilizatorul confirmă, execută callback-ul `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Deschide o pagină din panoul lateral. Props depind de `page` — de ex. `ViewRecord` primește `recordId` + `objectNameSingular` (plus un id `tab` opțional pentru a deschide înregistrarea într-un anumit tab), alte pagini primesc `pageTitle` + `pageIcon`.
Iată un exemplu complet de componentă front-end headless care folosește `Command` pentru a rula o acțiune din meniul de comenzi:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
Și un exemplu care folosește `CommandModal` pentru a cere confirmarea înainte de execuție:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/front-component';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
});
```
Și un exemplu care folosește `CommandOpenSidePanelPage` pentru a deschide înregistrarea curentă în panoul lateral, pe un tab specific. `tab` este un id de tab al layout-ului paginii (layout-urile implicite folosesc id-uri precum `company-tab-emails` sau `company-tab-timeline`; layout-urile personalizate folosesc propriul id al tab-ului). Dacă id-ul nu există în layout-ul înregistrării, se deschide în schimb tab-ul implicit:
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## Apelarea unei funcții logice
Componentele de front rulează în browser, într-un Web Worker sandboxat în interiorul unui iframe cu origine opacă, în timp ce [funcțiile logice](/l/ro/developers/extend/apps/logic/logic-functions) rulează pe server. Nu există un apel direct în același proces între cele două — în schimb, o componentă de front apelează o funcție logică prin HTTP.
O funcție logică declarată cu `httpRouteTriggerSettings` este accesibilă prin HTTP la ruta sa. `RestApiClient` tratează căile care încep cu `/s/` ca rute ale aplicației, le rezolvă către URL-ul de la care sunt deservite funcțiile tale și le autentifică folosind `TWENTY_APP_ACCESS_TOKEN`.
> **În Twenty Cloud, funcțiile logice declanșate prin HTTP sunt deservite pe un domeniu dedicat pentru fiecare spațiu de lucru** la `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Pentru apelanții externi, copiază URL-ul exact din setările **HTTP trigger** ale funcției sau din fila **Settings** a aplicației.
O componentă de front headless poate efectua apelul la montare prin componenta `Command`, apoi se demontează automat:
```tsx src/front-components/sync-prs.tsx
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
Calea transmisă către `RestApiClient` este proprietatea `httpRouteTriggerSettings.path` a funcției logice, cu prefixul `/s`. Păstrează `isAuthRequired: true`; `TWENTY_APP_ACCESS_TOKEN` pe care Twenty îl generează pentru componenta ta autentifică cererea:
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_APP_ACCESS_TOKEN` este injectat automat — vezi [Application variables](#application-variables). Deoarece variabilele de aplicație secrete nu sunt niciodată expuse componentelor de front, păstrează cheile API și altă logică sensibilă în funcția logică, nu în componenta de front.
</Note>
### Apelarea API-ului REST Twenty
Pentru a apela rute HTTP ale aplicației sau pentru a citi și scrie înregistrări Twenty dintr-un front component, folosește `RestApiClient` din `twenty-client-sdk/rest`. Trimite căile de forma `/s/...` către URL-ul de bază al funcțiilor spațiului tău de lucru, iar orice altă cale, inclusiv `/rest/...`, către `TWENTY_API_URL`.
Acționează întotdeauna ca persoana care se uită la pagină. `runAs: 'application'` este o opțiune exclusivă pentru funcțiile logice: o componentă nu primește niciodată propriul token al aplicației tale, astfel că solicitarea lui aici generează o eroare. Pune activitatea care necesită propriul acces al aplicației în spatele unei funcții logice și apeleaz-o în schimb pe aceasta.
| Metodă | Descriere |
| --------------------------------- | ----------------------------------------------------------------------------- |
| `get(path, options?)` | Trimite o cerere `GET` |
| `post(path, body?, options?)` | Trimite o cerere `POST` |
| `put(path, body?, options?)` | Trimite o cerere `PUT` |
| `patch(path, body?, options?)` | Trimite o cerere `PATCH` |
| `delete(path, options?)` | Trimite o cerere `DELETE` |
| `request(method, path, options?)` | Cerere generică cu orice metodă HTTP |
| `resolveUrl(path, options?)` | Rezolvă o cale la URL-ul ei complet fără a trimite o cerere (pentru link-uri) |
`options` acceptă `headers`, `query` (un „record” de parametri de query-string; valorile nule sau nedefinite sunt omise) și un `AbortSignal` prin `signal`. Un obiect `body` care nu este de tip `FormData` este serializat automat în JSON. La un `401`, clientul reîmprospătează o dată tokenul de acces prin gazdă și reîncearcă cererea.
URL-ul de bază și tokenul sunt rezolvate din mediu în mod implicit. Transmite suprascrieri către constructor atunci când este necesar — de exemplu, în teste:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
Cererile eșuate declanșează o eroare `RestApiClientError` care expune `status`, `statusText`, `url` și `body` analizat:
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const people = await client.get('/rest/people', {
query: { limit: 10 },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## Accesarea contextului de rulare
În interiorul componentei, folosiți hook-urile SDK pentru a accesa utilizatorul curent, înregistrarea curentă și instanța componentei:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
const componentId = useFrontComponentId();
return (
<div>
<p>User: {userId}</p>
<p>Record: {recordId ?? 'No record context'}</p>
<p>Component: {componentId}</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
name: 'record-info',
component: RecordInfo,
});
```
Hook-uri disponibile:
| Hook | Returnează | Descriere |
| --------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
| `useUserId()` | `string` sau `null` | ID-ul utilizatorului curent |
| `useSelectedRecordIds()` | `string[]` | Toate ID-urile înregistrărilor selectate (array gol dacă nu este selectată niciuna) |
| `useRecordId()` | `string` sau `null` | **Învechit.** Folosiți `useSelectedRecordIds()` în schimb |
| `useFrontComponentId()` | `string` | ID-ul acestei instanțe de componentă |
| `useTimelineActivityId()` | `string` sau `null` | ID-ul activității curente din cronologie la randarea unui rând personalizat din cronologie |
| `useColorScheme()` | `'light'` sau `'dark'` | Schema de culori activă a interfeței de utilizator a gazdei (`System` este deja rezolvat) |
| `useFrontComponentExecutionContext(selector)` | variază | Accesați întregul context de execuție cu o funcție selector |
## Variabile de aplicație
Variabilele de aplicație definite în [`defineApplication()`](/l/ro/developers/extend/apps/config/application) cu `isSecret: false` sunt disponibile în componentele de interfață prin utilitarul `getApplicationVariable`:
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
Variabilele secrete (`isSecret: true`) **nu** sunt expuse componentelor de interfață. Acestea sunt disponibile doar în [funcțiile logice](/l/ro/developers/extend/apps/logic/logic-functions), care rulează pe server. Acest lucru împiedică trimiterea către browser a valorilor sensibile, cum ar fi cheile API.
</Warning>
`getApplicationVariable` returnează întotdeauna un **string** (sau `undefined`), indiferent de `type`‑ul declarat al variabilei. Stringul este serializat în mod consecvent în funcție de tip (valorile boolean ca `"true"` / `"false"`, numerele ca stringuri zecimale, array‑urile / obiectele ca JSON), în același format folosit pentru `process.env` în funcțiile logice — parsează‑l tu însuți (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Vezi [Tipuri de variabile](/l/ro/developers/extend/apps/config/application#variable-types).
Următoarele variabile de sistem sunt întotdeauna disponibile prin `process.env`:
| Variabilă | Descriere |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | URL-ul de bază al API-ului de bază Twenty |
| `TWENTY_APP_ACCESS_TOKEN` | Token cu durată scurtă, limitat la rolul persoanei autentificate intersectat cu cel al aplicației tale, astfel încât o componentă nu poate face niciodată mai mult decât persoana care o privește |
### `TWENTY_FUNCTIONS_URL`
Twenty injectează, de asemenea, `TWENTY_FUNCTIONS_URL` în front components și în funcțiile logice: URL-ul de bază de la care sunt deservite funcțiile logice ale aplicației tale declanșate prin HTTP.
Există deoarece acel URL nu este întotdeauna chiar serverul Twenty. În Twenty Cloud, rutele aplicației sunt deservite pe un domeniu dedicat pentru fiecare spațiu de lucru (`https://\<your-workspace-subdomain>.withtwenty.com` sau domeniul public principal al aplicației atunci când este configurat unul), astfel încât răspunsurile generate de aplicație să ruleze pe o origine izolată, nu pe originea aplicației Twenty. Instanțele self-hosted și locale deservesc rutele aplicației sub prefixul `/s` chiar pe server și este posibil să nu seteze deloc variabila. Deoarece URL-ul de bază variază în funcție de spațiul de lucru și de instanță, codul tău nu îl poate hardcoda — serverul injectează valoarea corectă la runtime.
Rareori ai nevoie să o citești direct. Apelează-ți rutele prin `RestApiClient` folosind o cale prefixată cu `/s/`, iar clientul îți rezolvă URL-ul: elimină prefixul `/s` și țintește `TWENTY_FUNCTIONS_URL`, folosind `\<TWENTY_API_URL>/s` ca rezervă atunci când variabila nu este setată. Folosește `resolveUrl('/s/\<path>')` pentru a obține URL-ul absolut fără a trimite o cerere, de exemplu pentru un link. Citește variabila direct doar atunci când construiești manual un URL:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## API-ul de comunicare cu gazda
Componentele front-end pot declanșa navigare, ferestre modale și notificări folosind funcții din `twenty-sdk`:
| Funcție | Descriere |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | Navigați la o pagină din aplicație |
| `openSidePanelPage(params)` | Deschideți un panou lateral |
| `closeSidePanel()` | Închideți panoul lateral |
| `openCommandConfirmationModal(params)` | Afișați un dialog de confirmare |
| `enqueueSnackbar(params)` | Afișați o notificare tip toast |
| `unmountFrontComponent()` | Demontați componenta |
| `updateProgress(progress)` | Actualizați un indicator de progres |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Încarcă un `Blob` într-un câmp FILES; returnează `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` sau `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` |
Iată un exemplu care folosește API-ul gazdei pentru a afișa un snackbar și a închide panoul lateral după finalizarea unei acțiuni:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Stocare
`localStorage` și `sessionStorage` funcționează la fel ca într-o pagină normală, cu API-ul sincron standard. Cheile tale sunt limitate la instalarea aplicației tale și la utilizatorul autentificat: nicio altă aplicație nu le poate citi, iar un alt utilizator care se conectează în același browser pornește de la un spațiu de stocare gol. Valorile scrise în `localStorage` rămân pe dispozitiv între reîncărcări; `sessionStorage` durează pe durata sesiunii de browser.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty stochează valorile în numele aplicației tale, astfel încât o operație de scriere este aplicată local imediat și salvată în fundal. Citirile nu așteaptă niciodată gazda. Nimic nu este sincronizat: valorile nu urmează utilizatorul într-un alt browser sau pe o altă mașină, așa că folosește [key-value store-ul](/l/ro/developers/extend/apps/logic/key-value-store) al unei logic function pentru orice trebuie să supraviețuiască unei schimbări de dispozitiv.
Scrierile sunt limitate, iar fiecare limită ia în calcul caracterele, nu octeții: cheile au cel mult 512 caractere, o singură valoare are cel mult 262.144 de caractere, iar fiecare spațiu de stocare are cel mult 1.048.576 de caractere per aplicație și utilizator. O scriere care depășește o limită aruncă o eroare `QuotaExceededError`, la fel ca API-ul de browser.
### Lucrul cu mai multe înregistrări
Folosiți `useSelectedRecordIds()` pentru a gestiona mai multe înregistrări selectate. Acest lucru este util pentru operațiuni în masă:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
});
```
Afișați-o cu un [element de meniu de comandă](/l/ro/developers/extend/apps/layout/command-menu-items) restricționat la selecțiile de înregistrări:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## Resurse publice
Componentele front-end pot accesa fișiere din directorul `public/` al aplicației folosind `getPublicAssetUrl`:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
Consultați [secțiunea despre resurse publice](/l/ro/developers/extend/apps/config/public-assets) pentru detalii.
## Partajare dependențe între componentele din față
În mod implicit, fiecare componentă din față grupează propria copie a bibliotecilor pe care le importă, astfel încât o aplicație cu cinci componente expediază React de cinci ori. Declară dependențe partajate în fișierul `pachet al aplicației tale. son` pentru a construi acele biblioteci o dată și a avea fiecare componentă a aplicației să le încarce dintr-un singur fișier cache:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Fiecare componentă importă apoi dependențele sale exact ca înainte - nimic nu modifică codul componentei tale:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
Câteva lucruri de știut:
* **Un pachet de dependențe partajate pentru fiecare aplicație.** Pachetul este construit din propriile dependențe ale aplicației dvs., așa că țineți controlul deplin asupra versiunilor pe care le expediați.
* **Listați specificatorii pe care îi importați.** `douăzeci și ui/input` și `douăzeci și ui/afișează` sunt două intrări; doar un nume de pachet nu acoperă subcăile sale. Listarea `react` acoperă automat `react/jsx-runtime`.
* **Distribuie `react-dom/client` alături de `react`.** Fiecare componentă redă prin `createRoot`, astfel lăsând-o înseamnă că fiecare componentă încă lipsește React DOM.
* \*\*Pachetul este cacheat. \* Este servită sub un URL content-hash cu o geocutie mutabilă de lungă durată, astfel încât acesta este descărcat o dată și reutilizat pe toate componentele aplicației până când una dintre dependențele sale se schimbă.
* **Componente care importă niciunul dintre pachetele partajate nu îl descarcă niciodată.**
## Stilizare
Componentele front-end acceptă mai multe abordări de stilizare. Puteți folosi:
* **Stiluri inline** — `style={{ color: 'red' }}`
* **Componente UI Twenty** — biblioteca proprie de componente a Twenty; vezi [Folosirea componentelor UI Twenty](#using-twenty-ui-components) mai jos
* **Emotion** — CSS-in-JS cu `@emotion/react`
* **Styled-components** — pattern-uri `styled.div`
* **Tailwind CSS** — clase utilitare
* **Orice bibliotecă CSS-in-JS** compatibilă cu React
## Folosirea componentelor UI Twenty
Twenty livrează biblioteca sa de componente ca pachetul [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). Componentele frontend îl pot folosi pentru butoane, etichete, pastile de stare, chips, avataruri, pictograme, tipografie și tokeni de temă care se potrivesc automat cu tema luminoasă și întunecată a spațiului de lucru.
### Instalare
Adaugă pachetul în aplicația ta, fixat la versiunea cu care este livrată instanța ta de Twenty:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` este inclus în componenta ta frontend la momentul build-ului, astfel încât trebuie să fie doar o dependență a aplicației tale — nu este nimic de configurat la runtime.
### Importarea componentelor
Importă din subpath-ul corespunzător, nu din rădăcina pachetului, astfel încât doar componentele pe care le folosești să ajungă în bundle-ul tău:
| Subpath | Ce exportă |
| --------------------------- | -------------------------------------------------- |
| `twenty-ui/input` | `Button` și câmpuri de formular |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` și altele |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` și altele |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` și altele |
| `twenty-ui/icon` | Componente `Icon*` (de ex. `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
const StyledWidget = () => {
return (
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
<Button title="Click me" onClick={() => alert('Clicked!')} />
<Tag text="Active" color="green" />
<Status color="green" text="Online" />
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
name: 'styled-widget',
component: StyledWidget,
});
```
### Pictograme
Importă pictograme individuale din `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Fiecare pictogramă denumită este eliminată prin tree-shaking, astfel încât importarea câtorva adaugă foarte puțin la dimensiunea bundle-ului. Evită `IconsProvider`, `useIcons` și `iconsState` — acestea încarcă întregul set de pictograme Tabler (câțiva MB).
### Teme și tokeni de temă
Componentele Twenty UI se potrivesc automat cu tema luminoasă și întunecată a spațiului de lucru — renderer-ul aplică schema de culori activă pe gazdă, iar componentele își determină culorile în funcție de aceasta.
Pentru a folosi aceiași tokeni de design în propriile tale stiluri inline, apelează hook-ul `useTheme()`. Acesta returnează tokenii de temă ai Twenty (spațiere, culori, raze, fonturi) conectați la tema activă, fără a necesita vreo configurare `ThemeProvider` în componenta ta:
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
Deoarece `useTheme()` este un hook, citești tokenii în interiorul corpului componentei, astfel încât valorile reflectă întotdeauna tema activă în timp real. Aceeași hartă de tokeni este exportată și ca o constantă `themeCssVariables`, dar preferă `useTheme()` în componentele frontend — o constantă la nivel de modul care dereferențiază `themeCssVariables` poate fi nedefinită în timp ce manifestul aplicației este extras.
Pentru a ramifica explicit în funcție de schema activă, citește-o cu `useColorScheme()` din `twenty-sdk/front-component`, care returnează `'light'` sau `'dark'`.
## Limitări actuale
Componentele Front sunt în curs de dezvoltare activă. Redarea, stilizarea, gestionarea evenimentelor, măsurarea elementelor și spațiul de stocare al browserului funcționează bine. Orice ajunge *dincolo de* acestea (apelarea unei metode DOM pe un ref, observarea redimensionărilor elementelor, crearea unui portal în afara arborelui tău) lipsește sau este incomplet astăzi, iar majoritatea eșuează în tăcere: fără excepție și fără eroare TypeScript, deoarece scheletul este tipizat pentru întregul DOM al browserului.
Dacă unul dintre aceste lucruri te blochează, [deschide un tichet](https://github.com/twentyhq/twenty/issues/new/choose) ca să fie prioritar.
### Layout și măsurare
Elementele se pot măsura singure: gazda reflectă geometria în sandbox, astfel încât citirile sunt efectuate local, dar pot avea un decalaj de până la un cadru, iar prima citire a unui element nemăsurat anterior returnează zerouri. După scriere, recitește într-un callback `requestAnimationFrame` sau într-un efect.
| API | Ce se întâmplă |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Funcționează, din oglindă; setarea `scrollTop` / `scrollLeft` nu are efect |
| `getClientRects()`, `window.matchMedia()` | Aruncă o excepție |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Funcționează, raportând **viewportul browserului**; dimensiunea propriului widget este `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Returnează doar stilurile inline ale elementului, niciodată cascada calculată la nivel de gazdă |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (verificările cu `typeof` funcționează) |
| `MutationObserver` | Funcționează, inclusiv `subtree`, `attributeFilter`, valorile vechi și `takeRecords()` |
Poziționarea prin `getBoundingClientRect` funcționează acum, dar orice urmărește modificările de dimensiune prin `ResizeObserver` (recharts `ResponsiveContainer`, `autoUpdate` din Floating UI) tot nu funcționează. Oricum, preferă CSS pentru layout: stylesheet-ul tău ajunge la pagina reală, astfel încât flexbox, grid, `aspect-ratio`, `clamp()` și `@container` se comportă normal, fără întârziere de cadru.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` și `queueMicrotask` funcționează fără prefixul `window.`. Numai `window.requestAnimationFrame(...)` și cele similare aruncă o excepție.
</Note>
### Acces DOM
Un `ref` îți oferă un element din sandbox, nu un `HTMLElement`.
| Ce scrii | Ce se întâmplă | Folosește în schimb |
| ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Aruncă o excepție | Componente controlate; citește valorile din `event.target` |
| `element.classList.add(...)` | Aruncă o excepție (`classList` este `undefined`) | Construiește singur șirul `className` |
| `document.createTreeWalker()` | Aruncă o excepție | `querySelector()` / `querySelectorAll()` și `getElementById()` funcționează; `getElementsByClassName()` funcționează și el, dar returnează o colecție statică, nu o `HTMLCollection` live |
| `document.activeElement` | Întotdeauna `undefined` | Urmărește focusul cu `onFocus` / `onBlur` |
| `canvas` | Nu redă nimic, fără eroare | SVG sau desenează în afara ecranului și afișează rezultatul într-un element `img` |
| `createPortal(node, document.body)` | Nu redă nimic, în timp ce `isConnected` raportează succes | Suprapuneri inline cu `position: absolute` sau transmite bibliotecii propriul tău element container |
Golul portalului este motivul pentru care popover-urile Radix, Headless UI, MUI și react-select nu redau nimic în mod implicit. Majoritatea acceptă o proprietate de tip container; indică-i un element pe care l-ai redat.
### Evenimente
Mouse, pointer, touch, drag, tastatură, focus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` și `animationend`/`transitionend` trec către gazdă, plus câteva per element: `load`/`error` pe `img`, clipboard și compoziție pe `input`/`textarea`, media pe `video`/`audio`, `toggle` pe `details`/`dialog`. Orice altceva (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, pointer capture, `onLoad` de pe `img`) este eliminat fără avertisment.
`document.addEventListener()` și `window.addEventListener()` se înregistrează fără eroare și nu se declanșează niciodată, motiv pentru care un drag se oprește imediat ce pointerul părăsește elementul de pe care a început. `event.preventDefault()` nu trece nici el; trimiterea formularelor, `dragover`/`drop` și clicurile pe linkuri sunt deja protejate pentru tine.
### Atribute și stilizare
Fiecare element își transmite propriile proprietăți către DOM-ul gazdă (`href` pe `a`, `src`/`alt` pe `img`, `value`/`placeholder`/`disabled` pe `input` etc.), plus un set comun pe fiecare element: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` și orice atribut `aria-*` / `data-*` (cu cratimă, astfel încât `ariaLabel` este omis). Orice în afara acestora este ignorat în tăcere, așa că exprimă starea personalizată ca `data-*`.
CSS-ul componentelor, fie din `import './styles.css'`, CSS-in-JS sau un element `style`, este injectat în `head`-ul paginii gazdă **fără scope (unscoped)**. Astfel numele de clase intră în coliziune cu cele ale Twenty (prefixează-le și nu scrie niciodată un selector simplu `div { ... }`), iar `@media` se potrivește cu fereastra browserului, nu cu widgetul tău (folosește `@container` cu propriul tău `container-type`). Proprietățile `style` inline nu sunt afectate.
### Stocare și rețea
`localStorage` și `sessionStorage` sunt furnizate de Twenty, nu de browser: componenta rulează într-un worker cu o origine opacă, astfel încât gazda stochează valorile în numele aplicației tale. Consultă secțiunea [storage](#storage) pentru aria lor de aplicare și limite. IndexedDB, cookie-urile, Cache API și `BroadcastChannel` rămân indisponibile. Pentru a păstra starea între dispozitive, apelează o [logic function](/l/ro/developers/extend/apps/logic/logic-functions) și folosește [key-value store-ul](/l/ro/developers/extend/apps/logic/key-value-store) acesteia.
`fetch` funcționează, cu unele rezerve:
* Apelurile către Twenty API și către rutele aplicației tale sunt proxate de gazdă, așa că preferă [`RestApiClient`](#calling-the-twenty-rest-api). La apelurile proxate, `AbortSignal` și celelalte opțiuni `RequestInit` sunt eliminate, iar doar corpurile de tip `string` și `URLSearchParams` sunt acceptate.
* Alte origini părăsesc sandbox-ul cu `Origin: null`, astfel încât un API terț răspunde doar dacă trimite `Access-Control-Allow-Origin: *`. Apelează-l dintr-o logic function în schimb.
* `fetch('/rest/people')` nu este niciodată asociat cu Twenty API, deoarece sandbox-ul nu are un URL de pagină față de care să rezolve o cale relativă.
### Captură media
`navigator.mediaDevices.getUserMedia()` și `MediaRecorder` funcționează în componentele front prin sandbox polyfills, astfel încât codul standard de înregistrare rulează neschimbat, iar `MediaRecorder.isTypeSupported` răspunde pentru combinații obișnuite de container/codec. Obiectele detaliate de constrângeri `getUserMedia` sunt acceptate, dar nu sunt transmise mai departe — gazda capturează cu valorile implicite pentru tipurile solicitate — și doar o singură captură poate fi activă la un moment dat, în toate aplicațiile. Stochează un `Blob` înregistrat cu funcția gazdă `uploadFile`.
### Alte lacune
* **Conținutul fișierului.** Un `input` de tip `file` oferă handlerului tău doar metadatele fișierului, nu și octeții, astfel încât `FileReader` nu este disponibil. Pentru a încărca un `Blob` pe care codul tău îl deține deja — de exemplu, unul produs de `MediaRecorder` — folosește funcția gazdă `uploadFile`.
* **Payload-uri drag-and-drop.** Evenimentele de tip drag sunt declanșate, dar `event.dataTransfer` este `undefined`.
* **Built-in-uri Node.** `fs`, `path` și `node:crypto` eșuează la build, așa că mută acea logică într-o [logic function](/l/ro/developers/extend/apps/logic/logic-functions). Web Crypto, `fetch`, `TextEncoder` și `URL` sunt disponibile.
* **`iframe`** este întotdeauna re-sandboxat fără `allow-same-origin`, astfel încât o încorporare care se bazează pe propria sesiune se afișează ca delogat. Nu are nici `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Elemente de meniu de navigare
description: Adăugați intrări personalizate în bara laterală a spațiului de lucru — linkuri către vizualizări salvate sau URL-uri externe.
icon: bars
---
Un **element de meniu de navigare** este o intrare în bara laterală din stânga. Folosiți `defineNavigationMenuItem()` pentru a livra linkuri personalizate în bara laterală — de obicei unul pentru fiecare [vizualizare](/l/ro/developers/extend/apps/layout/views) pe care o livrați — sau pentru a indica către URL-uri externe.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
## Puncte cheie
* `type` determină la ce face legătura elementul de meniu. Fiecare tip este asociat cu un câmp identificator specific:
| Tip | Ce face | Câmp obligatoriu |
| ------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `NavigationMenuItemType.VIEW` | Deschide o vizualizare salvată | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Deschide un URL extern | `link` |
| `NavigationMenuItemType.FOLDER` | Grupează elementele îmbricate sub o etichetă | `name` (iar elementele copil fac referire la dosar prin `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Deschide pagina implicită de index a unui obiect | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Deschide un aranjament de pagină independent | `pageLayoutUniversalIdentifier` |
* `position` controlează ordonarea în bara laterală.
* Enumul conține și `NavigationMenuItemType.RECORD`, utilizat intern pentru marcajele de favorite ale utilizatorului pentru înregistrări — nu poate fi folosit dintr-un manifest de aplicație (nu există niciun câmp pentru a face referire la o înregistrare).
* `icon` și `color` sunt opționale și personalizează aspectul intrării.
* `folderUniversalIdentifier` este de asemenea disponibil pe orice element pentru a-l îmbrica într-un părinte de tip `FOLDER`.
<Note>
**Capcană frecventă:** crearea unui obiect fără un element de meniu de navigare asociat face ca acel obiect să fie inaccesibil din bara laterală. Dacă nu este un obiect tehnic/intern, fiecare obiect personalizat ar trebui să aibă o intrare în bara laterală care să indice către acesta.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Prezentare generală
description: Integrează-ți aplicația în interfața Twenty — intrări în bara laterală, vizualizări salvate, file în pagina de detalii a înregistrărilor și componente React izolate.
icon: table-columns
---
**Stratul de layout** al unei aplicații Twenty reprezintă tot ceea ce vede utilizatorul: unde apare aplicația în bara laterală, ce vizualizări de listă oferă, cum sunt aranjate paginile de detalii ale înregistrărilor și ce componente React personalizate sunt afișate în interiorul acelor pagini.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## În această secțiune
<CardGroup cols={2}>
<Card title="Vizualizări" icon="list" href="/l/ro/developers/extend/apps/layout/views">
`defineView` — configurații salvate pentru liste: coloane vizibile, filtre, grupuri.
</Card>
<Card title="Elemente de meniu de navigare" icon="bars" href="/l/ro/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — intrări în bara laterală care trimit către vizualizări sau URL-uri externe.
</Card>
<Card title="Layouturi de pagină" icon="table-columns" href="/l/ro/developers/extend/apps/layout/page-layouts">
`definePageLayout` și `definePageLayoutTab` — file și widgeturi pe pagina de detalii a unei înregistrări.
</Card>
<Card title="Componente front-end" icon="window-maximize" href="/l/ro/developers/extend/apps/layout/front-components">
`defineFrontComponent` — componente React izolate care rulează în interiorul Twenty.
</Card>
<Card title="Elemente din meniul de comenzi" icon="terminal" href="/l/ro/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — înregistrează componente front-end ca intrări Cmd+K și acțiuni rapide.
</Card>
</CardGroup>
## Unde este afișată aplicația
| Suprafață | Ce controlează | Entitate |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Bară laterală** | O intrare personalizată care face legătura către o vizualizare salvată sau un URL extern | `defineNavigationMenuItem` |
| **Listă de înregistrări** | O configurație salvată pentru un obiect — coloane vizibile, ordine, filtre, grupuri | `defineView` |
| **Pagină de detalii a unei înregistrări** | Filele și widgeturile de pe o pagină de înregistrare (ale propriului tău obiect sau ale unuia standard) | `definePageLayout`, `definePageLayoutTab` |
| **În interiorul oricăreia dintre cele de mai sus** | Un widget React personalizat — butoane, formulare, dashboarduri, integrări | `defineFrontComponent` |
| **Setări ale aplicației** | O secțiune de configurare personalizată din fila Setări a aplicației, în locul interfeței implicite pentru variabile | `defineSettingsFrontComponent` |
| **Meniul de comenzi (Cmd+K)** | O acțiune rapidă fixată sau o comandă ascunsă | `defineCommandMenuItem` |
Componentele front rulează în interiorul unui Web Worker izolat folosind Remote DOM — acestea sunt redate *nativ* în pagină (nu într-un iframe), dar nu pot accesa direct pagina gazdă sau DOM-ul. Comunicarea cu Twenty se face printr-un API al gazdei bazat pe transmiterea de mesaje.
Sunt încă în curs de dezvoltare activă: sandbox-ul implementează un DOM parțial, astfel încât utilizările avansate pot eșua. Vezi [Limitări actuale](/l/ro/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Layouturi de pagină
description: Personalizează paginile de detaliu ale înregistrărilor — filele, widgeturile și locul în care se redau componentele front-end — folosind definePageLayout și definePageLayoutTab.
icon: table-columns
---
Un **layout de pagină** controlează modul în care este aranjată pagina de detalii a unei înregistrări: ce file apar și ce widgeturi conțin acestea. Folosește `definePageLayout()` pentru a declara un layout pentru un obiect pe care îl deții sau `definePageLayoutTab()` pentru a adăuga o singură filă la un layout care există deja (al tău sau un layout standard Twenty).
| Caz de utilizare | Entitate |
| --------------------------------------------------------------------------------------- | --------------------- |
| Definește întregul layout pentru o pagină de înregistrare pe un obiect pe care îl deții | `definePageLayout` |
| Adaugă o filă la un layout existent (obiectul tău sau unul standard) | `definePageLayoutTab` |
## definePageLayout
Folosește aceasta atunci când deții întreaga pagină de detalii — de obicei pentru un obiect personalizat pe care l-ai definit chiar tu.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
### Puncte cheie
* `type` este unul dintre `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` sau `'STANDALONE_PAGE'`. Folosiți `'RECORD_PAGE'` pentru a personaliza vizualizarea de detaliu a unui obiect specific.
* `objectUniversalIdentifier` specifică la ce obiect se aplică această machetă.
* Fiecare `tab` definește o secțiune a paginii cu un `title`, `position` și `layoutMode`: `VERTICAL_LIST` pentru paginile de înregistrări și paginile independente, `GRID` pentru dashboard-uri și `CANVAS` pentru un singur widget care ar trebui să umple zona de vizualizare a filei. O filă `VERTICAL_LIST` stivuiește widgeturile pe verticală. Widgeturile încorporate care își gestionează propria derulare, cum ar fi cronologiile, fișierele, notițele, sarcinile și fluxurile de lucru, umplu o zonă de vizualizare; câmpurile, componentele frontale, graficele și alte widgeturi fit-content se redau la înălțimea conținutului sau la înălțimea configurată. O filă `GRID` își aranjează întotdeauna widgeturile ca carduri pe o grilă cu 12 coloane. Un widget `CANVAS` nu are o poziție explicită; dacă o filă canvas conține mai multe widgeturi, acestea sunt redate la înălțimea conținutului lor în loc să umple zona de vizualizare.
* Setează explicit `layoutMode`. Dacă îl omiți, vei obține `VERTICAL_LIST` pe un `STANDALONE_PAGE` și `GRID` în rest, ceea ce rareori îți dorești pe o pagină de înregistrare.
* Fiecare `widget` dintr-o filă poate reda un [front component](/l/ro/developers/extend/apps/layout/front-components), o listă de relații sau alte tipuri de widgeturi integrate.
* Un widget `FRONT_COMPONENT` poate seta `headerCommandMenuItemUniversalIdentifiers` la o matrice ordonată de identificatori universali ai elementelor de meniu de comandă din aceeași aplicație. Aceste acțiuni apar ca butoane cu pictograme în antetul cardului widgetului și își păstrează verificările de disponibilitate și de permisiuni la nivel de comandă. Identificatorii trebuie să fie unici și trebuie să se rezolve atunci când aplicația este instalată.
* `position` pe file le controlează ordinea. Folosește valori mai mari (de ex., 50) pentru a plasa filele personalizate după cele integrate.
### Widget-uri Câmp
Un widget `FIELD` redă un câmp al înregistrării. Pentru câmpurile de relație, acesta poate încorpora și o listă de înregistrări asociate:
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` primește identificatorul universal al unui câmp de pe obiectul layout-ului.
* `fieldDisplayMode` este unul dintre `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` sau `'TABLE'`. `TABLE` încorporează o vizualizare care listează înregistrările unui câmp de relație de tip unu-la-mulți.
* `nestedRelationFieldMetadataId` este opțional și primește identificatorul universal al unui câmp de relație unu-la-mulți de pe obiectul țintă al relației, pentru a afișa în listă înregistrări aflate la două salturi de relație distanță (de ex., o pagină Company care afișează oportunitățile persoanelor companiei sau o pagină Person care afișează oportunitățile companiei persoanei). Primul salt poate fi un câmp de relație unu-la-mulți sau mulți-la-unu, al doilea trebuie să fie unu-la-mulți (relațiile de tip junction nu sunt acceptate) și necesită `fieldDisplayMode: 'TABLE'` — combinarea lui cu orice alt mod de afișare reprezintă o eroare de validare, deoarece un widget îmbricat este întotdeauna redat ca o vizualizare încorporată.
## definePageLayoutTab
Folosește aceasta atunci când vrei doar să **adaugi** o filă la un layout existent — de exemplu, o filă de analize pe pagina standard Company sau o filă de rezumat AI atașată layoutului obiectului tău.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### Puncte cheie
* `pageLayoutUniversalIdentifier` este **obligatoriu** și trebuie să indice către un layout de pagină care există deja la momentul instalării — fie un layout standard Twenty, fie unul definit de propria ta aplicație. Referințele cross-app către layouturi deținute de o altă aplicație instalată nu sunt acceptate în prezent. Când lipsește layoutul părinte, instalarea eșuează cu o eroare clară de validare.
* Pentru layout-urile standard Twenty, importați identificatorii din `twenty-sdk/define`:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Fiecare intrare de layout își expune, de asemenea, `tabs` și `widgets`, astfel încât puteți face referire la orice nivel:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Este disponibil și un alias scurt `STANDARD_PAGE_LAYOUT`:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` sunt limitate doar la această filă — fac referire la [front components](/l/ro/developers/extend/apps/layout/front-components), vizualizări etc., exact ca widgeturile definite inline în `definePageLayout`.
* `position` controlează ordonarea în raport cu filele existente din layoutul țintă. Alege o valoare care să plaseze fila ta acolo unde dorești, relativ la filele predefinite.
* Folosește aceasta în loc de `definePageLayout` atunci când vrei doar să adaugi la un layout existent. Folosește `definePageLayout` atunci când deții întregul layout.

View File

@@ -0,0 +1,113 @@
---
title: Vizualizări
description: Livrați vizualizări salvate preconfigurate — ordinea coloanelor, filtre, grupuri — pentru obiectele din aplicația dvs.
icon: list
---
O **vizualizare** este o configurație salvată pentru modul în care sunt afișate înregistrările unui obiect: ce câmpuri apar, ordinea lor, dacă sunt vizibile și ce filtre sau grupuri sunt aplicate. Utilizați `defineView()` pentru a livra vizualizări preconfigurate împreună cu aplicația.
Fiecărui obiect i se atribuie automat vizualizarea principală de listă (tabelul „All ...” pe care îl deschide un element de navigare `OBJECT`) furnizată de server — inclusiv obiectele pe care le creează aplicația dvs., cu o coloană pentru fiecare câmp declarat de aplicația dvs. Vizualizările pe care le definiți cu `defineView()` sunt întotdeauna vizualizări **suplimentare** pe lângă cea implicită.
```ts src/views/example-view.ts
import { defineView } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## Puncte cheie
* `objectUniversalIdentifier` specifică la ce obiect se aplică această vizualizare. Poate fi un obiect personalizat pe care l-ați definit sau un obiect standard Twenty.
* Vizualizarea principală a listei obiectului aparține serverului: `key` este învechit și este ignorat, astfel încât o vizualizare din manifest nu o poate revendica niciodată. Livrați un element de navigare `VIEW` dacă doriți ca vizualizarea dvs. să fie la un singur clic distanță în bara laterală.
* `fields` controlează ce coloane apar și ordinea acestora. Fiecare câmp face referire la un `fieldMetadataUniversalIdentifier`. Pentru a face referire la un câmp de sistem creat automat, cum ar fi `createdAt`, consultă [Țintirea metadatelor de sistem](/l/ro/developers/extend/apps/data/system-fields).
* Puteți declara, de asemenea, `filters`, `filterGroups`, `sorts`, `groups` și `fieldGroups` pentru configurații avansate.
* `position` controlează ordonarea atunci când există mai multe vizualizări pentru același obiect.
## Proprietăți opționale
| Proprietate | Valori | Descriere |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (implicit), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Modul în care sunt dispuse înregistrările. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` și `CALENDAR_WIDGET` există, de asemenea, dar sunt folosite intern de către widget-urile de tip page-layout.) |
| `visibility` | `ViewVisibility.WORKSPACE` (implicit), `ViewVisibility.UNLISTED` | Dacă vizualizarea este listată pentru întregul spațiu de lucru sau ascunsă din selectoare. |
| `openRecordIn` | învechit | Nu se mai aplică: locul unde se deschid înregistrările este acum o proprietate a [obiectului](/l/ro/developers/extend/apps/data/objects) (`openRecordIn` pe `defineObject()`), revenindu-se la preferința proprie a fiecărui membru. |
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Ordinea implicită de sortare. |
| `isCompact` | `boolean` | Afișare compactă a rândurilor. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Gruparea înregistrărilor (de ex. coloane kanban) după un câmp. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Agregări și dimensionare pentru coloanele kanban. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Vizualizări de tip calendar: layout, câmpul de dată care poziționează înregistrările și un câmp opțional pentru data de sfârșit. |
Toate enum-urile de mai sus sunt exportate din `twenty-sdk/define`.
## Filtre
O vizualizare poate include filtre aplicate în prealabil. Fiecare filtru are trei coordonate: **câmpul** care este filtrat, **operandul** (cum se compară) și **valoarea** (față de ce se compară). Toate cele trei trebuie să se potrivească — folosirea unui operand care nu se aplică unui tip de câmp va fi respinsă în timpul sincronizării.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Operanzi acceptați pentru fiecare tip de câmp
| Tipul câmpului | Operanzi acceptați |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Tipurile de câmp cu nume similare pot folosi operanzi complet diferiți — `SELECT` și `MULTI_SELECT` fiind un caz comun.
### Formă de valoare per operand
Câmpul `value` este întotdeauna o valoare serializabilă JSON, dar forma sa așteptată depinde de operand:
| Familie de operanzi | Formă de valoare | Exemplu |
| ----------------------------------------------------- | ---------------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` pe `SELECT` | array de chei de opțiune (șiruri de caractere) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` pe `MULTI_SELECT` | array de chei de opțiune (șiruri de caractere) | `['TAG_A']` |
| `IS`, `IS_NOT` pe `RELATION` | array de ID-uri de înregistrare (uuid-uri) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` pe câmpuri de tip text | șir | `'acme'` |
| `IS`, `IS_NOT` pe `NUMBER` | șir de caractere (valoarea) | `'5'` |
| `IS` pe `RATING` / `UUID` | șir de caractere (valoarea) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | șir de caractere (limita) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` pe `DATE` / `DATE_TIME` | șir în format ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | șir gol | `''` |
| `IS` pe `BOOLEAN` | `'true'` sau `'false'` | `'true'` |
## Cum apar vizualizările în interfața utilizatorului
O vizualizare, de una singură, nu este accesibilă din bara laterală. Pentru a o face să apară acolo, asociați-o cu un [element de meniu de navigare](/l/ro/developers/extend/apps/layout/navigation-menu-items) de tip `VIEW` care indică către `universalIdentifier` al vizualizării. Acesta este tiparul canonic: fiecare obiect personalizat primește o vizualizare index implicită generată automat la creare, astfel încât trebuie doar să livrezi intrarea din bara laterală care o deschide.

View File

@@ -0,0 +1,197 @@
---
title: Sarcini în fundal
description: Predă munca de durată sau supusă limitării ratei către lucrătorii Twenty, punând în coadă o altă execuție a unei funcții logice, în loc să rulezi totul inline.
icon: layer-group
---
O rulare a unei funcții de logică este limitată de `timeoutSeconds` (maximum 900 de secunde). Orice lucru care nu poate fi finalizat în acel interval — o resincronizare completă, un fan-out per înregistrare, un API de la o terță parte care aplică limitare de rată — trebuie împărțit în execuții mai mici.
`enqueueJobs` face exact asta: le cere lucrătorilor Twenty să ruleze mai târziu una dintre funcțiile de logică ale aplicației tale, o dată pentru fiecare payload, fiecare rulare în propriul său proces, cu propriul său buget de timeout. Apelantul returnează imediat.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## Pune rulările în coadă
Importă `enqueueJobs` din `twenty-sdk/logic-function`, indică-l către `universalIdentifier` al funcției logice de rulat și transmite câte un payload pentru fiecare rulare funcției enqueue.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
Fiecare rulare primește payloadul său ca argument al handlerului, exact ca orice alt declanșator. Ținta trebuie să aparțină **aceleiași aplicații** ca apelantul — punerea în coadă a funcției altei aplicații este respinsă cu `Logic function not found` și nu se pune nimic în coadă. Un singur apel acceptă până la `200` payloaduri.
<Note>
`enqueueJobs` revine imediat ce joburile sunt acceptate, nu când au fost rulate. Nu întoarce rezultatele țintelor — fă ca fiecare țintă să scrie ce produce în [magazinul cheie-valoare](/l/ro/developers/extend/apps/logic/key-value-store) sau într-o înregistrare din spațiul de lucru dacă trebuie să le citești ulterior.
</Note>
<Note>
Ajutorul mai vechi `enqueueJob`, care pune în coadă un singur job per apel, este depreciat. Folosește în schimb `enqueueJobs` cu o listă `payloads` cu un element.
</Note>
## Opțiuni job
Opțiunile se aplică fiecărei rulări din lot.
| Opțiune | Implicit | Interval | Ce face |
| ------------ | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | Numărul total de încercări suplimentare în coadă. Reîncercările solicitate de aplicație sunt limitate la `3`. Crește această valoare numai pentru handleri care sunt siguri de rulat de două ori. |
| `delayMs` | `0` | `0`–`604800000` (7 zile) | Așteaptă atât timp înainte ca rulările să devină eligibile. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**Prioritatea nu este încă configurabilă.** Joburile puse în coadă rulează întotdeauna cu cea mai joasă prioritate, astfel încât activitatea platformei să nu fie niciodată întârziată de joburile aplicației. Controlul asupra priorității va fi disponibil în curând.
</Note>
Rularea pusă în coadă moștenește utilizatorul activ al funcției care a pus-o în coadă, astfel încât acționează cu aceleași permisiuni.
## Reîncercați după un eșec temporar
Twenty nu reîncearcă fiecare excepție din codul aplicației. O eroare obișnuită lansată este tratată ca un eșec permanent. Pentru un eșec temporar, o funcție logică pusă în coadă poate solicita până la trei reîncercări prin lansarea `RetryableLogicFunctionError`.
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
Lansați direct `RetryableLogicFunctionError` atunci când este posibil. Dacă o extindeți, nu îi înlocuiți `name`: Twenty recunoaște numele serializat `RetryableLogicFunctionError` în toate mediile de execuție.
`retryCount` este `0` pentru execuția inițială și crește doar atunci când codul aplicației solicită o reîncercare. `maxRetries` este cel mult `3` și poate fi mai mic atunci când sarcina din coadă are o limită totală de reîncercări mai mică. Eșecurile platformei nu cresc `retryCount`, deși consumă în continuare bugetul general de siguranță al cozii.
Coada întârzie încercările de reîncercare folosind backoff exponențial și jitter. Întârzierea exactă nu este garantată în mod intenționat, astfel că codul aplicației nu ar trebui să depindă de producerea unei reîncercări la un moment precis. După ce se atinge `maxRetries`, o altă `RetryableLogicFunctionError` este înregistrată ca eșecul final al aplicației, fără o altă execuție.
<Warning>
Reîncercările rulează din nou întregul handler și se pot produce după ce unele efecte secundare au reușit. Faceți handlerul idempotent înainte de a solicita reîncercări.
</Warning>
## Folosește-l: parcurge pe pagini o sincronizare lungă
Forma clasică este o funcție care pune în coadă *ea însăși* cu următorul cursor. Fiecare rulare procesează o pagină de lucru bine în interiorul propriului timeout, iar lanțul se oprește când nu mai rămâne nimic.
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## Dispersare pe înregistrare
Când munca este în mod natural pe element, pune în coadă un job per element într-un singur apel și lasă workerii să le proceseze în paralel în loc să iterezi inline.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## Bune practici pentru muncă de lungă durată
Două reguli acoperă aproape orice job lung: folosește recursie în locul buclelor și procesează un segment limitat per rulare.
O rulare care încearcă să facă totul este modul de eșec — atinge timeout-ul, iar la o reîncercare începe din nou totul de la zero. În schimb, dimensionează un segment astfel încât să se termine confortabil în interiorul `timeoutSeconds`, persistă-ți poziția și pune în coadă următoarea rulare.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
Ce îl face robust:
* **Dimensionează segmentul pornind de la cel mai lent element, nu de la medie.** `CHUNK_SIZE × worst-case item time` trebuie să încapă în `timeoutSeconds` cu marjă, altfel „coada” unui segment se pierde când rularea este întreruptă.
* **Fă condiția de terminare explicită.** Apelează recursiv doar cât timp a revenit un segment complet. Un lanț care se oprește doar pe baza „niciun rezultat” va continua la nesfârșit dacă sursa returnează vreodată o pagină scurtă la mijloc.
* **Păstrează progresul înainte de a pune în coadă următoarea rulare**, astfel încât o verigă eșuată să repornească de la ultimul segment finalizat în loc de la început.
* **Păstrează fiecare segment idempotent.** Reprocesarea unui segment după o reîncercare nu trebuie să ducă la scrieri duble — leagă scrierile de înregistrarea sau ID-ul extern pe care îl procesezi.
* **Preferă un lanț segmentat în locul unei dispersări uriașe** atunci când munca lovește o terță parte cu limită de rată: un lanț cu `delayMs` își dozează singur ritmul, în timp ce mii de joburi puse în coadă deodată devin toate eligibile imediat.

View File

@@ -0,0 +1,269 @@
---
title: Conexiuni
description: Permite aplicației tale să acționeze în numele unui utilizator în servicii ale terților prin OAuth.
icon: plug
---
Conexiunile sunt acreditări pe care un utilizator le deține pentru un serviciu extern (Linear, GitHub, Slack, ...). Aplicația ta declară **cum** sunt obținute acele acreditări — un **furnizor de conexiune** — și le folosește în timpul execuției pentru a efectua apeluri autentificate către API-ul terț.
În prezent este acceptat doar OAuth 2.0. Tipurile viitoare de acreditări (jetoane de acces personale, chei API, autentificare de bază) se vor integra în aceeași interfață — aplicațiile care deja folosesc `defineConnectionProvider({ type: 'oauth', ... })` nu vor trebui să migreze.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Declară cum sunt obținute conexiunile aplicației tale">
Un furnizor de conexiune descrie handshake-ul OAuth de care are nevoie aplicația ta. Utilizatorul face clic pe "Adaugă conexiune" în setările aplicației tale, completează ecranul de consimțământ al furnizorului și este creată o înregistrare `ConnectedAccount` în spațiul său de lucru.
O configurație funcțională are nevoie de **două fișiere** — furnizorul de conexiune și o declarație `serverVariables` corespunzătoare în `defineApplication` care conține acreditările clientului OAuth.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Puncte cheie:
* `name` este șirul identificator unic folosit în `listConnections({ providerName })` (kebab-case, trebuie să corespundă `^[a-z][a-z0-9-]*$`).
* `displayName` apare în fila de setări a aplicației și în lista de instrumente AI.
* `clientIdVariable` / `clientSecretVariable` sunt **nume**, nu valori — trebuie să se potrivească cheilor declarate în `defineApplication.serverVariables`. Valorile reale `client_id` și `client_secret` sunt introduse de administratorul serverului prin interfața de înregistrare a aplicației și nu sunt niciodată comise în repo-ul tău.
* Folosește `serverVariables` (nu `applicationVariables`) — acreditările OAuth sunt la nivel de server și există o singură aplicație OAuth pentru fiecare server Twenty.
* Până când ambele `serverVariables` sunt completate, fila de setări a aplicației afișează un indiciu "necesită administrator de server" și butonul "Adaugă conexiune" este dezactivat.
* `type: 'oauth'` este singura valoare acceptată în prezent. Discriminatorul este compatibil cu versiuni viitoare: tipurile viitoare (`'pat'`, `'api-key'`, ...) vor adăuga blocuri noi de sub-configurație alături de `oauth`.
URL-ul de callback OAuth pe care furnizorul tău trebuie să îl includă pe lista albă este:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="Rulează o funcție logică la conectare" description="Reacționează în momentul în care o conexiune este stabilită">
Unii furnizori îți furnizează date în momentul conectării, pe care trebuie să le stochezi înainte ca conexiunea să poată fi utilizată — exemplul clasic este Slack, unde răspunsul OAuth identifică `team_id` al spațiului de lucru după care vor fi indexate evenimentele primite. Setează `onConnectLogicFunction` pentru a face referire la o funcție logică din aceeași aplicație (prin `universalIdentifier`), iar aceasta rulează imediat după ce `ConnectedAccount` este creat.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
Hook-ul rulează **asincron în spațiul de lucru care se conectează** (este pus în coadă, nu este așteptat), astfel încât un hook lent sau care eșuează nu blochează și nu întrerupe niciodată callback-ul OAuth — fă-l idempotent și lasă-l să își gestioneze singur reîncercările. Handlerul primește:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
De acolo folosește `getConnection(connectedAccountId)` pentru a citi tokenul de acces proaspăt și a apela API-ul furnizorului (de ex. Slack `auth.test`) sau pentru a stoca o mapare în [magazinul cheie–valoare](/l/ro/developers/extend/apps/logic/key-value-store).
</Accordion>
<Accordion title="Rulează o funcție logică la deconectare" description="Curăță resursele atunci când o conexiune este eliminată">
Orice revendică o aplicație la momentul conectării trebuie eliberat atunci când conexiunea este eliminată. O integrare Slack care revendică un `team_id` la conectare, de exemplu, trebuie să elibereze acea revendicare astfel încât un alt spațiu de lucru să poată conecta aceeași echipă Slack. Setează `onDisconnectLogicFunction` pentru a face referire la o funcție logică din aceeași aplicație, iar aceasta rulează imediat după ce `ConnectedAccount` este șters.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
La fel ca hook-ul on-connect, acesta rulează **asincron în spațiul de lucru care se deconectează** și nu blochează niciodată deconectarea. Handler-ul primește aceeași structură de payload:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
`ConnectedAccount` nu mai există atunci când rulează hook-ul, astfel încât `getConnection(connectedAccountId)` nu se mai rezolvă. Tot ceea ce îi este necesar procesului de curățare (un `team_id`, un ID extern de abonament) trebuie să fi fost scris în [key-value store](/l/ro/developers/extend/apps/logic/key-value-store) la momentul conectării, indexat după `connectedAccountId`.
Hook-ul este declanșat atunci când o conexiune este eliminată în mod direct. Dezinstalarea aplicației elimină conexiunile acesteia printr-un mecanism în cascadă în baza de date, astfel încât hook-ul nu rulează în acest caz. Declară un `uninstallLogicFunction` pe `defineApplication` pentru acel flux: acesta rulează înainte ca metadatele aplicației să fie șterse, astfel încât poate apela în continuare `listConnections` și poate curăța tot ce a mai rămas.
</Accordion>
<Accordion title="listConnections / getConnection" description="Folosește conexiunile dintr-o funcție logică">
În interiorul unui handler de funcție logică, `listConnections({ providerName })` returnează înregistrările `ConnectedAccount` ale acestei aplicații pentru furnizorul dat, cu tokenuri de acces reîmprospătate.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Fiecare conexiune are:
| Câmp | Descriere |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID unic al înregistrării; pasează-l la `getConnection(id)` pentru a reobține acea înregistrare |
| `visibility` | `'user'` (privată pentru un membru al spațiului de lucru) sau `'workspace'` (partajată cu toți membrii) |
| `scopes` | Permisiunile OAuth acordate de furnizorul upstream (distincte de `visibility` — nu au legătură) |
| `userWorkspaceId` | ID-ul userWorkspace al deținătorului — util pentru a alege "conexiunea utilizatorului care face cererea" în declanșatoarele de rută HTTP |
| `accessToken` | Token de acces OAuth proaspăt (reîmprospătat automat dacă a expirat) |
| `name` | Numele afișat al conexiunii (derivat automat la callback-ul OAuth, poate fi redenumit de utilizator) |
| `handle` | Emailul contului din amonte conectat, actualizat la fiecare reconectare; rezolvat din `id_token` OIDC a revenit la schimbul tokenului, revenind la e-mailul de conectare a douăzeci de utilizatori atunci când furnizorul nu returnează unul |
| `authFailedAt` | Setat când cea mai recentă reîmprospătare a eșuat; utilizatorul trebuie să se reconecteze |
Puncte cheie:
* Pasează `{ providerName }` pentru a filtra după furnizor; omite-l pentru a obține toate conexiunile pe care această aplicație le deține la toți furnizorii.
* Serverul reîmprospătează transparent tokenul de acces înainte de a returna. Handlerul tău vede întotdeauna un token utilizabil (sau `authFailedAt` setat).
* `getConnection(id)` este echivalentul pentru o singură înregistrare.
</Accordion>
<Accordion title="Vizibilitate per utilizator vs partajată la nivel de spațiu de lucru" description="Cum aleg utilizatorii între acreditări private și partajate">
Când un utilizator face clic pe "Adaugă conexiune", i se solicită să aleagă o vizibilitate:
* **Doar pentru mine** — acreditarea este privată pentru utilizatorul care se conectează. Orice funcție logică apelată în numele lor (declanșator de rută HTTP cu `isAuthRequired: true`) o vede; declanșatoarele cron și evenimentele din bază de date nu.
* **Partajată la nivel de spațiu de lucru** — orice membru al spațiului de lucru poate folosi acreditarea. Declanșatoarele cron / din bază de date o văd, de asemenea, deoarece nu au un utilizator al cererii.
Folosește-o pe cea potrivită pentru fiecare handler:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Sunt permise mai multe conexiuni per (utilizator, furnizor), astfel încât același utilizator poate avea "Personal Linear" și "Work Linear" una lângă alta.
</Accordion>
<Accordion title="Configurare unică a furnizorului" description="Înregistrează-ți aplicația OAuth la serviciul terț">
Pentru fiecare furnizor de conexiune, administratorul serverului trebuie mai întâi să înregistreze o aplicație OAuth la serviciul terț.
1. Mergi la setările pentru dezvoltatori ale furnizorului (de ex. https://linear.app/settings/api/applications/new).
2. Setează **Redirect URI** la `\<SERVER_URL>/auth/apps/callback`.
3. Copiază **Client ID** și **Client Secret** generate.
4. Deschide aplicația instalată în Twenty ca administrator de server → setează valorile pe `serverVariables` corespunzătoare.
5. Membrii spațiului de lucru pot apoi să adauge conexiuni din secțiunea **Conexiuni** a fiecărei aplicații.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,110 @@
---
title: Stocare cheie-valoare
description: Păstrați rezultatele intermediare, puneți datele în cache și partajați starea între rulările funcțiilor logice cu spațiul de stocare cheie-valoare integrat al aplicației.
icon: database
---
Funcțiile logice rulează în sandbox în procese Node.js de scurtă durată — odată ce o rulare se termină, nimic din ce a fost păstrat în memorie nu supraviețuiește. Când ai nevoie să **îți amintești ceva între rulări** (să pui în cache un răspuns API costisitor, să stochezi un cursor pentru sincronizări incrementale, să aplici debounce sau să transmiți starea de la o funcție la alta), salvează-l în spațiul de stocare cheie-valoare integrat.
Fiecare aplicație primește propriul spațiu de nume izolat: intrările sunt asociate cu aplicația autentificată, astfel încât cheile tale nu pot intra niciodată în coliziune cu — sau fi citite de — o altă aplicație.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## Get, set, delete
Importați `kv` din `twenty-sdk/logic-function`. Valorile pot fi orice payload serializabil JSON.
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## Domenii de aplicare
Fiecare intrare are un domeniu de aplicare, transmis ca opțiune la fiecare apel. Valoarea implicită este `WORKSPACE`.
* **`WORKSPACE`** (implicit) — intrarea este privată pentru instalarea curentă a workspace-ului aplicației tale. Fiecare workspace care instalează aplicația primește propriul set independent de chei. Aceasta este opțiunea potrivită pentru cache-uri, cursoare și stare specifică fiecărui workspace.
* **`SERVER`** — intrarea este partajată între **fiecare instalare** a aplicației tale de pe server. Intrările de tip server se comportă ca niște **claim-uri**: valoarea stocată este întotdeauna workspaceId-ul care a revendicat cheia (omite `value` la `set` pentru a revendica cheia pentru workspace-ul curent), iar doar acel workspace o poate suprascrie sau șterge. Orice instalare poate citi intrarea.
Revendicările de server există pentru rutare între workspace-uri. Un [server-route resolver](/l/ro/developers/extend/apps/logic/logic-functions#server-route-trigger) rulează în workspace-ul proprietar al înregistrării aplicației, dar un webhook de intrare, de obicei, conține doar un id de cont extern — nu un Twenty workspaceId. Configurați fiecare workspace să își revendice ID-ul extern la momentul conectării, apoi rezolvați-l în rută:
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
Deoarece o cheie de tip server poate fi revendicată doar pentru propriul workspace al apelantului și nu poate fi niciodată suprascrisă de un altul, un workspace nu poate deturna o mapare care aparține altcuiva. `kv.set` aruncă o eroare atunci când cheia este deja revendicată de un alt workspace.
## Folosește-l: păstrează în cache un apel costisitor
O utilizare tipică este păstrarea în cache a unui răspuns lent sau cu limitare de rată de la un terț, astfel încât rulările repetate să îl refolosească în loc să plătească costul de fiecare dată.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## Tipare și sfaturi
* **Spații de nume.** Prefixează cheile pentru a păstra domeniile diferite separate — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Expiry (TTL).** Store-ul nu are expirare integrată. Stochează un timestamp în interiorul valorii (ca în exemplul de cache) și verifică-l la citire sau curăță cheile învechite dintr-o [funcție declanșată de cron](/l/ro/developers/extend/apps/logic/logic-functions).
* **Ce să stochezi.** Orice valoare serializabilă JSON — numere, stringuri, array-uri, obiecte. Păstrează intrările mici; acesta este pentru coordonare și caching, nu pentru blob-uri mari sau fișiere. Pentru fișiere, folosește un câmp `FILES` și [`uploadFile`](/l/ro/developers/extend/apps/logic/logic-functions#uploading-files).
* **Vizibilitate.** Intrările există în baza de date a instanței, nu ca înregistrări ale workspace-ului — nu apar niciodată în interfața workspace-ului, nu fac parte din modelul de date al aplicației tale și nu necesită permisiuni de rol sau de obiect.
## Alternativă: un obiect de stocare interogabil
Spațiul de stocare integrat este, în mod deliberat, opac: intrările nu sunt înregistrări, astfel că nu le poți răsfoi în UI, nu le poți corela cu alte obiecte și nu le poți filtra cu interogări de înregistrări. Când ai nevoie de oricare dintre acestea — de exemplu un jurnal de sincronizare vizibil sau o stare per-înregistrare — definește în schimb un mic **obiect tehnic** cu un câmp `key` unic și un câmp `RAW_JSON` `value`, și interoghează-l prin [clientul API tipizat](/l/ro/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Vezi [Objects](/l/ro/developers/extend/apps/data/objects) pentru referința `defineObject` și [Data → Unique indexes](/l/ro/developers/extend/apps/data/overview#unique-indexes) pentru aplicarea unicității cheilor.
* **Limitarea la o înregistrare.** Adaugă o [relație](/l/ro/developers/extend/apps/data/relations) de la obiectul de stocare la obiectul țintă, în loc să codifici id-ul în cheie.
* **Vizibilitate și permisiuni.** Rândurile trăiesc în baza de date a workspace-ului ca orice altă înregistrare, astfel încât pot fi interogate prin API și respectă [rolul](/l/ro/developers/extend/apps/config/roles) aplicației tale. Pentru a ține store-ul în afara UI-ului principal, nu îl include în [navigation menu](/l/ro/developers/extend/apps/layout/navigation-menu-items).
<Note>
Spre deosebire de spațiul de stocare integrat, un obiect personalizat este întotdeauna limitat la un singur workspace — nu poate partaja intrări între instalări, așa cum fac cheile `SERVER`.
</Note>

View File

@@ -0,0 +1,909 @@
---
title: Funcții logice
description: Definește funcții TypeScript pe partea de server cu declanșatoare HTTP, cron și de evenimente din baza de date.
icon: bolt
---
Funcțiile de logică sunt funcții TypeScript pe partea de server care rulează pe platforma Twenty. Acestea pot fi declanșate de solicitări HTTP, programări cron sau evenimente din baza de date — și pot fi, de asemenea, expuse ca instrumente pentru agenți AI.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="Definiți funcții logice și declanșatoarele acestora">
Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale.
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'POST',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
Tipuri de declanșatoare disponibile:
* **httpRoute**: Expune funcția pe o cale și o metodă HTTP. În codul aplicației, prefixează calea rutei cu `/s/` când folosești `RestApiClient`; URL-ul implementat folosește baza injectată `TWENTY_FUNCTIONS_URL` (sau `\<server-url>/s` atunci când nu este setată).
<Note>
Pentru a apela o funcție logică declanșată de o rută dintr-o componentă front-end (headless), consultă [Apelarea unei funcții logice](/l/ro/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **cron**: Rulează funcția pe un program folosind o expresie CRON.
* **databaseEvent**: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este `updated`, câmpurile specifice de urmărit pot fi specificate în array-ul `updatedFields`. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția.
> de ex. `person.updated`, `*.created`, `company.*`
* **serverRoute**: Expune o singură rută HTTP la nivelul înregistrării. O funcție de tip **resolver** (declarată cu `serverRouteTriggerSettings`) rulează în workspace-ul proprietar și fie returnează un `Response` sincron, fie workspace-ul țintă ȘI funcția logică de pus în coadă; pe ramura de punere în coadă, platforma confirmă cu `202` și rulează acea **țintă** în coada worker-ului. Consultați [declanșatorul de rută de server](#server-route-trigger).
<Note>
Puteți, de asemenea, să executați manual o funcție folosind CLI:
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Puteți urmări jurnalele cu:
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### Payload-ul declanșatorului de rută
Când un declanșator de rută invocă funcția logică, aceasta primește un obiect `RoutePayload` care urmează
[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
Importați tipul `RoutePayload` din `twenty-sdk/logic-function`:
```ts
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
Tipul `RoutePayload` are următoarea structură:
| Proprietate | Tip | Descriere | Exemplu |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Anteturi HTTP (doar cele listate în `forwardedRequestHeaders`) | consultați secțiunea de mai jos |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametri query string (valorile multiple unite cu virgule) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parametri de cale extrași din modelul rutei | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpul cererii analizat (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Corpul original al cererii în UTF-8, înainte de parsarea JSON. Util pentru verificarea semnăturilor de tip HMAC pentru webhook-uri (de exemplu, `X-Hub-Signature-256` de la GitHub, Stripe). `undefined` atunci când mediul de execuție nu a păstrat-o. | |
| `isBase64Encoded` | `boolean` | Indică dacă corpul este codificat în base64 | |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Calea brută a cererii | |
#### forwardedRequestHeaders
În mod implicit, anteturile HTTP din cererile de intrare **nu** sunt transmise funcției dvs. de logică din motive de securitate.
Pentru a accesa anumite anteturi, listați-le explicit în array-ul `forwardedRequestHeaders`:
```ts
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
```
În handler, accesați anteturile transmise mai departe astfel:
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
Numele anteturilor sunt normalizate la litere mici. Accesați-le folosind chei cu litere mici (de exemplu, `event.headers['content-type']`).
</Note>
#### Răspuns HTTP personalizat
În mod implicit, returnarea unei valori simple din handler trimite înapoi un răspuns `200` (JSON pentru obiecte, `text/plain` pentru șiruri). Pentru a controla codul de stare și antetele răspunsului, returnează un `Response` din `twenty-sdk/logic-function`:
```ts
import { Response } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
return new Response('<h1>Hello</h1>', {
status: 201,
headers: { 'content-type': 'text/html' },
});
};
```
Din motive de securitate, anteturile de răspuns sunt limitate la o listă de antete permise. Orice antet care nu se află pe listă (de exemplu, `Set-Cookie`, anteturi CORS precum `Access-Control-Allow-Origin` sau anteturi personalizate `X-*`) este eliminat în mod silențios înainte ca răspunsul să fie trimis. Anteturile de răspuns permise sunt:
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
Codul de stare trebuie să fie un cod de stare HTTP valid (între 100 și 599). Numele anteturilor de răspuns sunt comparate fără a ține cont de majuscule și minuscule.
</Note>
#### Răspunsuri de eroare ale platformei
Pe lângă propriile răspunsuri ale handlerului tău, platforma răspunde direct la apelurile către rute în anumite situații: `404` când ruta sau funcția nu există, `403` când aplicația este oprită, `429` când este atinsă limita ratei de execuție și `422` când `dependencies` de producție ale aplicației sunt prea mari pentru a fi instalate — vezi [limite privind dimensiunea dependențelor](/l/ro/developers/extend/apps/getting-started/project-structure#dependencies).
#### Declanșator de rută de server
`httpRouteTriggerSettings` expune o funcție sub `/s/` și rezolvă spațiul de lucru din gazda cererii — ceea ce funcționează atunci când fiecare spațiu de lucru are propriul domeniu. Furnizorii terți, însă, livrează evenimentele fiecărui tenant către **un** singur URL. Pentru acest caz, folosiți `serverRouteTriggerSettings`.
Declanșatorul are două părți:
1. O funcție logică de **resolver** — declarată cu `serverRouteTriggerSettings` — rulează în **workspace-ul deținător** (workspace-ul care deține înregistrarea aplicației). Inspectează cererea primită și returnează fie:
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — platforma pune în coadă acea țintă în workspace-ul rezolvat și confirmă cu `202 { queued: true }`, sau
* un `Response` de la `twenty-sdk/logic-function` — platforma transmite mai departe acel răspuns HTTP **sincron** și **nu** pune în coadă nicio țintă (folosiți acest lucru pentru handshake-uri de tip challenge, cum ar fi Slack `url_verification`).
Resolver-ul este singurul punct de autorizare — URL-ul conține doar identificatorul resolver-ului. **Acesta este locul preferat pentru a verifica semnăturile cererilor**: resolver-ul rulează înaintea oricărui efect secundar, are acces la `rawBody` original și la headerele redirecționate și poate respinge fără a atinge vreodată ținta.
2. O funcție logică **țintă** — o funcție logică obișnuită per-workspace — rulează apoi în workspace-ul rezolvat cu payload-ul returnat de resolver (sau payload-ul original al cererii dacă resolver-ul nu l-a transformat). Valoarea de returnare **nu** este observată de apelantul HTTP atunci când resolver-ul a ales calea de punere în coadă.
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
Endpoint-ul este accesibil la:
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
Identificatorul este `universalIdentifier` al resolver-ului din manifestul dvs. Înregistrați acel URL la furnizor.
**Răspunsul la un GET de verificare.** Unii furnizori verifică un punct final înainte de a livra către acesta, trimițând un `GET` care conține o provocare la același URL către care vor trimite ulterior evenimente prin `POST` — WhatsApp Cloud API de la Meta este unul dintre aceștia. O rută de server răspunde numai la `POST`, cu excepția cazului în care specifici altfel, așa că declară ambele metode:
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
Provocarea ajunge în `event.queryStringParameters`, iar returnarea unui `Response` o retransmite furnizorului în aceeași cerere. Un corp de tip șir este trimis ca `text/plain`, ceea ce așteaptă acești furnizori:
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` înlocuiește valoarea implicită, în loc să se adauge la aceasta, astfel încât doar `['GET']` face ca ruta să respingă `POST`. Sunt acceptate numai `GET` și `POST`. Lăsați-o nesetată, cu excepția cazului în care furnizorul are nevoie de al doilea verb: o rută care declară `GET` își va executa resolverul pentru orice apelant neautentificat, inclusiv pentru crawlerele și serviciile de desfășurare a linkurilor care trimit `GET` fără solicitare. Orice pentru care platforma nu are o metodă primește răspunsul `405`, fără ca resolverul să fie vreodată executat.
<Note>
**Aplicația trebuie revendicată și instalată în spațiul de lucru al proprietarului.** Deoarece resolverul rulează în **spațiul de lucru al proprietarului** (spațiul de lucru care deține înregistrarea aplicației), un declanșator de rută de server funcționează doar după ce aplicația a fost *revendicată* — adică are un spațiu de lucru al proprietarului — **și** acea aplicație este **instalată în spațiul de lucru al proprietarului**. Până când ambele condiții sunt adevărate, resolverul nu are unde să ruleze, astfel ruta nu poate fi apelată. O aplicație care expune o funcție logică `serverRouteTriggerSettings` nu poate fi, așadar, listată în marketplace până când nu este revendicată și instalată în spațiul de lucru al proprietarului.
</Note>
**Contractul resolver-ului.** Tipul `LogicFunctionConfig` din SDK impune acest lucru la compilare: de îndată ce setați `serverRouteTriggerSettings`, handler-ul este constrâns să returneze fie un `Response`, fie `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (sau un `Promise` al uneia dintre acestea). Pe calea de trimitere, `workspaceId` trebuie să fie un workspace în care funcția țintă este instalată, altfel cererea este respinsă cu `404`. Un rezultat care nu corespunde niciuneia dintre forme — inclusiv unul ale cărui identificatoare nu sunt UUID-uri — este respins cu `502`.
| Câmp | Tip | Notițe |
| ---------------------------------------- | ------------------- | --------------------------------------------------------------------------------- |
| `workspaceId` | `șir` | UUID-ul workspace-ului în care va rula ținta. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` al funcției logice care trebuie invocată în acel workspace. |
| `payload` | `object` (opțional) | Dacă este setat, înlocuiește corpul cererii trimis către țintă. |
<Warning>
**Verificarea semnăturii este responsabilitatea dvs. — verificați în resolver.** Platforma nu verifică semnăturile cererilor. Resolver-ul este locul recomandat pentru a face acest lucru: rulează primul, cu acces la `event.rawBody` și la headerele pe care le-ați enumerat în `forwardedRequestHeaders`, iar o eroare aruncată (sau orice `workspaceId` care nu se potrivește) oprește livrarea înainte ca ținta să fie invocată. Dacă, în schimb, mutați verificarea în funcția țintă, funcția țintă trebuie să aibă grijă să nu piardă `rawBody` și headerele — adică resolver-ul nu trebuie să returneze un `payload`. Verificați întotdeauna **înainte** de orice efect secundar și folosiți o comparație în timp constant.
</Warning>
Pentru semnăturile cererilor, majoritatea furnizorilor semnează cu HMAC-SHA256; părțile care diferă sunt numele headerului, codificarea digestului și șirul de payload semnat. Câteva exemple:
| Furnizor | Headere de redirecționat | Șir semnat | Digest |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (secretul este în base64 după eliminarea prefixului `whsec_`) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (cu prefixul `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (cu prefixul `v0=`) |
Exemplul de resolver de mai sus arată deja fluxul GitHub HMAC-SHA256 — adaptați numele headerului, codificarea digestului și șirul de payload semnat în funcție de furnizorul cu care vă integrați.
<Note>
Când resolver-ul returnează un obiect de dispatch, ruta răspunde cu `202 { queued: true }`, iar ținta rulează în coada worker-ului — apelantul nu observă niciodată latența, rezultatul sau erorile țintei (acestea sunt înregistrate în jurnalele de execuție). Acest lucru împiedică retrimiterile expeditorului să amplifice încetinirile procesării, ceea ce este de dorit pentru ingestia de webhook-uri.
Atunci când apelantul trebuie să citească corpul răspunsului în cadrul aceleiași cereri (challenge handshakes, confirmări interactive), returnați în schimb un `Response` din **resolver**. Platforma îl reflectă sincron și omite coada; header-ele acestuia trec prin aceeași listă de permisiuni ca răspunsurile rutelor HTTP. Mențineți resolver-ul rapid — unii furnizori (de ex. Slack) expiră după câteva secunde. Deoarece resolver-ul este accesibil ca endpoint public, protejați-l cu limitare de rată la marginea infrastructurii dvs.
</Note>
#### Payload-ul declanșatorului de eveniment al bazei de date
Când un declanșator de eveniment al bazei de date apelează funcția dvs. logică, aceasta primește un `DatabaseEventPayload` pentru fiecare înregistrare modificată. Payload-ul combină metadatele despre spațiul de lucru și obiectul sursă cu evenimentul la nivel de înregistrare.
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
Payload-ul include:
| Proprietate | Descriere |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `name` | Numele evenimentului, cum ar fi `person.updated`. |
| `workspaceId` | Spațiul de lucru în care a avut loc evenimentul. |
| `objectMetadata` | Metadate pentru obiectul care s-a modificat. |
| `recordId` | ID-ul înregistrării modificate. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Câmpurile actorului atunci când evenimentul a fost cauzat de un utilizator al spațiului de lucru. |
| `properties` | Datele înregistrării pentru eveniment, cu `before`, `after`, `diff` și `updatedFields` în funcție de operație. |
| Eveniment | Datele înregistrării |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
Pentru ștergeri logice (soft delete), `.deleted` urmează structura de tip update deoarece câmpul `deletedAt` al înregistrării se modifică.
Pentru ștergeri permanente, folosiți `.destroyed`.
<Note>
`databaseEventTriggerSettings.updatedFields` filtrează ce evenimente de actualizare declanșează funcția.
`event.properties.updatedFields` vă indică ce câmpuri s-au modificat efectiv în evenimentul curent.
</Note>
Exemplu de eveniment de creare:
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
Exemplu de eveniment de actualizare:
```ts
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
return {
personId: event.recordId,
updatedFields,
previousEmail: before.emails?.primaryEmail,
currentEmail: after.emails?.primaryEmail,
emailDiff: diff.emails,
};
};
```
Declanșare doar la actualizări ale e-mailului:
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
Exemplu de eveniment de ștergere:
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### Contextul execuției.
Fiecare gestionar primește un al doilea argument care descrie execuția propriu-zisă, indiferent de ce a declanșat-o. În timp ce primul argument își schimbă forma pentru fiecare declanșator, acesta nu se schimbă:
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| Câmp | Tip | Descriere |
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------ |
| `workspaceId` | `string` | Spațiul de lucru căruia îi aparține execuția. |
| `userWorkspaceId` | `string \| null` | Persoana care a declanșat execuția, ca membru al acestui spațiu de lucru. |
| `workspaceMemberId` | `string \| null` | Aceeași persoană ca înregistrare de membru al spațiului de lucru, la care conectați înregistrări. |
| `retryCount` | `number` | `0` la prima încercare. Consultați [sarcini de fundal](/l/ro/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | Limita superioară a reîncercărilor solicitate de aplicație. |
`userWorkspaceId` și `workspaceMemberId` sunt `null` atunci când nimeni nu a declanșat execuția: programările cron, hookurile de instalare și webhookurile neautentificate nu au nicio persoană în spatele lor. De asemenea, sunt `null` atunci când persoana nu are o înregistrare de membru al spațiului de lucru sau când aceasta a fost ștearsă.
<Note>
**Contextul vă indică cine a declanșat execuția.** Ceea ce poate *face* execuția este o declarație separată, mai jos.
</Note>
#### Ale cui accesări le utilizează un apel
Fiecare client — `CoreApiClient`, `MetadataApiClient` și `RestApiClient` — acționează ca persoana care a declanșat execuția: rolul acesteia se intersectează cu cel al aplicației dvs., astfel încât apelul nu poate face niciodată mai mult decât poate face oricare dintre voi. Acesta este comportamentul implicit și înseamnă că o persoană nu poate folosi niciodată aplicația dvs. pentru a-și depăși propriile permisiuni.
Când nimeni nu a declanșat execuția, nu există nicio persoană în numele căreia să se acționeze, astfel încât același client revine la accesul propriu al aplicației dvs.: programările cron, hookurile de instalare și webhookurile neautentificate nu necesită o gestionare specială.
Unele apeluri au nevoie în mod legitim de accesul propriu al aplicației chiar și atunci când o persoană *se află* în spatele execuției — de exemplu, pentru a citi înregistrările de configurare ale aplicației dvs. sau pentru a efectua lucrări pe care persoana respectivă nu le-ar putea face singură. Creați un al doilea client pentru acestea:
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
Creați-le pe amândouă o singură dată, la nivel de modul, iar fiecare loc de apel indică apoi accesul utilizat prin clientul pe care îl apelează. `RestApiClient` acceptă aceeași opțiune:
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**O execuție pe care nu a declanșat-o nimeni acționează ca aplicația dvs.** Programările cron, hookurile de instalare și webhookurile neautentificate nu au nicio persoană în spatele lor, astfel încât clientul implicit revine la accesul propriu al aplicației dvs. și continuă să funcționeze. `runAs: 'application'` este necesar doar când doriți acel acces într-o execuție pe care o persoană *a* declanșat-o.
Verificați `context.workspaceMemberId` atunci când o funcție se comportă diferit în funcție de existența unei persoane în spatele ei, de exemplu pentru a atribui o înregistrare.
</Note>
Ajutoarele SDK care accesează resursele proprii ale aplicației dvs. folosesc întotdeauna accesul acesteia și ignoră `runAs`: [stocarea cheie-valoare](/l/ro/developers/extend/apps/logic/key-value-store), [conexiunile](/l/ro/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` și taxarea creditelor.
#### Expunerea unei funcții ca instrument AI sau ca acțiune în fluxul de lucru
Funcțiile logice pot fi expuse în două locuri, fiecare cu propriul declanșator:
* **`toolTriggerSettings`** — face funcția descoperibilă de către funcționalitățile AI ale Twenty (chat, MCP, apelarea de funcții). Folosește JSON Schema standard, formatul pe care LLM-urile îl înțeleg nativ.
* **`workflowActionTriggerSettings`** — determină ca funcția să apară ca un pas în constructorul vizual de fluxuri de lucru. Folosește `InputSchema` bogat al Twenty, astfel încât constructorul să poată afișa editori de câmp adecvați, selectoare de variabile și etichete.
O funcție poate opta pentru una, cealaltă sau ambele. Acestea stau alături de `cronTriggerSettings`, `databaseEventTriggerSettings` și `httpRouteTriggerSettings` — același tipar, aceeași formă.
<Note>
**Relația cu acțiunea Code din fluxul de lucru.** Acțiunea integrată **Code** din constructorul de fluxuri de lucru este ea însăși o funcție logică — Twenty creează câte una pentru fiecare pas Code și afișează editorul inline. `workflowActionTriggerSettings` este modul în care transformi acel cod inline, de unică folosință, într-o acțiune **reutilizabilă**: definești funcția o singură dată în aplicația ta și devine selectabilă în orice flux de lucru, în loc să fie copiată și lipită în fiecare pas Code. Vezi [acțiunea Code](/l/ro/user-guide/workflows/capabilities/workflow-actions#code) în ghidul utilizatorului pentru vizualizarea din perspectiva utilizatorului final.
</Note>
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
Puncte cheie:
* O funcție poate combina suprafețele — declară atât `toolTriggerSettings`, cât și `workflowActionTriggerSettings` pentru a o expune atât în chat, cât și în constructorul de fluxuri de lucru.
* `toolTriggerSettings.inputSchema` și `workflowActionTriggerSettings.inputSchema` sunt ambele opționale. Când sunt omise, generatorul de manifest le deduce din codul sursă al handlerului (JSON Schema pentru instrumentul AI, `InputSchema` al Twenty pentru acțiunea de flux de lucru). Furnizează unul în mod explicit atunci când dorești o tipizare mai bogată — de exemplu, cu câmpuri compatibile cu `FieldMetadataType`, precum `CURRENCY` sau `RELATION` pentru constructorul de fluxuri de lucru, sau cu câmpuri `description` pe care agentul AI le poate citi:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
```
Pentru a declara parametrii **o singură dată** și a deservi ambele suprafețe, definește o singură schemă JSON (`InputJsonSchema`) și convertește-o pentru acțiunea din fluxul de lucru cu `jsonSchemaToInputSchema` din `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` primește direct schema JSON, în timp ce `workflowActionTriggerSettings.inputSchema` necesită `InputSchema` al Twenty:
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### Un exemplu complet de acțiune de flux de lucru
`workflowActionTriggerSettings` acceptă patru câmpuri:
| Câmp | Scop |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Numele afișat pentru acțiune în selectorul de pași al constructorului de fluxuri de lucru. Implicit, este `name` al funcției. |
| `icon` | Pictograma afișată lângă acțiune (un nume `tabler-icons`, de ex. `IconBuilding`). |
| `inputSchema` | `InputSchema` avansat al Twenty — ceea ce constructorul afișează ca câmpuri configurabile (cu selectoare de variabile). Opțional; dedus din handler atunci când este omis. |
| `outputSchema` | Declară structura returnată de handler, astfel încât **pașii următori să poată mapa la câmpurile de ieșire ale acesteia**. Opțional; fără acesta, ieșirea este expusă ca o singură valoare opacă. |
Reunind totul — o funcție expusă ca o acțiune de flux de lucru, cu o ieșire declarată astfel încât pașii următori să poată face referire la `taskId`:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
Odată ce aplicația este instalată, **Enrich Company** apare în selectorul de acțiuni al constructorului de fluxuri de lucru. Constructorul afișează `companyName` și `domain` ca câmpuri de intrare (fiecare putând prelua valori din pașii anteriori), iar pașii ulteriori pot face referire la ieșirile `taskId` și `enriched` ale pasului.
<Note>
**Scrieți o `description` bună.** Agenții AI se bazează pe câmpul `description` al funcției pentru a decide când să folosească instrumentul. Fiți specifici cu privire la ceea ce face instrumentul și când ar trebui apelat.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**Ajutoare la rulare (runtime helpers).** `twenty-sdk/utils` re-exportă mici ajutoare la rulare, astfel încât handlerii să nu importe niciodată direct din `twenty-shared`. De exemplu, `isDefined(value)` returnează `false` atât pentru `null`, cât și pentru `undefined` — folosește-l pentru a restrânge în siguranță intrările opționale ale handlerilor, care pot ajunge drept `null` la rulare, chiar și atunci când sunt tipate `T | undefined`:
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**Hook-uri de instalare** — handleri pre-instalare, post-instalare și dezinstalare — partajează acest runtime, dar sunt declarați cu propriile lor funcții `define` și nu folosesc setări de declanșare. Consultați [Hook-uri de instalare](/l/ro/developers/extend/apps/config/install-hooks) pentru `definePreInstallLogicFunction`, `definePostInstallLogicFunction` și `defineUninstallLogicFunction`.
</Note>
## Creați o activitate pe cronologie.
Utilizați `createTimelineActivity()` pentru a publica un eveniment de domeniu explicit dintr-o funcție logică. Definiți mai întâi evenimentul ca [tip de activitate pe cronologie](/l/ro/developers/extend/apps/data/timeline-activity-types), apoi faceți referire la tip și la obiecte prin identificatorii lor universali stabili:
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
Twenty convertește identificatorii universali în ID-urile metadatelor specifice instalării, validează că tipul de activitate pe cronologie aparține aplicației apelante și captează metadatele sale de prezentare în noua activitate.
Parametrii de intrare obligatorii sunt `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` și `targetRecordId`. De asemenea, puteți furniza `happensAt`, `properties` și `workspaceMemberId`. `happensAt` controlează poziția și ora afișată a evenimentului pe cronologie; în mod implicit, este momentul creării.
Pentru a asocia o altă înregistrare cu evenimentul, furnizați împreună `linkedRecordId` și `linkedObjectMetadataUniversalIdentifier`. În plus, puteți furniza `linkedRecordCachedName` ca alternativă istorică pentru afișare:
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
Rolul funcției logice trebuie să aibă permisiunea de scriere pentru obiectul standard `timelineActivity`. Păstrați tipurile explicite de evenimente nelegate de o `action`; un tip legat de o acțiune primește deja evenimente de audit automate și, în caz contrar, ar produce rânduri duplicate.
## Clienți API tipizați (twenty-client-sdk)
Pachetul `twenty-client-sdk` oferă doi clienți GraphQL tipați pentru a interacționa cu API-ul Twenty din funcțiile de logică și componentele Front.
| Client | Importați | Endpoint | Generat? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------- | ---------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — date ale spațiului de lucru (înregistrări, obiecte) | Da, în timpul dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurarea spațiului de lucru, încărcări de fișiere | Nu, este livrat preconstruit |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Interogați și modificați datele spațiului de lucru (înregistrări, obiecte)">
`CoreApiClient` este clientul principal pentru interogarea și modificarea datelor din spațiul de lucru. Este **generat din schema spațiului de lucru** în timpul `yarn twenty dev` sau `yarn twenty dev:build`, astfel încât este complet tipizat pentru a corespunde obiectelor și câmpurilor dvs.
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Clientul folosește o sintaxă de tip selection-set: transmiteți `true` pentru a include un câmp, folosiți `__args` pentru argumente și imbricați obiecte pentru relații. Obțineți autocompletare și verificare completă a tipurilor, pe baza schemei spațiului dvs. de lucru.
<Note>
**CoreApiClient este generat în timpul dev/build.** Dacă îl utilizați fără a rula mai întâi `yarn twenty dev` sau `yarn twenty dev:build`, va arunca o eroare. Generarea are loc automat — CLI inspectează schema GraphQL a spațiului dvs. de lucru și generează un client tipizat folosind `@genql/cli`.
</Note>
#### Folosirea CoreSchema pentru adnotări de tip
`CoreSchema` oferă tipuri TypeScript care corespund obiectelor din spațiul dvs. de lucru — utile pentru tiparea stării componentelor sau a parametrilor funcțiilor:
```ts
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
</Accordion>
<Accordion title="MetadataApiClient" description="Configurația spațiului de lucru, aplicații și încărcări de fișiere">
`MetadataApiClient` este livrat preconstruit împreună cu SDK-ul (nu este necesară generarea). Interoghează endpointul `/metadata` pentru configurarea spațiului de lucru, aplicații și încărcări de fișiere. Acceptă aceeași opțiune `runAs` ca `CoreApiClient` — consultați [Ale cui accesări le utilizează un apel](#whose-access-a-call-uses).
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### Încărcarea fișierelor
`MetadataApiClient` include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier:
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| Parametru | Tip | Descriere |
| ---------------------------------- | -------- | ------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Conținutul brut al fișierului |
| `filename` | `string` | Numele fișierului (folosit pentru stocare și afișare) |
| `contentType` | `șir` | Tipul MIME (implicit `application/octet-stream` dacă este omis) |
| `fieldMetadataUniversalIdentifier` | `șir` | `universalIdentifier` al câmpului de tip fișier de pe obiectul dvs. |
Puncte cheie:
* Folosește `universalIdentifier` al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul dvs. de încărcare funcționează în orice spațiu de lucru în care aplicația dvs. este instalată.
* `url` returnat este un URL semnat pe care îl puteți folosi pentru a accesa fișierul încărcat.
</Accordion>
</AccordionGroup>
<Note>
Când codul dvs. rulează pe Twenty (funcții de logică sau componente Front), platforma injectează acreditările ca variabile de mediu:
* `TWENTY_API_URL` — URL-ul de bază al API-ului Twenty
* `TWENTY_APP_ACCESS_TOKEN` — Cheie cu durată scurtă pentru accesul implicit: rolul unei persoane se intersectează cu cel al aplicației dvs. atunci când cineva se află în spatele execuției, iar rolul propriu al aplicației dvs. atunci când nu se află nimeni. Persoana este cea care a declanșat execuția într-o funcție logică sau cea care privește pagina într-o componentă front-end.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Cheie cu durată scurtă, limitată numai la rolul propriu al aplicației dvs. Numai funcții logice, injectată întotdeauna acolo și utilizată de `runAs: 'application'`.
Nu trebuie să transmiteți aceste valori clienților — aceștia citesc automat din `process.env`, iar [Ale cui accesări le utilizează un apel](#whose-access-a-call-uses) explică alegerea dintre ele. Permisiunile proprii ale aplicației dvs. sunt determinate de rolul declarat cu `defineApplicationRole()` (sau referențiat prin `defaultRoleUniversalIdentifier` în `application-config.ts`); o execuție care acționează ca o persoană nu poate depăși niciodată acel rol sau rolul persoanei.
</Note>

View File

@@ -0,0 +1,61 @@
---
title: Prezentare generală
description: TypeScript pe server care rulează în Twenty — declanșat de rute HTTP, programări cron, evenimente de bază de date, instrumente IA sau acțiuni de workflow.
icon: bolt
---
**Stratul de logică** al unei aplicații Twenty este codul care *rulează* — handler-e TypeScript pe server care reacționează la cereri HTTP, programări cron și modificări ale înregistrărilor; abilități și agenți IA care trăiesc în workspace; și conexiuni OAuth care permit funcțiilor tale să acționeze în numele utilizatorului în servicii terțe.
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
▼
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## În această secțiune
<CardGroup cols={2}>
<Card title="Funcții logice" icon="bolt" href="/l/ro/developers/extend/apps/logic/logic-functions">
Blocul de bază — tipuri de declanșatoare, payload-uri și clientul API cu tipuri.
</Card>
<Card title="Abilități și agenți" icon="robot" href="/l/ro/developers/extend/apps/logic/skills-and-agents">
Instrucțiuni reutilizabile pentru agenți IA și asistenți cu prompturi de sistem personalizate.
</Card>
<Card title="Conexiuni" icon="plug" href="/l/ro/developers/extend/apps/logic/connections">
Credențiale OAuth pe care aplicația ta le deține pentru servicii terțe — Linear, GitHub, Slack și altele.
</Card>
<Card title="Stocare cheie-valoare" icon="database" href="/l/ro/developers/extend/apps/logic/key-value-store">
Păstrează starea între execuțiile funcțiilor de logică — cache-uri, cursoare și revendicări între spații de lucru.
</Card>
<Card title="Sarcini în fundal" icon="layer-group" href="/l/ro/developers/extend/apps/logic/background-jobs">
Pune în coadă execuția unei funcții logice de către workeri pentru a depăși limita de timp per execuție.
</Card>
</CardGroup>
## Tipuri de declanșatoare, dintr-o privire
O funcție de logică alege unul sau mai multe declanșatoare — fiecare intrare de mai jos este un câmp separat pe `defineLogicFunction()`:
| Declanșator | Când rulează | Setare |
| ----------------------------- | ----------------------------------------------------------------- | ------------------------------- |
| **Rută HTTP** | O cerere ajunge la endpointul tău `/s/\<path>` | `httpRouteTriggerSettings` |
| **Cron** | O expresie CRON se potrivește | `cronTriggerSettings` |
| **Eveniment de bază de date** | O înregistrare din workspace este creată, actualizată sau ștearsă | `databaseEventTriggerSettings` |
| **Instrument IA** | O funcționalitate IA din Twenty decide să apeleze funcția ta | `toolTriggerSettings` |
| **Acțiune Workflow** | Un pas din workflow îți invocă funcția | `workflowActionTriggerSettings` |
Funcțiile rulează în sandbox, în procese Node.js izolate și accesează workspace-ul printr-un client API cu tipuri, limitat la rolul declarat în [`defineApplication()`](/l/ro/developers/extend/apps/config/application).
<Note>
**Hook-uri de instalare** — cod care rulează înainte sau după instalare — folosesc același runtime, dar au propriile lor funcții define și se află sub [Config → Install Hooks](/l/ro/developers/extend/apps/config/install-hooks).
</Note>

View File

@@ -0,0 +1,187 @@
---
title: Abilități și agenți
description: Definiți abilități și agenți AI pentru aplicația dvs.
icon: robot
---
<Warning>
Abilitățile și agenții sunt în prezent în stadiu alfa. Caracteristica funcționează, dar este încă în dezvoltare.
</Warning>
Aplicațiile pot defini capabilități AI care există în interiorul spațiului de lucru — instrucțiuni reutilizabile pentru abilități și agenți cu prompturi de sistem personalizate.
<AccordionGroup>
<Accordion title="defineSkill" description="Definiți abilități pentru agentul AI">
Abilitățile definesc instrucțiuni și capabilități reutilizabile pe care agenții AI le pot folosi în spațiul dvs. de lucru. Folosiți `defineSkill()` pentru a defini abilități cu validare încorporată:
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Puncte cheie:
* `name` este un șir identificator unic pentru abilitate (se recomandă kebab-case).
* `label` este numele lizibil afișat în interfața cu utilizatorul (UI).
* `content` conține instrucțiunile abilității — acesta este textul pe care agentul AI îl folosește.
* `icon` (opțional) setează pictograma afișată în UI.
* `description` (opțional) oferă context suplimentar despre scopul abilității.
</Accordion>
<Accordion title="defineAgent" description="Definiți agenți AI cu prompturi personalizate">
Agenții sunt asistenți AI care există în interiorul spațiului dvs. de lucru. Utilizați `defineAgent()` pentru a crea agenți cu un prompt de sistem personalizat:
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
Puncte cheie:
* `name` este un șir identificator unic pentru agent (se recomandă kebab-case).
* `label` este numele de afișare din interfața cu utilizatorul (UI).
* `prompt` conține promptul de sistem — acesta este textul de instrucțiuni care definește comportamentul agentului.
* `description` (opțional) oferă context despre ce face agentul.
* `icon` (opțional) setează pictograma afișată în UI.
* `modelId` (opțional) suprascrie modelul AI implicit utilizat de agent.
* `responseFormat` (opțional) controlează forma ieșirii agentului. Valoarea implicită este `{ type: 'text' }` pentru text liber. Folosește `{ type: 'json', schema }` pentru a impune ieșire JSON structurată.
În mod implicit, un agent returnează text liber. Pentru a obține o ieșire structurată, setează `responseFormat` la `{ type: 'json' }` și furnizează `schema`:
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
Note despre schemă:
* Schema este un obiect plat: `type` al fiecărei proprietăți trebuie să fie un tip primitiv (`string`, `number` sau `boolean`). Obiectele îmbricate și tablourile nu sunt acceptate.
* `description` (opțional) pe fiecare proprietate ghidează modelul cu privire la ce să pună acolo.
* `required` (opțional) enumeră proprietățile pe care modelul trebuie să le returneze întotdeauna.
* `additionalProperties: false` (opțional) interzice orice proprietate care nu este declarată în `properties`.
</Accordion>
<Accordion title="runAgent" description="Rulează un agent dintr-o funcție logică">
`runAgent()` permite unei funcții logice să ruleze unul dintre agenții aplicației tale (cu abilitățile și instrumentele sale). Identifică agentul prin `universalIdentifier` pe care l-ai transmis către `defineAgent()`. Transmite fie un șir `prompt`, fie un istoric de conversație `messages`,
nu ambele:
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
Pentru boți multi-turn (Slack, Discord, Teams, …), transmite istoricul firului de discuție ca
`messages` în loc de un singur `prompt`:
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
Puncte cheie:
* Furnizează **exact unul** dintre `prompt` (șir) sau `messages` (1 până la 100 de intrări
de `{ role: 'user' | 'assistant', content: string }`).
* Agentul rulează **sincron** și poate citi/actualiza direct înregistrări prin propriile sale instrumente — `runAgent()` este rezolvat după ce rularea se încheie.
* O aplicație poate rula doar propriii săi agenți.
* [Rolul implicit](/l/ro/developers/extend/apps/config/roles) al aplicației trebuie să acorde indicatorul de permisiune `AI` — adaugă `SystemPermissionFlag.AI` la `permissionFlagUniversalIdentifiers` (sau setează `canAccessAllTools: true`).
Fără acesta, `runAgent()` eșuează cu o eroare de permisiune.
* Setează un `timeoutSeconds` generos pentru funcția logică — rulările agentului pot dura câteva secunde.
* `success` este `true` și `result` este nenul când rularea se încheie; în caz de eșec `success` este `false`, `result` este `null`, iar `error` conține motivul (de exemplu, când spațiul de lucru rămâne fără credite AI în timpul rulării).
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**Evită buclele:** dacă apelezi `runAgent()` dintr-un declanșator de tip eveniment de bază de date `*.updated`, iar agentul actualizează aceeași înregistrare, restrânge declanșatorul cu `updatedFields` la un câmp pe care agentul nu îl scrie niciodată (de exemplu, URL-ul sursă) sau verifică dacă vreun câmp țintă este încă gol înainte de a apela `runAgent()`.
</Warning>
### Rulare în numele unui membru al spațiului de lucru
Transmite `runAsWorkspaceMemberId` atunci când rularea este declanșată de o persoană — de exemplu, un chatbot care răspunde la un mesaj — astfel încât agentul să acționeze ca acel membru în loc să acționeze ca aplicația:
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
Rularea acționează apoi cu **propriul rol al membrului**: poate face orice
poate face acel membru și nimic mai mult, înregistrările pe care le creează sunt atribuite lui, iar permisiunile lui la nivel de rând se aplică. Rolul agentului nu participă — este valoarea implicită a aplicației pentru rulările fără nicio persoană în spatele lor. Omite câmpul pentru
rulările autonome (joburi programate, declanșatoare de evenimente din baza de date): acelea păstrează propriul rol al agentului. Aplicația care acționează rămâne atașată la contextul rulării
pentru proveniență, fără a restrânge permisiunile membrului.
Aplicația ta este responsabilă să facă asocierea dintre persoana care a declanșat rularea și un membru al spațiului de lucru. A denumi unul necesită un token de acces al aplicației, iar ceea ce poate denumi un token depinde dacă acesta are sau nu un utilizator asociat:
* Un token **fără niciun utilizator atașat** poate denumi orice membru. O funcție logică rulează cu unul, la fel și tokenurile emise prin `client_credentials` sau dintr-o cheie API.
* Un token emis **în numele unui utilizator**, așa cum primește un front component, poate
denumi doar propriul membru al acelui utilizator.
Orice altceva — o sesiune simplă de utilizator, o cheie API fără un token al aplicației — nu poate denumi deloc un membru.
<Warning>
`runAgent()` aruncă o eroare atunci când membrul spațiului de lucru nu poate fi identificat — un membru necunoscut sau eliminat, ori unul fără niciun rol. Nu revine niciodată la
propriul rol al agentului, deoarece acest lucru ar acorda mai mult acces decât a cerut apelantul.
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: comenzi `yarn twenty` pentru executarea funcțiilor, transmiterea în flux a jurnalelor, gestionarea instalărilor de aplicații și comutarea între remote-uri.
icon: terminal
---
Interfața CLI `yarn twenty` este punctul tău de acces pentru tot ce ține de aplicație. Lista completă de comenzi:
| Comandă | Ce face | Documentat în |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `dev` | Monitorizează fișierele sursă și sincronizează în timp real modificările | [Ghid de pornire rapidă](/l/ro/developers/extend/apps/getting-started/quick-start) |
| `plan` | Previzualizează modificările metadatelor fără a le aplica | [Sincronizare și recuperare](/l/ro/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `aplică` | Aplică modificările metadatelor după afișarea planului | [Sincronizare și recuperare](/l/ro/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | Compilează aplicația și generează clientul API (`--tarball` pentru a împacheta un `.tgz`) | [Publicare](/l/ro/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | Rulează verificarea tipurilor TypeScript | [Testare](/l/ro/developers/extend/apps/operations/testing) |
| `dev:add` | Creează scheletul unei entități noi | [Generarea scheletului](/l/ro/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | Regenerează clientul API tipizat | această pagină |
| `dev:function:exec` / `dev:function:logs` | Execută funcții și transmite în flux jurnalele acestora | această pagină |
| `dev:translations-extract` | Extrage șirurile traducibile în cataloagele din `locales/` | [Traduceri](/l/ro/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | Declanșează o sincronizare a catalogului marketplace-ului | [Publicare](/l/ro/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | Ciclul de viață al lansărilor | [Publicare](/l/ro/developers/extend/apps/operations/publishing) și această pagină |
| `docker:*` | Administrează containerul serverului Twenty local | [Server local](/l/ro/developers/extend/apps/getting-started/local-server) |
| `remote:*` | Administrează conexiunile la server | această pagină |
Fiecare comandă acceptă `-r, --remote \<name>` pentru a viza un anumit server la distanță în locul celui implicit.
## Executarea funcțiilor (`yarn twenty dev:function:exec`)
Rulați manual o funcție logică fără a o declanșa prin HTTP, cron sau eveniment de bază de date:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## Vizualizarea jurnalelor funcțiilor (`yarn twenty dev:function:logs`)
Transmiteți în flux jurnalele de execuție pentru funcțiile logice ale aplicației:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Acest lucru este diferit de `yarn twenty docker:logs`, care afișează jurnalele containerului Docker. `yarn twenty dev:function:logs` afișează jurnalele de execuție ale funcțiilor aplicației de pe serverul Twenty.
</Note>
## Generarea clientului tipizat (`yarn twenty dev:generate-client`)
Regenerați clientul tipizat al API-ului (`twenty-client-sdk`) din schema remote-ului activ, fără a construi sau sincroniza o aplicație. Folosiți-l pentru a obține un client tipizat în orice proiect — cum ar fi un serviciu backend aflat într-un repository separat — care comunică cu instanța dvs. Twenty:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
Apoi importați clientul în codul dvs.:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
Rulați din nou comanda ori de câte ori se schimbă modelul de date pentru a reîmprospăta tipurile generate.
<Note>
Clientul este generat în interiorul `node_modules`, astfel încât nu este inclus în commit împreună cu codul dvs. Rulați `yarn twenty dev:generate-client` după fiecare instalare (de exemplu, într-un script `postinstall` sau în CI).
</Note>
## Dezinstalarea unei aplicații (`yarn twenty app:uninstall`)
Eliminați aplicația din spațiul de lucru activ:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## Gestionarea remote-urilor
Un „remote” este un server Twenty la care se conectează aplicația. În timpul configurării, Scaffolderul creează automat unul pentru dvs. Puteți adăuga mai multe remote-uri sau comuta între ele oricând.
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
Acreditările dvs. sunt stocate în `~/.twenty/config.json`.

View File

@@ -0,0 +1,32 @@
---
title: Prezentare generală
description: Construiește, testează și livrează aplicația ta — comenzi CLI, teste de integrare, CI și publicare pe un server sau pe npm.
icon: rocket
---
**Stratul de operațiuni** este tot ceea ce faci *asupra* aplicației tale, mai degrabă decât *cu* ea: invocarea comenzilor CLI, rularea testelor de integrare pe un server Twenty real, configurarea CI și livrarea versiunilor — fie ca un tarball implementat pe un singur server, fie ca un pachet npm listat în marketplace.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## În această secțiune
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/ro/developers/extend/apps/operations/cli">
Referință pentru `yarn twenty` — exec, logs, uninstall, remotes.
</Card>
<Card title="Sincronizare și recuperare" icon="compass" href="/l/ro/developers/extend/apps/operations/sync-and-recovery">
Ce comandă și când, citirea diferențelor de sincronizare și o schemă de recuperare.
</Card>
<Card title="Testare" icon="flask" href="/l/ro/developers/extend/apps/operations/testing">
Configurare Vitest, teste de integrare, verificare a tipurilor, flux de lucru CI.
</Card>
<Card title="Publicare" icon="upload" href="/l/ro/developers/extend/apps/operations/publishing">
Construire, implementare a unui tarball, publicare pe npm, instalare.
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: Publicare
icon: upload
description: Distribuie aplicația ta Twenty în marketplace sau implementeaz-o intern.
---
## Prezentare generală
După ce aplicația ta este [construită și testată local](/l/ro/developers/extend/apps/getting-started/concepts), ai două căi pentru distribuire:
* **Implementează un tarball** — încarcă aplicația direct pe un server Twenty anume pentru uz intern sau privat.
* **Publică pe npm** — listează aplicația ta în marketplace-ul Twenty pentru ca orice spațiu de lucru să o poată descoperi și instala.
Ambele căi pornesc din aceeași etapă de **build**.
## Construirea aplicației
Rulează comanda `build` pentru a compila aplicația și a genera un `manifest.json` pregătit pentru distribuire:
```bash filename="Terminal"
yarn twenty dev:build
```
Aceasta compilează sursele TypeScript, transpilează funcțiile de logică și componentele de front-end și scrie totul în `.twenty/output/`. Adaugă `--tarball` pentru a produce și un pachet `.tgz` pentru distribuire manuală sau pentru comanda de publish.
## Implementare pe un server (tarball)
Pentru aplicațiile pe care nu le dorești disponibile public — instrumente proprietare, integrări doar pentru enterprise sau build-uri experimentale — poți implementa un tarball direct pe un server Twenty.
### Cerințe
Înainte de implementare, ai nevoie de un remote configurat care să indice serverul țintă. Remote-urile stochează local URL-ul serverului și credențialele de autentificare în `~/.twenty/config.json`.
Adaugă un remote:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### Implementare
Construiește și încarcă aplicația ta pe server într-un singur pas:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### Partajarea unei aplicații implementate
Aplicațiile tarball nu sunt listate în marketplace-ul public, astfel încât alte spații de lucru de pe același server nu le vor descoperi prin navigare. Pentru a partaja o aplicație implementată:
1. Mergi la **Setări > Aplicații > Înregistrări** și deschide aplicația ta
2. În fila **Distribuție**, fă clic pe **Copiază linkul de partajare**
3. Partajează acest link cu utilizatori din alte spații de lucru — îi duce direct la pagina de instalare a aplicației
Linkul de partajare folosește URL-ul de bază al serverului (fără niciun subdomeniu de spațiu de lucru), astfel încât funcționează pentru orice spațiu de lucru de pe server.
### Gestionarea versiunilor
Când actualizezi o aplicație tarball deja implementată, serverul solicită ca `version` din `package.json` să fie **strict mai mare** (conform ordonării [semver](https://semver.org)) decât versiunea implementată în prezent. Redeployarea aceleiași versiuni sau trimiterea uneia inferioare este respinsă înainte ca tarball-ul să fie stocat — vei vedea o eroare `VERSION_ALREADY_EXISTS` de la CLI.
Pentru a lansa o actualizare:
1. Incrementează câmpul `version` din `package.json` (de ex. `1.2.3` → `1.2.4`, `1.3.0` sau `2.0.0`)
2. Rulează `yarn twenty app:publish --private` (sau `yarn twenty app:publish --private --remote production`)
3. Spațiile de lucru care au aplicația instalată și au activat actualizarea automată pentru aceasta (în fila Setări a aplicației) sunt actualizate automat în fundal; celelalte vor vedea actualizarea disponibilă în setările lor
<Note>
Etichetele de pre-lansare funcționează conform așteptărilor: incrementarea de la `1.0.0-rc.1` la `1.0.0-rc.2` este permisă, iar o lansare finală precum `1.0.0` este recunoscută corect ca fiind mai mare decât `1.0.0-rc.5`. Versiunea din `package.json` trebuie să fie ea însăși un șir semver valid.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### Compatibilitatea versiunii serverului
Dacă aplicația ta folosește o funcționalitate introdusă într-o anumită versiune de server Twenty (de exemplu, furnizori OAuth adăugați în v2.3.0), ar trebui să declari versiunea minimă de server necesară aplicației folosind câmpul `engines.twenty` din `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
Valoarea este un [interval semver](https://github.com/npm/node-semver#ranges) standard. Tipare comune:
| Interval | Semnificație |
| ---------------------------------- | ------------------------------------------------------ |
| `>=2.3.0` | Orice server de la 2.3.0 încolo |
| `>=2.3.0 \<3.0.0` | 2.3.0 sau ulterior, dar sub următoarea versiune majoră |
| `^2.3.0` | La fel ca `>=2.3.0 \<3.0.0` |
**Ce se întâmplă în timpul implementării și instalării:**
* Dacă `engines.twenty` este setat și versiunea serverului țintă nu respectă intervalul, implementarea (încărcarea arhivei tarball) sau instalarea este respinsă cu eroarea `SERVER_VERSION_INCOMPATIBLE` și cu un mesaj care indică atât intervalul necesar, cât și versiunea efectivă a serverului.
* Dacă `engines.twenty` nu este setat, aplicația este acceptată pe orice versiune de server (retrocompatibilă cu aplicațiile existente).
* Dacă serverul nu are nicio `APP_VERSION` configurată, verificarea este omisă.
<Note>
Serverul este verificarea autoritativă — validează `engines.twenty` atât la încărcarea arhivei tarball, cât și la instalarea în spațiul de lucru. Dacă implementezi un tarball în afara fluxului standard sau instalezi din marketplace, serverul impune în continuare compatibilitatea.
</Note>
## CI/CD automatizat (fluxuri de lucru preconfigurate)
Aplicațiile generate cu `create-twenty-app` vin, gata de utilizare, cu trei fluxuri de lucru GitHub Actions, în `.github/workflows/`. CI rulează fără nicio configurare, CD necesită un singur secret, iar publicarea pe npm necesită o configurare unică pentru npm trusted publisher.
### CI — `ci.yml`
Rulează testele de integrare la fiecare push pe `main` și la fiecare pull request.
**Ce face:**
1. Preia codul sursă al aplicației.
2. Pornește o instanță de test Twenty izolată folosind acțiunea compozită `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (echivalentul din CI al `yarn twenty docker:start --test`).
3. Activează Corepack, configurează Node.js pe baza fișierului `.nvmrc` și instalează dependențele cu `yarn install --immutable`.
4. Rulează `yarn test`, transmitând `TWENTY_API_URL` și `TWENTY_API_KEY` din instanța pornită, astfel încât testele să poată comunica cu un server real.
**Opțiuni de configurare:**
* `TWENTY_VERSION` (variabilă de mediu, implicit `latest`) — fixează versiunea serverului Twenty folosită în CI editând acest parametru în `ci.yml`.
* Concurența este grupată după `github.ref` și anulează execuțiile în desfășurare la noile push-uri.
Nu sunt necesare secrete — instanța de test este efemeră și există doar pe durata jobului.
### CD — `cd.yml`
Implementează aplicația pe un server Twenty configurat la fiecare push pe `main` și, opțional, dintr-un pull request când se aplică eticheta `deploy`.
**Ce face:**
1. Preia head-ul PR-ului (pentru PR-urile etichetate) sau commitul împins.
2. Rulează `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — echivalentul din CI al `yarn twenty app:publish --private`.
3. Rulează `twentyhq/twenty/.github/actions/install-twenty-app@main` astfel încât versiunea nou implementată să fie instalată în spațiul de lucru țintă.
**Configurare necesară:**
| Setare | Unde | Scop |
| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TWENTY_DEPLOY_URL` | `env` în `cd.yml` (implicit `http://localhost:3000`) | Serverul Twenty la care se face implementarea. Modifică-l la URL-ul real al serverului înainte de prima utilizare. |
| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | Cheie API cu permisiune de implementare pe serverul țintă. |
<Note>
Valoarea implicită a `TWENTY_DEPLOY_URL`, `http://localhost:3000`, este un placeholder — nu va putea accesa nimic dintr-un runner găzduit de GitHub. Actualizează-l la URL-ul public al serverului tău (sau folosește un runner self-hosted cu acces la rețea) înainte de a activa CD.
</Note>
**Declanșarea unei implementări de previzualizare dintr-un PR:**
Adaugă eticheta `deploy` la un pull request. Condiția `if:` din `cd.yml` va rula jobul pentru acel PR folosind commitul head al PR-ului, permițându-ți să validezi o modificare pe serverul țintă înainte de a face merge.
### Publicare — `publish.yml`
Publică aplicația pe npm, cu proveniență, când împingi un tag de versiune (de ex. `v1.0.0`) sau când rulezi manual fluxul de lucru din fila Actions.
**Ce face:**
1. Face checkout al aplicației tale, configurează Node.js și actualizează npm (publicarea de încredere necesită npm 11.5.1 sau o versiune ulterioară).
2. Rulează `yarn twenty app:publish`, care construiește aplicația și publică `.twenty/output` în npm. În CI adaugă automat `--provenance` și `--access public`, astfel încât nu sunt necesare flaguri în fluxul de lucru.
**Configurare unică:**
Pe npmjs.com deschide pachetul tău > **Settings → Trusted Publisher** și înregistrează acest repozitoriu cu fluxul de lucru `publish.yml` (vezi [documentația npm trusted publishing](https://docs.npmjs.com/trusted-publishers)). Publicarea cu provenance certifică ce repozitoriu GitHub a construit pachetul, ceea ce este, de asemenea, modul în care îți revendici proprietatea asupra aplicației tale într-un marketplace Twenty.
<Note>
npm acceptă doar proveniență din depozite sursă **publice**. Dacă publici dintr-un depozit privat, npm respinge pachetul de proveniență OIDC cu un `E422 ... Eroare „Unsupported GitHub Actions source repository visibility: "private"`.” Pentru a publica dintr-un depozit privat, renunță la proveniență setând `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` în `env` al etapei de publicare (un indiciu comentat este inclus în fișierul generat `publish.yml`):
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### Fixarea acțiunilor reutilizabile
Fluxurile de lucru `ci.yml` și `cd.yml` fac referire la acțiuni reutilizabile la `@main`, astfel încât actualizările acțiunilor din repo-ul `twentyhq/twenty` sunt preluate automat. Dacă dorești builduri deterministe, înlocuiește `@main` cu un SHA de commit sau cu un tag de release pe fiecare linie `uses:`.
## Publicarea pe npm
Publicarea pe npm face ca aplicația ta să poată fi descoperită în marketplace-ul Twenty. Orice spațiu de lucru Twenty poate răsfoi, instala și actualiza aplicațiile din marketplace direct din interfață.
### Cerințe
* Un cont [npm](https://www.npmjs.com)
* Cuvântul cheie `twenty-app` din array-ul `keywords` al fișierului `package.json` (adaugă-l manual — nu este inclus în mod implicit în șablonul `create-twenty-app`)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### Metadate pentru marketplace
Configurația `defineApplication()` acceptă câmpuri opționale care controlează modul în care aplicația ta apare în marketplace. Folosește `logo` și `galleryImages` pentru a face referire la imaginile din folderul `public/`:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
Vezi [acordeonul defineApplication](/l/ro/developers/extend/apps/config/application#marketplace-metadata) din pagina Building Apps pentru lista completă de câmpuri ale marketplace-ului (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensiuni recomandate pentru imaginile din galerie
Marketplace-ul redă `galleryImages` într-un container fix cu raport `8:5` (de exemplu, `1600×1000 px`).
<Note>
Imaginile din galerie cu orice raport de aspect sunt afișate integral și nu sunt niciodată decupate, însă orice imagine semnificativ mai înaltă sau mai îngustă decât `8:5` va afișa benzi goale pe laterale.
</Note>
#### Limită de dimensiune pentru imagini
Fișierul `logo` și fiecare fișier din `galleryImages` nu trebuie să depășească **10 MB**. Fișierele mai mari sunt omise atunci când marketplace-ul re-găzduiește resursele tale publicate, astfel că nu vor fi afișate.
### Publicare
```bash filename="Terminal"
yarn twenty app:publish
```
Pentru a publica sub un dist-tag specific (de ex., `beta` sau `next`):
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### Cum funcționează descoperirea în marketplace
Serverul Twenty sincronizează catalogul marketplace-ului din registrul npm **la fiecare oră**.
Poți declanșa sincronizarea imediat, în loc să aștepți:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
Metadatele afișate în marketplace provin din configurația `defineApplication()` — vezi secțiunea [Metadate pentru marketplace](#marketplace-metadata) de mai sus.
<Note>
Dacă aplicația ta nu definește un `aboutDescription` în `defineApplication()`, piața va folosi automat fișierul `README.md` al pachetului tău de pe npm drept conținut pentru pagina Despre. Acest lucru înseamnă că poți menține un singur README atât pentru npm, cât și pentru piața Twenty. Dacă vrei o descriere diferită în piață, setează explicit `aboutDescription`.
</Note>
### Publicare CI
Fluxul de lucru generat `publish.yml` descris mai sus publică automat pe npm la etichetele de versiune, cu provenance. Deoarece `yarn twenty app:publish` adaugă `--provenance` și `--access public` pentru tine când rulează în CI, fluxul de lucru nu are nevoie de flaguri npm — doar de configurarea unică a Trusted Publisher.
Pentru alte sisteme CI (GitLab CI, CircleCI etc.), rulează `yarn install` apoi `yarn twenty app:publish`. Provenance este emisă atunci când mediul poate genera un token OIDC și este omisă automat în caz contrar.
<Note>
**npm provenance** adaugă un badge de încredere la listarea ta în npm, permițând utilizatorilor să verifice că pachetul a fost construit dintr-un commit specific într-un pipeline CI public. Este, de asemenea, ceea ce îți permite să revendici proprietatea asupra aplicației tale într-un marketplace Twenty. Vezi [documentația npm provenance](https://docs.npmjs.com/generating-provenance-statements) pentru detalii.
</Note>
## Instalarea aplicațiilor
După ce o aplicație este publicată (npm) sau implementată (tarball), spațiile de lucru o pot instala prin interfața utilizatorului (UI).
Mergi la pagina **Setări > Aplicații** din Twenty, unde pot fi parcurse și instalate atât aplicațiile din marketplace, cât și cele implementate prin tarball.
{/* TODO: add screenshot of the UI when the app is registered */}
Poți instala aplicații și din linia de comandă:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
Serverul impune versionarea semver la instalare, reflectând regulile de la deploy:
* Instalarea aceleiași versiuni care este deja instalată în spațiul tău de lucru este respinsă cu o eroare `APP_ALREADY_INSTALLED`.
* Instalarea unei versiuni mai mici decât cea instalată în prezent este respinsă cu o eroare `CANNOT_DOWNGRADE_APPLICATION`.
Pentru a instala o versiune mai nouă, fă mai întâi deploy sau public-o, apoi rulează din nou `yarn twenty app:install`.
</Note>

View File

@@ -0,0 +1,126 @@
---
title: Sincronizare și recuperare
description: Ce comandă să folosești și când, cum să citești rezultatul sincronizării și un plan în trepte de recuperare pentru situațiile în care metadatele locale deviază — înainte de a ajunge la o resetare completă.
icon: compass
---
Dezvoltarea locală de aplicații se învârte în jurul **sincronizării**: CLI-ul reconstruiește manifestul și serverul aplică doar diferența dintre acesta și metadatele deja existente în spațiul tău de lucru. Această pagină explică ce comandă să folosești, cum să citești ce a modificat o sincronizare și ce să faci — în ordine — atunci când starea locală pare inconsistentă.
## Ce comandă și când
<Note>
Pentru iterațiile locale de zi cu zi vei dori aproape întotdeauna `yarn twenty dev`. Publicarea și deploy-ul sunt pentru lansarea de versiuni, **nu** pentru bucla locală.
</Note>
| Vrei să… | Comandă | Notițe |
| -------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Iterează local cu sincronizare în timp real | `yarn twenty dev` | Monitorizează fișierele și sincronizează la fiecare modificare. |
| Sincronizează o singură dată și iese (CI, scripturi, hook-uri) | `yarn twenty apply` | O singură compilare + sincronizare, apoi iese. Adaugă `--force` pentru a omite confirmarea schimbărilor distructive. |
| Previzualizează modificările **fără a le aplica** | `yarn twenty plan` | Calculează și afișează diff-ul; nu scrie nimic. |
| Elimină aplicația din spațiul de lucru | `yarn twenty app:uninstall` | Adaugă `--yes` pentru a sări peste prompt. |
| Trimite un tarball către un server | `yarn twenty app:publish --private` | Necesită o versiune `package.json` **strict mai mare** — vezi [Publicare](/l/ro/developers/extend/apps/operations/publishing). |
| Publică în marketplace (npm) | `yarn twenty app:publish` | — |
| Instalează / actualizează o versiune deja implementată | `yarn twenty app:install` | Instalează versiunea implementată în prezent. |
| Șterge serverul local și pornește de la zero | `yarn twenty docker:reset` | Șterge **toate** datele locale — ultimă soluție. |
<Note>
`yarn twenty dev --once` și `yarn twenty dev --once --dry-run` funcționează în continuare ca aliasuri depreciate pentru `yarn twenty apply` și `yarn twenty plan`.
</Note>
### Sincronizarea locală nu are nevoie de incrementarea versiunii
Regula de `version` strict crescătoare (`VERSION_ALREADY_EXISTS` la deploy, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` la instalare) se aplică pentru **`app:publish` / `app:install`** — calea de release. `yarn twenty dev` sincronizează manifestul pe loc și nu necesită niciodată schimbarea versiunii, astfel încât nu trebuie să atingi `package.json` pentru a itera. Dacă ajungi să crești versiunea ca să testezi o modificare locală, folosești calea de release atunci când ai nevoie de bucla de dezvoltare.
## Citirea rezultatului sincronizării
Fiecare sincronizare afișează modificările de metadate pe care le-a aplicat (sau le-ar aplica, cu `plan`), în stil Terraform — un bloc per entitate cu atributele sale, apoi o linie de rezumat:
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
Acesta este primul tău instrument de diagnostic: îți spune exact ce obiecte, câmpuri și layout-uri s-au schimbat, astfel încât să poți confirma că o sincronizare a făcut ce te așteptai înainte să verifici interfața.
Schimbările distructive (`to destroy`) sunt listate împreună cu ceea ce elimină (de ex. `objectMetadata "auditNote" — drops the table and all its rows`) și necesită confirmare interactivă sau `--force` în scripturi.
Când o sincronizare eșuează pe o singură entitate, eroarea numește entitatea problematică și `universalIdentifier`-ul acesteia, de exemplu:
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
Folosește acel identificator pentru a găsi entitatea în manifest (și, dacă este nevoie, în spațiul de lucru) în loc să ghicești care intră în conflict.
## Previzualizarea modificărilor (plan)
`yarn twenty plan` construiește manifestul, cere serverului planul de migrare și îl afișează — **fără a aplica nimic**. Este modalitatea sigură de a răspunde la întrebarea „ce ar schimba această sincronizare?” înainte de a te angaja la ea.
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
Un plan:
* **Nu scrie nimic** — fără migrare de metadate, fără actualizare a înregistrării aplicației, fără modificări ale rolului sau filei implicite și fără generare de client API.
* Returnează **același diff** pe care l-ar aplica o sincronizare reală, astfel încât poți revizui dinainte entitățile create/actualizate/șterse.
* Este util înaintea unei modificări riscante, când revizuiești o modificare generată de AI sau într-un script care ar trebui să eșueze dacă o modificare neașteptată este pe cale să fie aplicată.
<Note>
Un plan previzualizează doar modificările de **metadate** și necesită ca aplicația să fi fost sincronizată cel puțin o dată (astfel încât spațiul de lucru să știe de ea). Dacă îl rulezi pentru o aplicație care nu a fost niciodată sincronizată, serverul va raporta că aplicația nu este instalată — rulează mai întâi o dată `yarn twenty dev`.
</Note>
## Plan de recuperare în trepte
Când metadatele locale par greșite, escaladează în această ordine și oprește-te de îndată ce ești deblocat. Fiecare pas este mai disruptiv decât precedentul.
1. **Resincronizează.** Rulează din nou `yarn twenty apply`. Sincronizările sunt idempotente — rularea din nou a unui manifest curat este sigură și rezolvă adesea o problemă temporară.
2. **Previzualizează planul.** Rulează `yarn twenty plan` pentru a vedea exact ce intenționează să schimbe următoarea sincronizare, fără a o aplica.
3. **Citește eroarea nominalizată.** Dacă o sincronizare eșuează, notează tipul de metadate și `universalIdentifier`-ul din mesaj (vezi mai sus) și localizează acea entitate în manifest. Un conflict indică de obicei un identificator duplicat sau reutilizat.
4. **Dezinstalează și reinstalează.** `yarn twenty app:uninstall`, apoi sincronizează din nou (`yarn twenty dev`). Acest lucru reconstruiește metadatele aplicației de la zero, păstrând în același timp restul spațiului de lucru intact.
5. **Resetare completă (ultimă soluție).** `yarn twenty docker:reset`, apoi reinițializează datele și resincronizează.
<Warning>
`yarn twenty docker:reset` șterge **toate** datele din instanța ta locală — fiecare spațiu de lucru, înregistrare și aplicație. Folosește această comandă doar după ce pașii anteriori au eșuat.
</Warning>
<Note>
Ai întâlnit o eroare de metadate? Te rugăm să [deschizi un issue](https://github.com/twentyhq/twenty/issues/new/choose) și să incluzi mesajul de migrare eșuată (cu tipul de metadate și `universalIdentifier`-ul), rezultatul `Metadata changes` din sincronizare și comenzile pe care le-ai rulat.
</Note>
## Evită sincronizările concurente pe același spațiu de lucru
Sincronizarea aplică migrațiile de metadate. Rularea mai multor operațiuni de sincronizare, deploy sau instalare împotriva aceluiași spațiu de lucru în același timp — de exemplu, mai multe terminale sau agenți AI care iterează în paralel — poate intercala acele migrații și poate lăsa metadatele într-o stare aplicată parțial.
Serverul serializează sincronizările per spațiu de lucru pentru a preveni acest lucru, dar ar trebui totuși să treci operațiunile sensibile de metadate printr-un proces **unic**, în loc să le declanșezi concurent. Dacă orchestrezi dezvoltarea cu mai mulți agenți, rutează apelurile lor de sync/deploy/install printr-o singură coadă, astfel încât să ruleze doar unul la un moment dat.
## Deosebirea tipurilor de erori
Când ceva nu merge bine, diff-ul de metadate și erorile nominalizate îți permit să localizezi eșecul:
* **Eroare de construire a manifestului** — CLI-ul eșuează înainte de sincronizare (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`); repară sursa aplicației.
* **Eroare de sincronizare / migrare** — build-ul reușește, dar aplicarea diff-ului eșuează, menționând entitatea și `universalIdentifier`-ul; repară metadatele care intră în conflict.
* **Eroare de dimensiune a dependențelor** — sincronizarea sau instalarea eșuează deoarece `dependencies` de producție ale aplicației sunt prea mari pentru a fi instalate ca strat de rulare (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`); mută pachetele pe care funcțiile tale logice nu le importă la rulare (biblioteci UI, unelte de dezvoltare) în `devDependencies`.
* **Eroare de execuție în codul aplicației** — sincronizarea reușește, dar funcțiile sau componentele tale logice se comportă incorect la execuție; verifică [jurnalele funcțiilor](/l/ro/developers/extend/apps/operations/cli).
* **Stare locală a instanței** — niciuna dintre cele de mai sus și spațiul de lucru tot arată greșit; coboară pe scara de recuperare.

View File

@@ -0,0 +1,264 @@
---
title: Testare
description: Configurare Vitest, teste de integrare pe un server Twenty real, verificare a tipurilor și CI cu GitHub Actions.
icon: flask
---
SDK-ul oferă API-uri programatice care vă permit să construiți, să distribuiți, să instalați și să dezinstalați aplicația din codul de test. Combinat cu [Vitest](https://vitest.dev/) și clienții API tipizați, puteți scrie teste de integrare care verifică faptul că aplicația funcționează de la un cap la altul pe un server Twenty real.
## Utilizarea pachetelor npm
Puteți instala și utiliza orice pachet npm în aplicația dvs. Atât funcţiile logice cât şi componentele frontale sunt grupate cu [esbuild](https://esbuild.github.io/), care înglobează dependențele lor la ieșire - nu sunt necesare `node_modules` în timpul rulării.
### Instalarea unui pachet
```bash filename="Terminal"
yarn add axios
```
Apoi importați-l în codul dvs.:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
Același lucru funcționează și pentru componentele frontend:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### Cum funcționează împachetarea
Pasul de build folosește esbuild pentru a produce un singur fișier autonom pentru fiecare funcție logică și pentru fiecare componentă frontend. Ambalajele importate sunt încorporate în pachet, cu excepţia celor enumerate mai jos.
**Funcțiile logice** rulează într-un mediu Node.js. Modulele built-in Node (`fs`, `path`, `crypto`, `http` etc.) sunt disponibile și nu trebuie instalate.
**Componentele frontend** rulează într-un Web Worker. Modulele built-in Node **nu** sunt disponibile — doar pachetele npm care funcționează într-un mediu de browser. Reține că sandbox-ul implementează un DOM *parțial*, astfel încât un pachet se poate construi fără erori, dar totuși poate eșua la execuție; vezi [Limitări curente](/l/ro/developers/extend/apps/layout/front-components#current-limitations).
Ambele medii au `twenty-client-sdk/core` și `twenty-client-sdk/metadata` disponibile ca module pre-furnizate — acestea nu sunt incluse în bundle, ci sunt rezolvate la rulare de către server.
Componentele frontului pot de asemenea partaja dependențe printr-un [pachet de dependențe partajate](/l/ro/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components). Pachete listate în câmpul `frontComponentSharedDependencies` din pachetul aplicației. Fiul este construit o dată într-un fișier separat și încărcat din el la rulare în loc să fie înglobat în fiecare componentă, astfel încât o componentă care importă numai pachete comune să nu aibă aproape niciun cod propriu de dependență.
## Configurare
Aplicația generată (scaffolded) include deja Vitest. Dacă o configurați manual, instalați dependențele:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Creați un `vitest.config.ts` în rădăcina aplicației:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
Creează un fișier global de configurare inițială care verifică faptul că serverul este accesibil, scrie un fișier de configurare de test pentru SDK (`~/.twenty/config.test.json`) și sincronizează aplicația înainte ca testele să ruleze:
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## API-uri SDK programatice
Subcalea `twenty-sdk/cli` exportă funcții pe care le puteți apela direct din codul de test:
| Funcție | Descriere |
| -------------- | -------------------------------------------------------------------------------------- |
| `appBuild` | Construiți aplicația și, opțional, împachetați un tarball |
| `appDeploy` | Încărcați un tarball pe server |
| `appDevOnce` | Construiește și sincronizează aplicația o singură dată (la fel ca `yarn twenty apply`) |
| `appInstall` | Instalați aplicația în spațiul de lucru activ |
| `appUninstall` | Dezinstalați aplicația din spațiul de lucru activ |
Fiecare funcție returnează un obiect rezultat cu `success: boolean` și fie `data`, fie `error`.
## Scrierea unui test de integrare
Iată un exemplu complet care construiește, distribuie și instalează aplicația, apoi verifică faptul că aceasta apare în spațiul de lucru:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## Rularea testelor
Asigurați-vă că serverul Twenty local rulează, apoi:
```bash filename="Terminal"
yarn test
```
Sau în modul watch în timpul dezvoltării:
```bash filename="Terminal"
yarn test:watch
```
## Verificarea tipurilor
Puteți rula și verificarea tipurilor pe aplicație fără a rula testele:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
Aceasta rulează `tsc --noEmit` pe fișierul `tsconfig.json` al aplicației și raportează orice erori de tip. Aplicațiile create cu scaffolderul includ, de asemenea, un script `yarn typecheck` care acoperă și fișierele de test (`tsconfig.spec.json`).
## CI cu GitHub Actions
Scaffolderul generează un workflow gata de utilizare la `.github/workflows/ci.yml`. La fiecare push pe `main` și la fiecare pull request, acesta pornește un server Twenty efemer în runner (prin acțiunea `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`), apoi rulează `yarn lint`, `yarn typecheck`, `yarn test:unit` și `yarn test` cu `TWENTY_API_URL` / `TWENTY_API_KEY` îndreptate către acel server. Nu sunt necesare secrete și poți fixa versiunea serverului prin variabila de mediu `TWENTY_VERSION` din partea de sus a workflow-ului.
Vezi [Publicare → CI/CD automatizat](/l/ro/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) pentru un ghid complet al celor trei workflow-uri create cu scaffolderul (`ci.yml`, pipeline-ul de deploy `cd.yml` și `publish.yml` pentru publicare pe npm).

View File

@@ -0,0 +1,142 @@
---
title: Traduceri
description: Lansează-ți aplicația în mai multe limbi — tradu etichetele din manifest și șirurile UI printr-un singur catalog de locale.
icon: language
---
Aplicațiile Twenty sunt create în **engleză**: șirurile din sursă și din manifest
sunt textul sursă în engleză, `en` este localizarea sursă din care traduci, iar
orice localizare lăsată netradusă revine implicit la aceasta.
Aplicația ta are două tipuri de text traducibil și ambele trec prin același
catalog `locales/`:
* **Etichete de manifest** — nume de obiecte și câmpuri, titluri de vizualizări, elemente de meniu și
alte șiruri declarate în metadatele aplicației tale.
* **Șiruri componentă-față** - textul UI al componentelor dvs. React din față.
Marchează șirurile traduse, le extrag în cataloage per-localizare,
traduce acele cataloage, iar clădirea servește limba corectă pentru utilizatorul curent* fără cableri suplimentare.
## Marcarea sirurilor componentei frontale
Importă ajutătoarele de traducere din `douăzeci de aplicații/front-componente`:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### Când să utilizaţi care
* **`Trans`** — text static în JSX. Folosește proprietățile `message` și `values` pentru
interpolare, ca în `Trans message="Hi {name}" values={{ name }}`;
interpolarea direct în elementele copil nu poate fi extrasă static.
* **`useTranslate().t`** - șiruri dinamice în interiorul unei componente. Re-randează atunci când utilizatorul
schimbă limba. Preferă acest lucru în redare.
* **`t(...)`** (importat direct) - traducerea mai ușoară **oriunde**,
inclusiv manipulatorii de evenimente, ajutoarele și domeniul de aplicare al modulului - nu numai în randare.
* **`msg(...)`** — un descriptor leneș pentru șiruri declarate ca date (constante,
config). Rezolvați-l mai târziu cu `t(descriptor)`.
### Context
Treceți `context` pentru a disambigua șirurile sursă identice care traduc
în mod diferit:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
Șirurile contextuale apar în cataloagele de localizare ca un grup indexat după
contextul lor: `"\<context>": { "\<message>": "\<translation>" }`. Etichetele din manifest sunt
contextuale prin construcție — fiecare poartă rolul de metadată pe care îl joacă
(`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …), astfel încât
același cuvânt poate fi tradus diferit ca nume de obiect și ca nume de vizualizare.
## Extragerea și traducerea
Rulează comanda de extragere din directorul aplicației:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
Procesul de extragere colectează atât etichetele manifestului, cât și șirurile `t()`/`msg()`/`Trans`
din sursa front-component în `locales/\<locale>.json`, indexate după
șirul sursă. Completează traducerile:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
Șirurile fără context sunt intrări simple; șirurile contextuale — inclusiv fiecare etichetă din manifest — sunt îmbricate sub cheia lor de context.
Localnicii ca `{name}` sunt substituiți la timp - țineți-le în traducerea
. Orice şir rămas gol se întoarce la textul sursă.
## Cum rulează
`douăzeci de dev:build` compilează cataloagele şi serveşte limba corectă pentru utilizatorul curent
: etichetele manifest sunt rezolvate pe server, iar cataloagele de tip front-component
sunt copite în fiecare pachet de componente. La runtime, o componentă citește localizarea
din contextul de execuție (limba curentă a gazdei) și rezolvă
fiecare șir din catalog, revenind la sursă atunci când lipsește o traducere
. Schimbarea limbii în gazdă reafișează în timp real șirurile `Trans` și
`useTranslate().t`.
Etichetele manifestului sunt compilate la fiecare sincronizare — `twenty dev:build`, `twenty apply`
și monitorizarea continuă `twenty dev` — astfel că editarea unui fișier de localizare și
sincronizarea sunt suficiente pentru a vedea textul nou. Cataloagele componentelor Front sunt integrate în
fiecare pachet de componente la momentul compilării, astfel că modificarea unuia dintre ele înseamnă totuși
rularea din nou a `twenty dev:build` (și redistribuirea).
Elementele text copil ale `Trans` pot acoperi mai multe linii — spațiile albe sunt comprimate în
același mod în care JSX le comprimă, astfel încât ambele variante sunt extrase cu cheia `Welcome back`:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -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>

View File

@@ -0,0 +1,307 @@
---
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ă. Serverul creează deja automat pentru fiecare obiect vizualizarea implicită "All ...", astfel încât vizualizările pe care le definește o aplicație sunt unele suplimentare, selectate cu atenție.
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
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 { RestApiClient } from 'twenty-client-sdk/rest';
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 res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
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 { RestApiClient } from 'twenty-client-sdk/rest';
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 = new RestApiClient().resolveUrl('/s/documents/view', {
query: { 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 în vizualizarea înregistrării unui obiect; o filă cu un singur widget
`FRONT_COMPONENT` găzduiește componenta pe întreaga lățime:
```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.VERTICAL_LIST,
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',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, 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).
Widgeturile `FIELD` acceptă și alte moduri de afișare și opțiuni, inclusiv
listarea înregistrărilor unei relații imbricate aflate la două salturi distanță — consultă
[referința pentru widgetul field](/l/ro/developers/extend/apps/layout/page-layouts#field-widgets).
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,149 @@
---
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ției sunt accesate prin calea `/s/`; pe instanțele locale
și auto-găzduite, această cale este servită de serverul tău Twenty
(de ex. `http://localhost:2020/s/documents/generate`). Pe Twenty Cloud, `RestApiClient`
rezolvă calea prin domeniul de funcții al spațiului de lucru.
## 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>

View File

@@ -0,0 +1,70 @@
---
title: "Tutorial: Document Generator"
icon: wand-magic-sparkles
description: Construiește o aplicație Twenty reală care generează documente personalizate din datele din CRM-ul tău.
---
Î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, printr-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>

View File

@@ -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 un logo în `public/` și fă referire la el cu `logo`.
```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.',
logo: '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. Plasează câteva fișiere PNG în
`public/gallery/` și fă referire la ele cu `galleryImages` — acestea sunt afișate ca o
galerie pe pagina listării, în ordinea din array.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'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 plan # preview the metadata diff
```
Planul afișează exact ce s-ar schimba pe server, fără a aplica modificările —
o bună verificare 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>