9a1a057d8f
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23555?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
772 lines
41 KiB
Plaintext
772 lines
41 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`.
|
||
|
||
| 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ă |
|
||
| `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 aplicației dvs. |
|
||
|
||
### `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 |
|
||
|
||
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,
|
||
});
|
||
```
|
||
|
||
### 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.
|
||
|
||
## 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 dezvoltare activă. Redarea, stilizarea și gestionarea evenimentelor funcționează bine. Orice ajunge *dincolo de* redare (măsurarea unui element, apelarea unei metode DOM pe un ref, portarea în afara arborelui tău, accesarea stocării browserului) lipsește sau este incompletă astăzi, iar majoritatea eșuează în mod silențios: nu există excepție și nici eroare TypeScript, deoarece scheletul este tastat pentru întregul DOM al browserului.
|
||
|
||
Dacă una dintre acestea te blochează, [deschide un tichet](https://github.com/twentyhq/twenty/issues/new/choose) pentru a fi prioritizată.
|
||
|
||
### Layout și măsurare
|
||
|
||
Nimic nu se poate măsura singur încă.
|
||
|
||
| API | Ce se întâmplă |
|
||
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||
| `getBoundingClientRect()`, `getClientRects()` | Aruncă o excepție |
|
||
| `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Este în mod silențios `undefined`, astfel încât `width ?? 0` produce `0`, iar `width > 600` este întotdeauna fals |
|
||
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (gardurile `typeof` funcționează) |
|
||
| `window.matchMedia()`, `window.getComputedStyle()` | Aruncă o excepție |
|
||
| `window.innerWidth`, `innerHeight`, `devicePixelRatio` | Este în mod silențios `undefined` |
|
||
| `new MutationObserver(fn)` | Se construiește, apoi `.observe()` aruncă o excepție |
|
||
|
||
Prin urmare, `ResponsiveContainer` din recharts, Floating UI / Popper, virtualizarea listelor și redimensionarea prin tragere nu funcționează încă. Fă layout-ul în CSS în schimb: foaia ta de stil ajunge la pagina reală, astfel încât flexbox, grid, `aspect-ratio`, `clamp()` și `@container` se comportă toate normal.
|
||
|
||
<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 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 manual șirul `className` |
|
||
| `document.getElementById()`, `getElementsByClassName()`, `createTreeWalker()` | Aruncă o excepție | `querySelector()` / `querySelectorAll()`, care funcționează |
|
||
| `document.activeElement` | Întotdeauna `undefined` | Urmărește focusul cu `onFocus` / `onBlur` |
|
||
| `\<canvas>` | Nu redă nimic, fără eroare | SVG sau desenează în offscreen și afișează un `<img src={dataUrl}>` |
|
||
| `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, keyboard, focus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` and `animationend`/`transitionend` cross to the host, plus a few per element: `load`/`error` on `<img>`, clipboard and composition on `<input>`/`\<textarea>`, media on `\<video>`/`\<audio>`, `toggle` on `\<details>`/`\<dialog>`. Anything else (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, pointer capture, `onLoad` off `<img>`) is dropped without warning.
|
||
|
||
`document.addEventListener()` and `window.addEventListener()` register without error and never fire, which is why a drag stops as soon as the pointer leaves the element it started on. `event.preventDefault()` does not cross either; form submission, `dragover`/`drop` and link clicks are already guarded for you.
|
||
|
||
### Attributes and styling
|
||
|
||
Each element forwards its own properties to the host DOM (`href` on `\<a>`, `src`/`alt` on `<img>`, `value`/`placeholder`/`disabled` on `<input>`, and so on), plus a common set on every element: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` and any `aria-*` / `data-*` attribute (hyphenated, so `ariaLabel` is dropped). Anything outside that is silently discarded, so express custom state as `data-*`.
|
||
|
||
Component CSS, whether from `import './styles.css'`, CSS-in-JS or a `\<style>` element, is injected into the host page's `\<head>` **unscoped**. So class names collide with Twenty's own (prefix them, and never write bare `div { ... }` selectors), and `@media` matches the browser window rather than your widget (use `@container` with your own `container-type`). Inline `style` props are unaffected.
|
||
|
||
### Storage and network
|
||
|
||
`localStorage`, `sessionStorage`, IndexedDB, cookies, the Cache API and `BroadcastChannel` are all unavailable, since the component runs in a worker at an opaque origin. To persist state, call a [logic function](/l/ro/developers/extend/apps/logic/logic-functions) and use its [key-value store](/l/ro/developers/extend/apps/logic/key-value-store).
|
||
|
||
`fetch` works, with caveats:
|
||
|
||
* Calls to the Twenty API and your app's routes are proxied by the host, so prefer [`RestApiClient`](#calling-the-twenty-rest-api). On proxied calls, `AbortSignal` and the other `RequestInit` options are dropped, and only `string` and `URLSearchParams` bodies are supported.
|
||
* Other origins leave the sandbox with `Origin: null`, so a third-party API answers only if it sends `Access-Control-Allow-Origin: *`. Call it from a logic function instead.
|
||
* `fetch('/rest/people')` is never matched to the Twenty API, because the sandbox has no page URL to resolve a relative path against.
|
||
|
||
### Other gaps
|
||
|
||
* **File contents.** `<input type="file">` gives your handler file metadata only, not the bytes, so `FileReader` and uploads are not possible yet.
|
||
* **Drag-and-drop payloads.** Drag events fire, but `event.dataTransfer` is `undefined`.
|
||
* **Node built-ins.** `fs`, `path` and `node:crypto` fail the build, so move that work into a [logic function](/l/ro/developers/extend/apps/logic/logic-functions). Web Crypto, `fetch`, `TextEncoder` and `URL` are available.
|
||
* **`\<iframe>`** is always re-sandboxed without `allow-same-origin`, so an embed relying on its own session renders logged out. It has no `onLoad` either.
|