i18n - docs translations (#22335)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22335?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
ba8e1bf5a3
commit
10e0f1d29f
@@ -1,104 +0,0 @@
|
||||
---
|
||||
title: Arhitectură
|
||||
description: Cum funcționează aplicațiile Twenty — izolarea (sandboxing), ciclul de viață și elementele de bază.
|
||||
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.
|
||||
|
||||
```
|
||||
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, permisiuni, variabile | [Model de date](/l/ro/developers/extend/apps/data-model) |
|
||||
| **Rol** | Seturi de permisiuni pentru obiecte și câmpuri | [Model de date](/l/ro/developers/extend/apps/data-model) |
|
||||
| **Obiect** | Tabele de date personalizate cu câmpuri | [Model de date](/l/ro/developers/extend/apps/data-model) |
|
||||
| **Câmp** | Extindeți obiectele existente, definiți relații | [Model de date](/l/ro/developers/extend/apps/data-model) |
|
||||
| **Funcție logică** | TypeScript pe partea de server cu declanșatoare | [Funcții logice](/l/ro/developers/extend/apps/logic-functions) |
|
||||
| **Componentă front-end** | UI React izolat în pagina Twenty | [Componente front-end](/l/ro/developers/extend/apps/front-components) |
|
||||
| **Abilitate** | Instrucțiuni reutilizabile pentru agenți AI | [Abilități și agenți](/l/ro/developers/extend/apps/skills-and-agents) |
|
||||
| **Agent** | Agenți AI cu prompturi personalizate | [Abilități și agenți](/l/ro/developers/extend/apps/skills-and-agents) |
|
||||
| **Vizualizare** | Vizualizări preconfigurate ale listelor de înregistrări | [Aspect](/l/ro/developers/extend/apps/layout) |
|
||||
| **Element de meniu de navigare** | Intrări personalizate în bara laterală | [Aspect](/l/ro/developers/extend/apps/layout) |
|
||||
| **Layout pagină** | File și widgeturi personalizate ale paginii de înregistrare | [Aspect](/l/ro/developers/extend/apps/layout) |
|
||||
|
||||
## Izolare (sandboxing)
|
||||
|
||||
* **Funcțiile logice** rulează în procese Node.js izolate pe server. Acestea accesează datele doar prin clientul API tipizat, limitat de permisiunile rolului aplicației.
|
||||
* **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. Tokenul de rulare (`TWENTY_APP_ACCESS_TOKEN`) este derivat din rolul definit în `defineApplication()`.
|
||||
|
||||
## Ciclul de viață al aplicației
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Development │
|
||||
│ npx create-twenty-app → yarn twenty dev (live sync) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Build & Deploy │
|
||||
│ yarn twenty build → yarn twenty deploy │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Install flow │
|
||||
│ upload → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 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 build`** — compilează TypeScript, împachetează funcțiile logice și componentele front-end cu esbuild și produce un manifest.
|
||||
* **Hook-uri pre/post-instalare** — funcții logice opționale care rulează în timpul instalării. Consultați [Funcții logice](/l/ro/developers/extend/apps/logic-functions) pentru detalii.
|
||||
|
||||
## Pașii următori
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Model de date" icon="database" href="/l/ro/developers/extend/apps/data-model">
|
||||
Definiți obiecte, câmpuri, roluri și relații.
|
||||
</Card>
|
||||
<Card title="Funcții logice" icon="bolt" href="/l/ro/developers/extend/apps/logic-functions">
|
||||
Funcții pe partea de server cu declanșatoare HTTP, cron și de evenimente.
|
||||
</Card>
|
||||
<Card title="Componente front-end" icon="window-maximize" href="/l/ro/developers/extend/apps/front-components">
|
||||
Componente React izolate în interfața Twenty.
|
||||
</Card>
|
||||
<Card title="Aspect" icon="table-columns" href="/l/ro/developers/extend/apps/layout">
|
||||
Vizualizări, elemente de navigare și layout-uri ale paginilor de înregistrare.
|
||||
</Card>
|
||||
<Card title="Abilități și agenți" icon="robot" href="/l/ro/developers/extend/apps/skills-and-agents">
|
||||
Abilități și agenți AI cu prompturi personalizate.
|
||||
</Card>
|
||||
<Card title="CLI & Testare" icon="terminal" href="/l/ro/developers/extend/apps/cli-and-testing">
|
||||
Comenzi CLI, testare, resurse, remote-uri și CI.
|
||||
</Card>
|
||||
<Card title="Publicare" icon="rocket" href="/l/ro/developers/extend/apps/publishing">
|
||||
Implementați pe un server sau publicați în marketplace.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,434 +0,0 @@
|
||||
---
|
||||
title: CLI & Testare
|
||||
description: Comenzi CLI, configurare pentru testare, resurse publice, pachete npm, remote-uri și configurare CI.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## Resurse publice (folderul `public/`)
|
||||
|
||||
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 `logoUrl` și `screenshots` 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ă.
|
||||
* **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 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, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
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, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
export default defineFrontComponent(() => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Utilizarea pachetelor npm
|
||||
|
||||
Puteți instala și utiliza orice pachet npm în aplicația dvs. Atât funcțiile logice, cât și componentele frontend sunt împachetate cu [esbuild](https://esbuild.github.io/), care integrează toate dependențele în output — nu sunt necesare `node_modules` la rulare.
|
||||
|
||||
### 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. Toate pachetele importate sunt integrate în bundle.
|
||||
|
||||
**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 API-urile de browser și pachetele npm care funcționează într-un mediu de browser.
|
||||
|
||||
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.
|
||||
|
||||
## Testarea aplicației
|
||||
|
||||
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ă cap-coadă împotriva unui server Twenty real.
|
||||
|
||||
### 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';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Creați un fișier de configurare care verifică faptul că serverul este accesibil înainte de rularea testelor:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### API-uri SDK programatice
|
||||
|
||||
Subruta `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 |
|
||||
| `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 typecheck
|
||||
```
|
||||
|
||||
Aceasta rulează `tsc --noEmit` și raportează orice erori de tip.
|
||||
|
||||
## Referință CLI
|
||||
|
||||
Dincolo de `dev`, `build`, `add` și `typecheck`, CLI oferă comenzi pentru executarea funcțiilor, vizualizarea jurnalelor și gestionarea instalărilor de aplicații.
|
||||
|
||||
### Executarea funcțiilor (`yarn twenty 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 exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
### Vizualizarea jurnalelor funcțiilor (`yarn twenty 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 logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
Acest lucru este diferit de `yarn twenty server logs`, care afișează jurnalele containerului Docker. `yarn twenty logs` afișează jurnalele de execuție ale funcțiilor aplicației de pe serverul Twenty.
|
||||
</Note>
|
||||
|
||||
### Dezinstalarea unei aplicații (`yarn twenty uninstall`)
|
||||
|
||||
Eliminați aplicația din spațiul de lucru activ:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty 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 --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
Acreditările dvs. sunt stocate în `~/.twenty/config.json`.
|
||||
|
||||
## CI cu GitHub Actions
|
||||
|
||||
Scaffolderul generează un workflow GitHub Actions gata de utilizare în `.github/workflows/ci.yml`. Rulează automat testele de integrare la fiecare push pe `main` și la pull request-uri.
|
||||
|
||||
Workflow-ul:
|
||||
|
||||
1. Preia codul
|
||||
2. Pornește un server Twenty temporar folosind acțiunea `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Instalează dependențele cu `yarn install --immutable`
|
||||
4. Rulează `yarn test` cu `TWENTY_API_URL` și `TWENTY_API_KEY` injectate din rezultatele acțiunii
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
Nu trebuie să configurați niciun secret — acțiunea `spawn-twenty-docker-image` pornește un server Twenty efemer direct în runner și oferă detaliile de conectare. Secretul `GITHUB_TOKEN` este furnizat automat de GitHub.
|
||||
|
||||
Pentru a fixa o versiune Twenty specifică în loc de `latest`, modificați variabila de mediu `TWENTY_VERSION` din partea de sus a workflow-ului.
|
||||
@@ -1,193 +0,0 @@
|
||||
---
|
||||
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',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
defaultRoleUniversalIdentifier: '...',
|
||||
// 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>/apps/oauth/callback
|
||||
```
|
||||
|
||||
</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` / `handle` | Numele afișat al conexiunii (derivat automat la callback-ul OAuth, poate fi redenumit de utilizator) |
|
||||
| `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>/apps/oauth/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>
|
||||
@@ -1,493 +0,0 @@
|
||||
---
|
||||
title: Model de date
|
||||
description: Definiți obiecte, câmpuri, roluri și metadatele aplicației cu SDK-ul Twenty.
|
||||
icon: database
|
||||
---
|
||||
|
||||
Pachetul `twenty-sdk` furnizează funcții `defineEntity` pentru a declara modelul de date al aplicației dvs. Trebuie să folosiți `export default defineEntity({...})` pentru ca SDK-ul să detecteze entitățile. Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
|
||||
|
||||
<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. Gruparea fișierelor după tip (de exemplu, `logic-functions/`, `roles/`) este doar o convenție pentru organizarea codului, nu o cerință.
|
||||
</Note>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineRole" description="Configurați permisiunile rolurilor și accesul la obiecte">
|
||||
|
||||
Rolurile încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru.
|
||||
|
||||
```ts restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
PermissionFlag,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} 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,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineApplication" description="Configurați metadatele aplicației (obligatoriu, una per aplicație)">
|
||||
|
||||
Fiecare aplicație trebuie să aibă exact un apel `defineApplication` care descrie:
|
||||
|
||||
* **Identitate**: identificatori, nume de afișare și descriere.
|
||||
* **Permisiuni**: ce rol folosesc funcțiile și componentele front-end ale acesteia.
|
||||
* **(Opțional) Variabile**: perechi cheie–valoare expuse funcțiilor ca variabile de mediu.
|
||||
* **(Opțional) funcții de pre-instalare / post-instalare**: funcții logice care rulează înainte sau după instalare.
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
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 (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` trebuie să facă referire la un rol definit cu `defineRole()` (vezi mai sus).
|
||||
* Funcțiile de pre-instalare și post-instalare sunt detectate automat în timpul construirii manifestului — nu trebuie să le referiți în `defineApplication()`.
|
||||
|
||||
#### Metadate pentru marketplace
|
||||
|
||||
Dacă intenționați să [publicați aplicația](/l/ro/developers/extend/apps/publishing), aceste câmpuri opționale controlează modul în care apare în marketplace:
|
||||
|
||||
| Câmp | Descriere |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `autor` | Numele autorului sau al companiei |
|
||||
| `categorie` | Categoria aplicației pentru filtrarea în marketplace |
|
||||
| `logoUrl` | Calea către logo-ul aplicației (de ex., `public/logo.png`) |
|
||||
| `screenshots` | Array de căi către capturi de ecran (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 |
|
||||
|
||||
#### Roluri și permisiuni
|
||||
|
||||
Câmpul `defaultRoleUniversalIdentifier` din `application-config.ts` desemnează rolul implicit utilizat de funcțiile logice și componentele front-end ale aplicației. Consultați `defineRole` mai sus pentru detalii.
|
||||
|
||||
* Tokenul de runtime injectat ca `TWENTY_APP_ACCESS_TOKEN` este derivat din acest rol.
|
||||
* Clientul tipizat este restricționat la permisiunile acordate acelui rol.
|
||||
* Respectați principiul celui mai mic privilegiu: creați un rol dedicat doar cu permisiunile de care au nevoie funcțiile.
|
||||
|
||||
##### Rol implicit pentru funcții
|
||||
|
||||
Când generați o aplicație nouă, CLI creează un fișier de rol implicit:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
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: [],
|
||||
permissionFlags: [],
|
||||
});
|
||||
```
|
||||
|
||||
`universalIdentifier` al acestui rol este apoi referențiat în `application-config.ts` ca `defaultRoleUniversalIdentifier`.
|
||||
|
||||
* **\*.role.ts** definește ce poate face rolul.
|
||||
* **application-config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui.
|
||||
|
||||
Notițe:
|
||||
* Porniți de la rolul generat, apoi restrângeți-l progresiv urmând principiul celui mai mic privilegiu.
|
||||
* Înlocuiți `objectPermissions` și `fieldPermissions` cu obiectele și câmpurile de care au nevoie efectiv funcțiile.
|
||||
* `permissionFlags` controlează accesul la capabilități la nivelul platformei. Mențineți-le la minimum.
|
||||
* Vedeți un exemplu funcțional: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineObject" description="Definiți obiecte personalizate cu câmpuri">
|
||||
|
||||
Obiectele personalizate descriu atât schema, cât și comportamentul înregistrărilor din spațiul dvs. de lucru. Utilizați `defineObject()` pentru a defini obiecte cu validare încorporată:
|
||||
|
||||
```ts postCard.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:
|
||||
|
||||
* Folosiți `defineObject()` pentru validare încorporată și suport mai bun în IDE.
|
||||
* `universalIdentifier` trebuie să fie unic și stabil între implementări.
|
||||
* Fiecare câmp necesită un `name`, un `type`, un `label` și propriul `universalIdentifier` stabil.
|
||||
* Matricea `fields` este opțională — puteți defini obiecte fără câmpuri personalizate.
|
||||
* Puteți genera obiecte noi folosind `yarn twenty add`, care vă ghidează prin denumire, câmpuri și relații.
|
||||
|
||||
<Note>
|
||||
**Câmpurile de bază sunt create automat.** Când definiți un obiect personalizat, Twenty adaugă automat câmpuri standard
|
||||
precum `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` și `deletedAt`.
|
||||
Nu trebuie să le definiți în tabloul `fields` — adăugați doar câmpurile personalizate proprii.
|
||||
Puteți suprascrie câmpurile implicite definind un câmp cu același nume în tabloul `fields`,
|
||||
dar acest lucru nu este recomandat.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Câmpuri standard" description="Extindeți obiectele existente cu câmpuri suplimentare">
|
||||
|
||||
Utilizați `defineField()` pentru a adăuga câmpuri la obiecte pe care nu le dețineți — cum ar fi obiectele standard Twenty (Person, Company etc.). sau obiecte din alte aplicații. Spre deosebire de câmpurile inline din `defineObject()`, 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, utilizați `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exportați din `twenty-sdk`.
|
||||
* Atunci când definiți câmpuri inline în `defineObject()`, nu aveți nevoie de `objectUniversalIdentifier` — 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()`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Câmpuri de relație" description="Conectați obiectele între ele cu relații bidirecționale">
|
||||
|
||||
Relațiile conectează obiectele între ele. În Twenty, relațiile sunt întotdeauna bidirecționale — definiți ambele părți, iar fiecare parte o referențiază pe cealaltă.
|
||||
|
||||
Există două tipuri de relații:
|
||||
|
||||
| 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
|
||||
|
||||
Presupuneți că 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 fișier. Sistemul de build le rezolvă în timpul compilării.
|
||||
</Note>
|
||||
|
||||
#### 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 |
|
||||
| ------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `tip` | 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`) |
|
||||
|
||||
#### Câmpuri de relație inline în defineObject
|
||||
|
||||
Puteți defini, de asemenea, câmpuri de relație direct în `defineObject()`. În acest caz, 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
|
||||
],
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Generarea scheletului entităților cu `yarn twenty add`
|
||||
|
||||
În loc să creați manual fișiere de entități, puteți folosi generatorul interactiv (scaffolder):
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add
|
||||
```
|
||||
|
||||
Acesta vă solicită să alegeți un tip de entitate și vă ghidează prin câmpurile necesare. Generează 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 add object
|
||||
yarn twenty add logicFunction
|
||||
yarn twenty add frontComponent
|
||||
```
|
||||
|
||||
### Tipuri de entități disponibile
|
||||
|
||||
| Tipul entității | Comandă | Fișier generat |
|
||||
| ---------------------------- | ------------------------------------ | ------------------------------------------------------- |
|
||||
| Obiect | `yarn twenty add object` | `src/objects/\<name>.ts` |
|
||||
| Câmp | `yarn twenty add field` | `src/fields/\<name>.ts` |
|
||||
| Funcție logică | `yarn twenty add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| Componentă frontend | `yarn twenty add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| Rol | `yarn twenty add role` | `src/roles/\<name>.ts` |
|
||||
| Abilitate | `yarn twenty add skill` | `src/skills/\<name>.ts` |
|
||||
| Agent | `yarn twenty add agent` | `src/agents/\<name>.ts` |
|
||||
| Vizualizare | `yarn twenty add view` | `src/views/\<name>.ts` |
|
||||
| Element de meniu de navigare | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| Machetă de pagină | `yarn twenty add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
|
||||
### Ce generează scaffolder-ul
|
||||
|
||||
Fiecare tip de entitate are propriul său șablon. De exemplu, `yarn twenty 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 add logicFunction --path src/custom-folder
|
||||
```
|
||||
@@ -1,489 +0,0 @@
|
||||
---
|
||||
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. Rulează într-un **Web Worker** izolat folosind Remote DOM — codul este izolat (sandboxed), dar se redă nativ în pagină, nu într-un iframe.
|
||||
|
||||
## Unde pot fi utilizate componentele frontale
|
||||
|
||||
Componentele frontale pot fi afișate în două locații în cadrul Twenty:
|
||||
|
||||
* **Panou lateral** — Componentele frontale care nu sunt headless se deschid în panoul lateral din dreapta. Acesta este comportamentul implicit atunci când o componentă frontală 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ă. La configurarea unui tablou de bord sau a machetei unei pagini de înregistrare, utilizatorii pot adăuga un widget de componentă frontală.
|
||||
|
||||
## Exemplu de bază
|
||||
|
||||
Cel mai rapid mod de a vedea o componentă front-end în acțiune este să o înregistrați ca **element din meniul de comenzi**. Folosiți `defineCommandMenuItem` într-un fișier separat pentru ca componenta să apară ca 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',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
După sincronizarea cu `yarn twenty dev` (sau prin rularea comenzii `yarn twenty dev --once` o singură dată), 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ă**. Consultați secțiunea [definePageLayout](/l/ro/developers/extend/apps/skills-and-agents#definepagelayout) pentru detalii.
|
||||
|
||||
## Headless vs non-headless
|
||||
|
||||
Componentele frontale 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 { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
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 de interfață la final.
|
||||
|
||||
Importă-le din `twenty-sdk/command`:
|
||||
|
||||
* **`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 anumită pagină din panoul lateral. Props: `page`, `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/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
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',
|
||||
icon: 'IconPlayerPlay',
|
||||
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/command';
|
||||
|
||||
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,
|
||||
});
|
||||
```
|
||||
|
||||
## 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,
|
||||
useRecordId,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
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ă |
|
||||
| `useFrontComponentExecutionContext(selector)` | variază | Accesați întregul context de execuție cu o funcție selector |
|
||||
|
||||
## 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 |
|
||||
|
||||
Iată un exemplu care folosește API-ul gazdă 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 { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
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,
|
||||
});
|
||||
```
|
||||
|
||||
### 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, numberOfSelectedRecords } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
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,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## defineCommandMenuItem
|
||||
|
||||
Folosiți `defineCommandMenuItem` pentru a înregistra o componentă front-end în meniul de comenzi (Cmd+K). Dacă `isPinned` este `true`, apare și ca buton 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',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
| 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 | Numele pictogramei afișat lângă etichetă (de ex. `'IconBolt'`, `'IconSend'`) |
|
||||
| `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ă), `'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ă pentru a controla dinamic dacă comanda este vizibilă (vezi mai jos) |
|
||||
|
||||
## 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,
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
**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 |
|
||||
|
||||
## Resurse publice
|
||||
|
||||
Componentele front-end pot accesa fișiere din directorul `public/` al aplicației folosind `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
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/cli-and-testing#public-assets-public-folder) pentru detalii.
|
||||
|
||||
## Stilizare
|
||||
|
||||
Componentele front-end acceptă mai multe abordări de stilizare. Puteți folosi:
|
||||
|
||||
* **Stiluri inline** — `style={{ color: 'red' }}`
|
||||
* **Componente Twenty UI** — import din `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar și altele)
|
||||
* **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
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||||
|
||||
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,
|
||||
});
|
||||
```
|
||||
@@ -1,273 +0,0 @@
|
||||
---
|
||||
title: Începeți
|
||||
icon: rocket
|
||||
description: Creați prima dvs. aplicație Twenty în câteva minute.
|
||||
---
|
||||
|
||||
## Cerințe
|
||||
|
||||
* **Node.js 24+** — [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 de schelet le reduce la o singură comandă pe happy path, 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 server` | 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
|
||||
```
|
||||
|
||||
Vi se va cere un nume și o descriere — apăsați **Enter** pentru valorile implicite. Aceasta generează un proiect TypeScript în `my-twenty-app/` cu un fișier inițial `application-config.ts`, un rol implicit, un flux de lucru CI ș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 schelet vă propune să pornească unul pentru dvs.:
|
||||
|
||||
> **Doriți să configurați o instanță Twenty locală?**
|
||||
|
||||
* **Yes (recomandat)** — descarcă imaginea Docker `twentycrm/twenty-app-dev` și o pornește pe portul `2020`. Asigurați-vă mai întâi că Docker rulează.
|
||||
* **No** — alegeți această opțiune dacă aveți deja un server Twenty la care doriți să vă conectați. Îl puteți conecta ulterior cu `yarn twenty remote add`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Porniți instanța locală?" />
|
||||
</div>
|
||||
|
||||
După ce serverul pornește, se deschide un browser pentru autentificare. Folosiți contul demo preconfigurat:
|
||||
|
||||
* **E-mail:** `tim@apple.dev`
|
||||
* **Parolă:** `tim@apple.dev`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Ecranul de autentificare Twenty" />
|
||||
</div>
|
||||
|
||||
Faceți clic pe **Authorize** pe ecranul următor — aceasta oferă CLI-ului acces la spațiul dvs. de lucru.
|
||||
|
||||
<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 server 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 decurs de o secundă serverul reflectă modificarea. Veți vedea în terminal un panou de stare în timp real.
|
||||
|
||||
Pentru un output 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
|
||||
|
||||
Adăugați `--once` pentru a rula un singur build + sync și a ieși — același flux, fără watcher:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| 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 dev --once` | Un singur build + sync, iese cu `0` la succes, `1` la eșec. | CI, hook-uri pre-commit, agenți AI, fluxuri de lucru scriptate. |
|
||||
|
||||
Ambele moduri necesită un server în modul de dezvoltare și un remote autentificat.
|
||||
|
||||
<Warning>
|
||||
Modul de dezvoltare este disponibil doar pe instanțele Twenty care rulează în modul development (`NODE_ENV=development`). Instanțele de producție resping cererile de sincronizare din modul de dezvoltare — folosiți `yarn twenty deploy` pentru a implementa pe serverele de producție. Consultați [Publicarea aplicațiilor](/l/ro/developers/extend/apps/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
## 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ă: [Crearea aplicațiilor](/l/ro/developers/extend/apps/building).
|
||||
|
||||
## Structura proiectului
|
||||
|
||||
```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
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
```
|
||||
|
||||
| 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/__tests__/` | Teste de integrare (configurare + test exemplu). |
|
||||
| `public/` | Resurse statice (imagini, fonturi) servite împreună cu aplicația. |
|
||||
|
||||
### Pornind de la un exemplu
|
||||
|
||||
Folosiți `--example` pentru a începe cu un proiect mai complet (obiecte personalizate, câmpuri, funcții logice, componente front-end):
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
Exemplele se află în [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). Puteți, de asemenea, să creați scheletul entităților individuale într-un proiect existent cu `yarn twenty add` — vedeți [Crearea aplicațiilor](/l/ro/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
|
||||
|
||||
---
|
||||
|
||||
## Gestionarea serverului local
|
||||
|
||||
Folosiți `yarn twenty server` pentru a controla containerul Twenty local:
|
||||
|
||||
| Comandă | Ce face |
|
||||
| -------------------------------------- | ------------------------------------------------------------ |
|
||||
| `yarn twenty server start` | Pornește serverul (descarcă imaginea dacă este necesar) |
|
||||
| `yarn twenty server start --port 3030` | Pornește pe un port personalizat |
|
||||
| `yarn twenty server stop` | Oprește serverul (păstrează datele) |
|
||||
| `yarn twenty server status` | Afișează URL-ul, versiunea și credențialele de autentificare |
|
||||
| `yarn twenty server logs` | Transmite în flux jurnalele serverului |
|
||||
| `yarn twenty server reset` | Șterge datele și pornește de la zero |
|
||||
| `yarn twenty server upgrade` | Descarcă cea mai recentă imagine `twenty-app-dev` |
|
||||
| `yarn twenty server 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.
|
||||
|
||||
### Actualizarea imaginii serverului
|
||||
|
||||
`yarn twenty server 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 server start` pentru a aștepta până când devine funcțional.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server upgrade # Latest
|
||||
yarn twenty server upgrade 2.2.0 # Specific version
|
||||
```
|
||||
|
||||
Puteți verifica versiunea care rulează cu `yarn twenty server status` (aceasta afișează `APP_VERSION` încorporat în container).
|
||||
|
||||
### Rularea unei instanțe de test în paralel
|
||||
|
||||
Adăugați `--test` la orice comandă `server` 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 server start --test` | Pornește instanța de test (implicit pe portul 2021) |
|
||||
| `yarn twenty server stop --test` | Opriți-o |
|
||||
| `yarn twenty server status --test` | Afișați-i starea |
|
||||
| `yarn twenty server logs --test` | Transmiteți în flux jurnalele sale |
|
||||
| `yarn twenty server reset --test` | Ștergeți-i datele |
|
||||
| `yarn twenty server upgrade --test` | Actualizați-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 server 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>
|
||||
|
||||
---
|
||||
|
||||
## Depanare
|
||||
|
||||
* **Erori Docker** — Asigurați-vă că Docker Desktop (sau daemonul) rulează înainte de `yarn twenty server start`. Mesajul de eroare va afișa comanda corectă de pornire pentru sistemul dvs. de operare.
|
||||
* **Versiune Node greșită** — Aveți nevoie de 24+. Verificați cu `node -v`.
|
||||
* **Lipsește Yarn 4** — Rulați `corepack enable`.
|
||||
* **Dependențe nefuncționale** — `rm -rf node_modules && yarn install`.
|
||||
|
||||
Blocat? Întrebați pe [Discordul Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
@@ -1,178 +0,0 @@
|
||||
---
|
||||
title: Aspect
|
||||
description: Definiți vizualizări, elemente de meniu de navigare și layouturi de pagină pentru a stabili modul în care aplicația dvs. se afișează în Twenty.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
Entitățile de layout controlează modul în care aplicația dvs. apare în UI-ul Twenty — ce se află în bara laterală, care vizualizări salvate vin împreună cu aplicația și cum este aranjată pagina de detalii a unei înregistrări.
|
||||
|
||||
## Concepte de layout
|
||||
|
||||
| Concept | Ce controlează | Entitate |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||
| **Vizualizare** | O configurație salvată a unei liste pentru un obiect — câmpuri vizibile, ordine, filtre, grupuri | `defineView` |
|
||||
| **Element de meniu de navigare** | Un element în bara laterală stângă care face legătura către o vizualizare sau un URL extern | `defineNavigationMenuItem` |
|
||||
| **Layout pagină** | Filele și widgeturile care alcătuiesc pagina de detalii a unei înregistrări | `definePageLayout` |
|
||||
| **Filă layout pagină** | O filă independentă atașată unui layout pagină existent (standard sau al propriei tale aplicații) | `definePageLayoutTab` |
|
||||
|
||||
Vizualizările, elementele de navigare și layouturile de pagină fac referire unele la altele prin `universalIdentifier`:
|
||||
|
||||
* Un **element de meniu de navigare** de tip `VIEW` indică către un identificator `defineView`, astfel încât linkul din bara laterală deschide acea vizualizare salvată.
|
||||
* Un **layout de pagină** de tip `RECORD_PAGE` vizează un obiect și poate încorpora [componente frontale](/l/ro/developers/extend/apps/front-components) în filele sale ca widgeturi.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineView" description="Definește vizualizări salvate pentru obiecte">
|
||||
|
||||
Vizualizările sunt configurații salvate despre cum sunt afișate înregistrările unui obiect — inclusiv ce câmpuri sunt vizibile, ordinea lor și orice filtre sau grupuri aplicate. Utilizați `defineView()` pentru a livra vizualizări preconfigurate împreună cu aplicația:
|
||||
|
||||
```ts src/views/example-view.ts
|
||||
import { defineView, ViewKey } 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',
|
||||
key: ViewKey.INDEX,
|
||||
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.
|
||||
* `key` determină tipul vizualizării (de ex., `ViewKey.INDEX` pentru vizualizarea principală de listă).
|
||||
* `fields` controlează ce coloane apar și ordinea acestora. Fiecare câmp face referire la un `fieldMetadataUniversalIdentifier`.
|
||||
* Puteți defini, de asemenea, `filters`, `filterGroups`, `groups` și `fieldGroups` pentru configurații mai avansate.
|
||||
* `position` controlează ordonarea atunci când există mai multe vizualizări pentru același obiect.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineNavigationMenuItem" description="Definește linkuri de navigare în bara laterală">
|
||||
|
||||
Elementele de meniu de navigare adaugă intrări personalizate în bara laterală a spațiului de lucru. Utilizați `defineNavigationMenuItem()` pentru a crea linkuri către vizualizări, URL-uri externe sau obiecte:
|
||||
|
||||
```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 trimitere elementul de meniu: `NavigationMenuItemType.VIEW` pentru o vizualizare salvată sau `NavigationMenuItemType.LINK` pentru un URL extern.
|
||||
* Pentru link-uri către vizualizări, setați `viewUniversalIdentifier`. Pentru link-uri externe, setați `link`.
|
||||
* `position` controlează ordonarea în bara laterală.
|
||||
* `icon` și `color` (opțional) personalizează aspectul.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePageLayout" description="Definiți machete de pagină personalizate pentru vizualizările de înregistrare">
|
||||
|
||||
Machetele de pagină vă permit să personalizați aspectul unei pagini de detalii a unei înregistrări — ce file apar, ce widgeturi sunt în fiecare filă și cum sunt aranjate. Utilizați `definePageLayout()` pentru a livra machete personalizate împreună cu aplicația:
|
||||
|
||||
```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.CANVAS,
|
||||
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 de obicei `'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` (`CANVAS` pentru layout liber).
|
||||
* Fiecare `widget` dintr-o filă poate reda o componentă frontend, o listă de relații sau alte tipuri de widgeturi integrate.
|
||||
* `position` pe file le controlează ordinea. Folosiți valori mai mari (de ex., 50) pentru a plasa filele personalizate după cele integrate.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePageLayoutTab" description="Adaugă o filă la un layout de pagină existent">
|
||||
|
||||
`definePageLayoutTab` permite aplicației tale să atașeze o singură filă — cu widgeturi opționale — la un layout de pagină **existent**. Cel mai comun caz de utilizare este adăugarea unei file personalizate (de exemplu, o filă de analize sau o filă cu rezumat AI) la una dintre paginile de înregistrare predefinite ale Twenty sau la un layout de pagină pe care propria ta aplicație îl livrează deja.
|
||||
|
||||
Layoutul de pagină țintă trebuie să fie fie un layout de pagină Twenty **standard**, fie unul definit de **propria ta aplicație**; referințele între aplicații la layouturi de pagină deținute de o altă aplicație instalată nu sunt acceptate în prezent.
|
||||
|
||||
```ts src/page-layouts/example-extra-tab.ts
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-ab01-4001-8001-c0aba11c0100';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
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 **necesar** când folosești `definePageLayoutTab` și trebuie să indice către un layout de pagină care există deja la momentul instalării (standard sau al aplicației tale). Când lipsește layoutul de pagină părinte, instalarea eșuează cu o eroare clară de validare.
|
||||
* `widgets` sunt limitate doar la această filă — fac referire la componente front-end, 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` când deții întregul layout (de obicei un `RECORD_PAGE` pentru un obiect pe care îl livrezi în aplicația ta sau un `STANDALONE_PAGE`).
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -1,566 +0,0 @@
|
||||
---
|
||||
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 { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
|
||||
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: '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: 'GET',
|
||||
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 metodă HTTP **sub endpoint-ul `/s/`**:
|
||||
> de ex. `path: '/post-card/create'` este apelabil la `https://your-twenty-server.com/s/post-card/create`
|
||||
* **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.*`
|
||||
|
||||
<Note>
|
||||
Puteți, de asemenea, să executați manual o funcție folosind CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
|
||||
```
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
Puteți urmări jurnalele cu:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty 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`:
|
||||
|
||||
```ts
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
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>
|
||||
|
||||
#### 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ă.
|
||||
|
||||
```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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<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>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Definește o funcție logică post-instalare (una per aplicație)">
|
||||
|
||||
O funcție post-instalare este o funcție logică care rulează automat după instalarea aplicației într-un spațiu de lucru. Serverul o execută **după** ce metadatele aplicației au fost sincronizate și clientul SDK a fost generat, astfel încât spațiul de lucru este complet pregătit pentru utilizare, iar noua schemă este disponibilă. Cazuri tipice de utilizare includ popularea cu date implicite, crearea de înregistrări inițiale, configurarea setărilor spațiului de lucru sau provizionarea resurselor în cadrul serviciilor terților.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Puteți, de asemenea, să executați manual funcția post-instalare oricând folosind CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
Puncte cheie:
|
||||
* Funcțiile de post-instalare folosesc `definePostInstallLogicFunction()` — o variantă specializată care omite setările de declanșare (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
* Handlerul primește un `InstallPayload` cu `{ previousVersion?: string; newVersion: string }` — `newVersion` este versiunea care este instalată, iar `previousVersion` este versiunea instalată anterior (sau `undefined` la o instalare nouă). Folosiți aceste valori pentru a distinge instalările noi de actualizări și pentru a rula logică de migrare specifică versiunii.
|
||||
* **Când rulează hook-ul**: doar la instalări noi, în mod implicit. Transmiteți `shouldRunOnVersionUpgrade: true` dacă doriți să ruleze și atunci când aplicația este actualizată de la o versiune anterioară. Când este omis, indicatorul are implicit valoarea `false`, iar actualizările sar peste hook.
|
||||
* **Model de execuție — implicit asincron, sincron opțional**: indicatorul `shouldRunSynchronously` controlează *modul în care* este executat post-install.
|
||||
* `shouldRunSynchronously: false` *(implicit)* — hook-ul este **pus în coadă în message queue** cu `retryLimit: 3` și rulează asincron într-un worker. Răspunsul la instalare revine imediat ce jobul este pus în coadă, astfel încât un handler lent sau care eșuează nu blochează apelantul. Workerul va reîncerca de până la trei ori. **Folosiți acest mod pentru joburi de lungă durată** — popularea unor seturi mari de date, apelarea API-urilor lente ale terților, provizionarea resurselor externe, orice ar putea depăși o fereastră rezonabilă de răspuns HTTP.
|
||||
* `shouldRunSynchronously: true` — hook-ul este executat **inline în timpul fluxului de instalare** (același executor ca pre-install). Cererea de instalare blochează până când handlerul se termină, iar dacă acesta aruncă o eroare, apelantul instalării primește un `POST_INSTALL_ERROR`. Fără reîncercări automate. **Folosiți acest mod pentru sarcini rapide, care trebuie să se finalizeze înainte de răspuns** — de exemplu, emiterea unei erori de validare către utilizator sau o configurare rapidă de care clientul va depinde imediat după ce apelul de instalare revine. Reține că migrarea metadatelor a fost deja aplicată până când rulează post-install, astfel încât un eșec în modul sincron **nu** anulează modificările de schemă — doar expune eroarea.
|
||||
* Asigurați-vă că handlerul dvs. este idempotent. În modul asincron, coada poate reîncerca de până la trei ori; în oricare mod, hook-ul poate rula din nou la actualizări când `shouldRunOnVersionUpgrade: true`.
|
||||
* Variabilele de mediu `APPLICATION_ID`, `APP_ACCESS_TOKEN` și `API_URL` sunt disponibile în interiorul handlerului (la fel ca în orice altă funcție logică), astfel încât puteți apela API-ul Twenty cu un token de acces al aplicației limitat la aplicația dvs.
|
||||
* Este permisă o singură funcție de post-instalare per aplicație. Construirea manifestului va genera o eroare dacă este detectată mai mult de una.
|
||||
* `universalIdentifier`, `shouldRunOnVersionUpgrade` și `shouldRunSynchronously` ale funcției sunt atașate automat la manifestul aplicației în câmpul `postInstallLogicFunction` în timpul build-ului — nu este nevoie să le referi în `defineApplication()`.
|
||||
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de configurare mai lungi, cum ar fi popularea datelor.
|
||||
* **Nu se execută în modul dev**: când o aplicație este înregistrată local (prin `yarn twenty dev`), serverul sare complet peste fluxul de instalare și sincronizează fișierele direct prin watcher-ul CLI — astfel încât post-install nu rulează niciodată în modul dev, indiferent de `shouldRunSynchronously`. Folosiți `yarn twenty exec --postInstall` pentru a-l declanșa manual într-un workspace care rulează.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Definește o funcție logică de pre-instalare (una per aplicație)">
|
||||
|
||||
O funcție de pre-instalare este o funcție logică ce rulează automat în timpul instalării, **înainte ca migrarea metadatelor workspace-ului să fie aplicată**. Are aceeași structură a payload-ului ca post-install (`InstallPayload`), dar este plasată mai devreme în fluxul de instalare, astfel încât poate pregăti starea de care depinde migrarea iminentă — utilizări tipice includ realizarea unui backup al datelor, validarea compatibilității cu noua schemă sau arhivarea înregistrărilor care urmează să fie restructurate sau eliminate.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Puteți, de asemenea, să executați manual funcția de pre-instalare oricând folosind CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Puncte cheie:
|
||||
* Funcțiile de pre-instalare folosesc `definePreInstallLogicFunction()` — aceeași configurare specializată ca pentru post-install, doar că atașată la un alt punct din ciclul de viață.
|
||||
* Atât handlerele de pre-install, cât și cele de post-install primesc același tip `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importați-l o singură dată și reutilizați-l pentru ambele hook-uri.
|
||||
* **Când rulează hook-ul**: poziționat chiar înainte de migrarea metadatelor workspace-ului (`synchronizeFromManifest`). Înainte de execuție, serverul rulează un "sync redus", pur aditiv, care înregistrează funcția de pre-instalare a versiunii **noi** în metadatele workspace-ului — nimic altceva nu este atins — și apoi o execută. Deoarece acest sync este doar aditiv, obiectele, câmpurile și datele versiunii precedente sunt încă intacte când rulează handlerul dvs.: puteți citi și face backup în siguranță stării pre-migrare.
|
||||
* **Model de execuție**: pre-install este executat **sincron** și **blochează instalarea**. Dacă handlerul aruncă o eroare, instalarea este întreruptă înainte ca orice modificări de schemă să fie aplicate — 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ă.
|
||||
* La fel ca la post-install, este permisă o singură funcție de pre-instalare per aplicație. Este atașată automat la manifestul aplicației sub `preInstallLogicFunction` în timpul build-ului.
|
||||
* **Nu se execută în modul dev**: la fel ca post-install — fluxul de instalare este sărit complet pentru aplicațiile înregistrate local, astfel încât pre-install nu rulează niciodată sub `yarn twenty dev`. Folosiți `yarn twenty exec --preInstall` pentru a-l declanșa manual.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-install vs post-install: când să folosești fiecare" description="Alegerea hook-ului de instalare potrivit">
|
||||
|
||||
Ambele hook-uri fac parte din același flux de instalare și primesc același `InstallPayload`. Diferența constă în **momentul** în care rulează în raport cu migrarea metadatelor workspace-ului, iar asta schimbă ce date pot atinge în siguranță.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Pre-install este întotdeauna **sincron** (blochează instalarea și o poate întrerupe). Post-install este **implicit asincron** — pus în coadă pe un worker cu reîncercări automate — dar poate opta pentru execuție sincronă cu `shouldRunSynchronously: true`. Consultați acordeonul `definePostInstallLogicFunction` de mai sus pentru când să folosiți fiecare mod.
|
||||
|
||||
**Folosiți `post-install` pentru orice are nevoie ca noua schemă să existe.** Acesta este cazul obișnuit:
|
||||
|
||||
* Popularea datelor implicite (crearea înregistrărilor inițiale, a vizualizărilor implicite, a conținutului demo) pentru obiectele și câmpurile adăugate recent.
|
||||
* Înregistrarea webhook-urilor la servicii terțe, acum că aplicația are acreditările sale.
|
||||
* Apelarea propriului tău API pentru a finaliza configurarea care depinde de metadatele sincronizate.
|
||||
* Logică idempotentă de tipul "asigurați-vă că acest lucru există" care ar trebui să reconcilieze starea la fiecare actualizare — combină cu `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Exemplu — populează o înregistrare `PostCard` implicită după instalare:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Folosiți `pre-install` atunci când o migrare altfel ar distruge sau ar corupe datele existente.** Deoarece pre-install rulează pe schema *anterioară* și eșecul său anulează actualizarea, acesta este locul potrivit pentru orice este riscant:
|
||||
|
||||
* **Crearea unui backup al datelor care urmează să fie eliminate sau restructurate** — de exemplu, elimini un câmp în v2 și trebuie să-i copiezi valorile într-un alt câmp sau să le exporți în stocare înainte de rularea migrării.
|
||||
* **Arhivarea înregistrărilor pe care o nouă constrângere le-ar invalida** — de exemplu, un câmp devine `NOT NULL` și trebuie mai întâi să ștergi sau să corectezi rândurile cu valori nule.
|
||||
* **Validarea compatibilității și refuzarea actualizării dacă datele curente nu pot fi migrate fără probleme** — aruncă din handler și instalarea se oprește fără ca modificări să fie aplicate. Aceasta este mai sigur decât să descoperi incompatibilitatea în mijlocul migrării.
|
||||
* **Redenumirea sau schimbarea cheilor datelor** înaintea unei modificări de schemă care ar pierde asocierile.
|
||||
|
||||
Exemplu — arhivează înregistrări înainte de o migrare distructivă:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
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 = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
```
|
||||
|
||||
**Regulă practică:**
|
||||
|
||||
| Vrei să... | Folosiți |
|
||||
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| Populați date implicite, configurați workspace-ul, înregistrați resurse externe | `post-install` |
|
||||
| Rulați populări de durată sau apeluri către terți care nu ar trebui să blocheze răspunsul la instalare | `post-install` (implicit — `shouldRunSynchronously: false`, cu reîncercări ale workerului) |
|
||||
| Rulați o configurare rapidă de care apelantul va depinde imediat după ce apelul de instalare revine | `post-install` cu `shouldRunSynchronously: true` |
|
||||
| Citești sau faci backup datelor pe care migrarea iminentă le-ar pierde | `pre-install` |
|
||||
| Respingeți o actualizare care ar corupe datele existente | `pre-install` (aruncă din handler) |
|
||||
| Rulați o reconciliere la fiecare actualizare | `post-install` cu `shouldRunOnVersionUpgrade: true` |
|
||||
| Faceți o configurare unică doar la prima instalare | `post-install` cu `shouldRunOnVersionUpgrade: false` (implicit) |
|
||||
|
||||
<Note>
|
||||
Dacă aveți dubii, alegeț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ă.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 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 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 a tipurilor complete, 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 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.
|
||||
|
||||
```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` | `string` | Tipul MIME (implicit `application/octet-stream` dacă este omis) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | `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ă, limitată la rolul implicit de funcție al aplicației
|
||||
|
||||
Nu trebuie să le transmiteți clienților — aceștia citesc automat din `process.env`. Permisiunile cheii API sunt determinate de rolul referențiat în `defaultRoleUniversalIdentifier` din `application-config.ts`.
|
||||
</Note>
|
||||
@@ -1,295 +0,0 @@
|
||||
---
|
||||
title: Publicare
|
||||
icon: încarcă
|
||||
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/building), ai două căi pentru distribuire:
|
||||
|
||||
* **Implementați o arhivă tar** — încărcați 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 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 deploy.
|
||||
|
||||
## 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 --api-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 deploy
|
||||
# To deploy to a specific remote:
|
||||
# yarn twenty deploy --remote production
|
||||
```
|
||||
|
||||
### Partajarea unei aplicații implementate
|
||||
|
||||
<Warning>
|
||||
Partajarea aplicațiilor private (tarball) între spații de lucru este o funcționalitate **Enterprise**. Fila **Distribution** va afișa un mesaj de actualizare în locul controalelor de partajare până când spațiul tău de lucru are o cheie Enterprise validă. Mergi la [Setări > Panou de administrare > Enterprise](/settings/admin-panel#enterprise) pentru a o activa.
|
||||
</Warning>
|
||||
|
||||
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. După ce spațiul tău de lucru este pe planul Enterprise, poți partaja o aplicație implementată astfel:
|
||||
|
||||
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. Incrementați 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 deploy` (sau `yarn twenty deploy --remote production`)
|
||||
3. Spațiile de lucru care au aplicația instalată 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 două fluxuri de lucru GitHub Actions, în `.github/workflows/`. Acestea sunt gata să ruleze imediat ce faci push al repozitoriului pe GitHub — nu este necesară nicio configurare suplimentară pentru CI, iar CD necesită doar un singur secret.
|
||||
|
||||
### 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 server 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 deploy`.
|
||||
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.
|
||||
|
||||
### Fixarea acțiunilor reutilizabile
|
||||
|
||||
Ambele fluxuri de lucru 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 `logoUrl` și `screenshots` 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',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
logoUrl: 'public/logo.png',
|
||||
screenshots: [
|
||||
'public/screenshot-1.png',
|
||||
'public/screenshot-2.png',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Vezi [acordeonul defineApplication](/l/ro/developers/extend/apps/building#defineentity-functions) din pagina Building Apps pentru lista completă de câmpuri ale marketplace-ului (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
|
||||
|
||||
#### Dimensiuni recomandate pentru capturi de ecran
|
||||
|
||||
marketplace-ul redă `screenshots` într-un container fix cu raport `8:5` (de exemplu, `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
Capturile de ecran cu orice raport de aspect sunt afișate integral și nu sunt niciodată decupate, însă orice este semnificativ mai înalt sau mai îngust decât `8:5` va afișa benzi goale pe laterale.
|
||||
</Note>
|
||||
|
||||
### Publicare
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty publish
|
||||
```
|
||||
|
||||
Pentru a publica sub un dist-tag specific (de ex., `beta` sau `next`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty 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 server catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty server catalog-sync --remote production
|
||||
```
|
||||
|
||||
Metadatele afișate în marketplace provin din configurația `defineApplication()` — câmpuri precum `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` și `termsUrl`.
|
||||
|
||||
<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
|
||||
|
||||
Folosește acest workflow GitHub Actions pentru a publica automat la fiecare release (folosește [OIDC](https://docs.npmjs.com/trusted-publishers)):
|
||||
|
||||
```yaml filename=".github/workflows/publish.yml"
|
||||
name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: yarn install --immutable
|
||||
- run: npx twenty build
|
||||
- run: npm publish --provenance --access public
|
||||
working-directory: .twenty/output
|
||||
```
|
||||
|
||||
Pentru alte sisteme CI (GitLab CI, CircleCI etc.), se aplică aceleași trei comenzi: `yarn install`, `yarn twenty build`, apoi `npm publish` din `.twenty/output`.
|
||||
|
||||
<Note>
|
||||
**npm provenance** este opțională, dar recomandată. Publicarea cu `--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. Vezi [documentația npm provenance](https://docs.npmjs.com/generating-provenance-statements) pentru instrucțiuni de configurare.
|
||||
</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 install
|
||||
```
|
||||
|
||||
<Note>
|
||||
Serverul impune versionarea semver la instalare, reflectând regulile de la deploy:
|
||||
|
||||
* Instalarea aceleiași versiuni care este deja instalată în workspace-ul tău 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 install`.
|
||||
</Note>
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
title: Abilități și agenți
|
||||
description: Definiți abilități și agenți AI pentru aplicația dvs.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Aptitudinile și agenții sunt în prezent în testare 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 suplimentar despre scopul agentului.
|
||||
* `icon` (opțional) setează pictograma afișată în UI.
|
||||
* `modelId` (opțional) suprascrie modelul AI implicit utilizat de agent.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
title: Aplicații Twenty
|
||||
description: Construiți și gestionați personalizările Twenty sub formă de cod.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Aplicațiile sunt în prezent în testare alfa. Caracteristica funcționează, dar este încă în dezvoltare.
|
||||
</Warning>
|
||||
|
||||
## Ce sunt aplicațiile?
|
||||
|
||||
Aplicațiile vă permit să extindeți Twenty cu obiecte personalizate, câmpuri, funcții logice, componente front-end, abilități IA și altele — toate gestionate ca cod. În loc să configurați totul prin interfața de utilizator (UI), vă definiți modelul de date și logica în TypeScript și le implementați în unul sau mai multe spații de lucru.
|
||||
|
||||
**Ce puteți construi:**
|
||||
|
||||
* **Obiecte și câmpuri personalizate** — extindeți modelul de date cu entități noi sau adăugați câmpuri la obiecte existente, precum Companie sau Persoană
|
||||
* **Funcții logice** — funcții pe partea de server declanșate de evenimente ale bazei de date, programări cron sau rute HTTP
|
||||
* **Componente front-end** — componente React care se afișează în interfața Twenty (pagini de înregistrări, meniul de comenzi, panouri laterale)
|
||||
* **Abilități și agenți IA** — extindeți IA din Twenty cu capabilități personalizate
|
||||
* **Vizualizări și navigare** — vizualizări salvate preconfigurate și linkuri în bara laterală
|
||||
|
||||
## Pornire rapidă
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Aceasta creează scheletul unei aplicații noi, pornește opțional un server Twenty local și începe să monitorizeze fișierele pentru modificări. Consultați ghidul [Începeți](/l/ro/developers/extend/apps/getting-started) pentru prezentarea completă.
|
||||
|
||||
## Ghiduri detaliate
|
||||
|
||||
| Ghid | Descriere |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Începeți](/l/ro/developers/extend/apps/getting-started) | Creați scheletul unei aplicații, configurați un server local, structură de proiect, CI |
|
||||
| [Crearea aplicațiilor](/l/ro/developers/extend/apps/building) | Definiții ale entităților (`defineObject`, `defineLogicFunction`, `defineFrontComponent` etc.), clienți API, pachete npm, resurse publice, testare |
|
||||
| [Publicare](/l/ro/developers/extend/apps/publishing) | Implementare pe un server, publicare pe npm, marketplace |
|
||||
|
||||
## Concepte cheie
|
||||
|
||||
### Detectarea entităților
|
||||
|
||||
SDK-ul detectează entitățile scanând fișierele TypeScript pentru apeluri `export default define<Entity>({...})`. Denumirea fișierelor și structura folderelor sunt flexibile — detectarea este bazată pe AST, nu pe căi.
|
||||
|
||||
### Tipuri de entități disponibile
|
||||
|
||||
| Funcție | Scop |
|
||||
| ---------------------------------- | -------------------------------------------------------- |
|
||||
| `defineApplication()` | Metadate ale aplicației (obligatoriu, una per aplicație) |
|
||||
| `defineObject()` | Obiecte personalizate cu câmpuri |
|
||||
| `defineField()` | Câmpuri pe obiecte existente |
|
||||
| `defineLogicFunction()` | Logică pe partea de server cu declanșatoare |
|
||||
| `defineFrontComponent()` | Componente React în interfața Twenty |
|
||||
| `defineRole()` | Roluri de permisiuni |
|
||||
| `defineView()` | Configurații pentru vizualizări salvate |
|
||||
| `defineNavigationMenuItem()` | Linkuri de navigare în bara laterală |
|
||||
| `defineSkill()` | Abilități ale agentului IA |
|
||||
| `defineAgent()` | Agenți IA cu prompturi |
|
||||
| `definePageLayout()` | Dispuneri personalizate pentru paginile de înregistrare |
|
||||
| `definePreInstallLogicFunction()` | Rulează înainte de instalarea aplicației |
|
||||
| `definePostInstallLogicFunction()` | Rulează după instalarea aplicației |
|
||||
|
||||
### Flux de lucru pentru dezvoltare
|
||||
|
||||
1. **`yarn twenty dev`** — monitorizează fișierele sursă, reconstruiește la modificări, sincronizează cu serverul, generează clienți API tipizați
|
||||
2. **`yarn twenty build`** — produce o versiune distribuibilă
|
||||
3. **`yarn twenty deploy`** — implementează pe un server Twenty la distanță
|
||||
4. **`yarn twenty add`** — generează interactiv o entitate nouă
|
||||
|
||||
### Referință CLI
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty help # Listează toate comenzile
|
||||
yarn twenty server start # Pornește serverul local de dezvoltare
|
||||
yarn twenty remote add # Conectează-te la un server Twenty
|
||||
yarn twenty exec -n fn # Execută o funcție logică
|
||||
yarn twenty logs -n fn # Transmite în flux jurnalele funcției
|
||||
```
|
||||
|
||||
Consultați ghidul [Începeți](/l/ro/developers/extend/apps/getting-started) pentru referința completă CLI.
|
||||
Reference in New Issue
Block a user