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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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