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
850 lines
51 KiB
Plaintext
850 lines
51 KiB
Plaintext
---
|
||
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`.
|