Files
twenty/packages/twenty-docs/l/it/developers/extend/apps/layout/front-components.mdx
T
github-actions[bot] cdeebb1a18 i18n - docs translations (#23559)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-30 13:09:47 +02:00

772 lines
41 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Componenti front-end
description: Crea componenti React che vengono renderizzati all'interno della UI di Twenty con isolamento in sandbox.
icon: window-maximize
---
I componenti front-end sono componenti React che vengono renderizzati direttamente all'interno della UI di Twenty. Vengono eseguiti in un **Web Worker isolato** utilizzando Remote DOM — il tuo codice viene eseguito all'interno di un iframe con origine opaca e in sandbox, ma la sua interfaccia utente continua a essere renderizzata in modo nativo nella pagina invece di essere confinata in quell'iframe.
<Warning>
I componenti Front sono ancora in fase di sviluppo attivo. Il tuo codice viene eseguito su un DOM parziale, non su una vera pagina del browser, quindi gli utilizzi avanzati possono fallire, spesso in modo silenzioso. Vedi [Limitazioni attuali](#current-limitations).
</Warning>
## Dove possono essere utilizzati i componenti front-end
I componenti front-end possono essere renderizzati in tre posizioni all'interno di Twenty:
* **Pannello laterale** — I componenti front-end non headless si aprono nel pannello laterale destro. Questo è il comportamento predefinito quando un componente front-end viene avviato dal menu comandi.
* **Widget (dashboard e pagine dei record)** — I componenti front possono essere incorporati come widget all'interno dei [layout di pagina](/l/it/developers/extend/apps/layout/page-layouts). Quando si configura una dashboard o il layout di una pagina record, gli utenti possono aggiungere un widget del componente front-end.
* **Impostazioni dell'app** — Definito con [`defineSettingsFrontComponent()`](#custom-settings-component), il componente front-end viene renderizzato come una sezione all'interno della scheda **Settings** dell'app, al posto dell'interfaccia utente predefinita per la configurazione delle variabili.
Un componente front da solo non è raggiungibile dall'interfaccia utente: devi renderlo visibile. I tre modi per farlo sono:
* **Associarlo a un [elemento di menu dei comandi](/l/it/developers/extend/apps/layout/command-menu-items)** — lo registra nel menu dei comandi (Cmd+K) e, facoltativamente, come azione rapida fissata.
* **Incorporarlo come widget in un [layout di pagina](/l/it/developers/extend/apps/layout/page-layouts)** — lo posiziona nella pagina dei dettagli di un record o in una dashboard.
* **Definirlo con [`defineSettingsFrontComponent()`](#custom-settings-component)** — lo renderizza come una sezione all'interno della scheda **Settings** dell'app, al posto dell'interfaccia utente predefinita per la configurazione delle variabili.
## Esempio di base
Il modo più rapido per vedere un componente front in azione è associarlo a [`defineCommandMenuItem`](/l/it/developers/extend/apps/layout/command-menu-items), così appare come pulsante di azione rapida nellangolo in alto a destra della pagina:
```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',
});
```
Dopo la sincronizzazione con `yarn twenty dev` (o eseguendo una volta sola `yarn twenty apply`), l'azione rapida appare nell'angolo in alto a destra della pagina:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Pulsante di azione rapida nell'angolo in alto a destra" />
</div>
Fai clic per renderizzare il componente in linea.
## Campi di configurazione
| Campo | Obbligatorio | Descrizione |
| --------------------- | ------------ | ---------------------------------------------------------------------- |
| `universalIdentifier` | Sì | ID univoco stabile per questo componente |
| `component` | Sì | Una funzione di componente React |
| `name` | No | Nome visualizzato |
| `description` | No | Descrizione di ciò che fa il componente |
| `isHeadless` | No | Imposta su `true` se il componente non ha una UI visibile (vedi sotto) |
## Posizionare un componente front-end su una pagina
Oltre ai comandi, puoi incorporare un componente front-end direttamente in una pagina record aggiungendolo come widget in un **layout di pagina**. Vedi [Layout di pagina](/l/it/developers/extend/apps/layout/page-layouts) per i dettagli.
## Componente delle impostazioni personalizzato
Per sostituire l'interfaccia utente di configurazione delle variabili generata automaticamente nella scheda **Settings** della tua app con il tuo componente, definiscilo con `defineSettingsFrontComponent` invece di `defineFrontComponent`. Utilizza gli stessi [campi di configurazione](#configuration-fields) (tranne `isHeadless`, che non è accettato poiché un componente delle impostazioni renderizza sempre un'interfaccia utente visibile) e inoltre contrassegna il componente come l'interfaccia delle impostazioni dell'app.
Il componente viene renderizzato come una sezione **all'interno** della scheda Settings, non come una sostituzione dell'intera scheda. Le sezioni gestite dal sistema di Twenty — aggiornamento automatico, URL dell'app e connessioni — vengono sempre renderizzate sopra di essa e non possono essere sovrascritte dall'app.
```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,
});
```
È consentito un solo componente di front-end delle impostazioni per app; dichiararne più di uno fa fallire la build. Quando presente, la scheda **Settings** dell'app renderizza questo componente al posto dell'interfaccia utente di configurazione delle variabili predefinita.
## Headless vs non headless
I componenti front-end prevedono due modalità di rendering controllate dall'opzione `isHeadless`:
**Non headless (predefinito)** — Il componente renderizza un'interfaccia utente visibile. Quando viene avviato dal menu comandi, si apre nel pannello laterale. Questo è il comportamento predefinito quando `isHeadless` è `false` o omesso.
**Headless (`isHeadless: true`)** — Il componente viene montato in modo invisibile in background. Non apre il pannello laterale. I componenti headless sono pensati per azioni che eseguono una logica e poi si smontano — ad esempio, eseguire un'attività asincrona, navigare a una pagina o mostrare una finestra modale di conferma. Si abbinano naturalmente ai componenti Command dell'SDK descritti di seguito.
```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,
});
```
Poiché il componente restituisce `null`, Twenty evita di renderizzare un contenitore per esso — non appare alcuno spazio vuoto nel layout. Il componente ha comunque accesso a tutti gli hook e all'API di comunicazione con l'host.
## Componenti Command dell'SDK
Il pacchetto `twenty-sdk` fornisce quattro componenti di supporto Command progettati per i componenti front-end headless. Ogni componente esegue un'azione al montaggio, gestisce gli errori mostrando una notifica snackbar e smonta automaticamente il componente front-end al termine.
Importali da `twenty-sdk/front-component`:
* **`Command`** — Esegue una callback asincrona tramite la prop `execute`.
* **`CommandLink`** — Naviga verso un percorso dell'app. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Apre una finestra modale di conferma. Se l'utente conferma, esegue la callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Apre una pagina del pannello laterale. Le props dipendono da `page` — ad esempio `ViewRecord` accetta `recordId` + `objectNameSingular` (più un id `tab` opzionale per aprire il record in una scheda specifica), altre pagine accettano `pageTitle` + `pageIcon`.
Ecco un esempio completo di componente front-end headless che usa `Command` per eseguire un'azione dal menu comandi:
```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',
});
```
E un esempio che usa `CommandModal` per chiedere conferma prima di eseguire:
```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,
});
```
Ed ecco un esempio che usa `CommandOpenSidePanelPage` per aprire il record corrente nel pannello laterale in una scheda specifica. `tab` è un id di scheda di layout della pagina (i layout predefiniti usano id come `company-tab-emails` o `company-tab-timeline`; i layout personalizzati usano l'id della scheda stessa). Se l'id non esiste nel layout del record, viene invece aperta la scheda predefinita:
```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,
});
```
## Chiamare una funzione logica
I componenti front vengono eseguiti lato browser in un Web Worker in sandbox all'interno di un iframe con origine opaca, mentre le [funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions) vengono eseguite lato server. Non esiste una chiamata diretta in-process tra i due; invece, un front component chiama una funzione logica tramite HTTP.
Una funzione logica dichiarata con `httpRouteTriggerSettings` è raggiungibile tramite HTTP al relativo percorso della route. `RestApiClient` tratta i percorsi che iniziano con `/s/` come route dellapp, li risolve nellURL da cui vengono servite le tue funzioni e li autentica con `TWENTY_APP_ACCESS_TOKEN`.
> **Su Twenty Cloud, le funzioni logiche attivate tramite HTTP sono servite su un dominio dedicato per ogni workspace** in `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Per i chiamanti esterni, copia lURL esatto dalle impostazioni del **trigger HTTP** della funzione o dalla scheda **Settings** dellapplicazione.
Un front component headless può eseguire la chiamata al mount tramite il componente `Command`, quindi smontarsi automaticamente:
```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,
});
```
Il percorso passato a `RestApiClient` è la proprietà `httpRouteTriggerSettings.path` della funzione di logica, con prefisso `/s`. Mantieni `isAuthRequired: true`; il `TWENTY_APP_ACCESS_TOKEN` che Twenty genera per il tuo componente autentica la richiesta:
```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` viene inserito automaticamente — vedi [Variabili dellapplicazione](#application-variables). Poiché le variabili di applicazione segrete non vengono mai esposte ai front component, mantieni le chiavi API e altra logica sensibile allinterno della funzione logica, non nel front component.
</Note>
### Chiamare le Twenty REST API
Per chiamare le route HTTP dellapp o leggere e scrivere record di Twenty da un front component, usa `RestApiClient` da `twenty-client-sdk/rest`. Invia i percorsi `/s/...` alla base URL delle funzioni del tuo workspace e tutti gli altri percorsi, inclusi `/rest/...`, a `TWENTY_API_URL`.
| Metodo | Descrizione |
| --------------------------------- | --------------------------------------------------------------------------------- |
| `get(path, options?)` | Invia una richiesta `GET` |
| `post(path, body?, options?)` | Invia una richiesta `POST` |
| `put(path, body?, options?)` | Invia una richiesta `PUT` |
| `patch(path, body?, options?)` | Invia una richiesta `PATCH` |
| `delete(path, options?)` | Invia una richiesta `DELETE` |
| `request(method, path, options?)` | Richiesta generica con qualsiasi metodo HTTP |
| `resolveUrl(path, options?)` | Risolve un percorso nel suo URL completo senza inviare una richiesta (per i link) |
`options` accetta `headers`, `query` (un record di parametri della query string; i valori nullish vengono ignorati) e un `AbortSignal` tramite `signal`. Un oggetto `body` non-`FormData` viene serializzato automaticamente in JSON. In caso di `401`, il client aggiorna il token di accesso una volta tramite l'host e ritenta la richiesta.
L'URL di base e il token vengono risolti dall'ambiente per impostazione predefinita. Passa gli override al costruttore quando necessario — per esempio nei test:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
Le richieste non riuscite generano un `RestApiClientError` che espone `status`, `statusText`, `url` e il `body` analizzato:
```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);
}
}
```
## Accesso al contesto di runtime
All'interno del tuo componente, usa gli hook dell'SDK per accedere all'utente corrente, al record e all'istanza del componente:
```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 disponibili:
| Hook | Restituisce | Descrizione |
| --------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
| `useUserId()` | `string` o `null` | L'ID dell'utente corrente |
| `useSelectedRecordIds()` | `string[]` | Tutti gli ID dei record selezionati (array vuoto se nessuno è selezionato) |
| `useRecordId()` | `string` o `null` | **Deprecato.** Usa `useSelectedRecordIds()` al suo posto |
| `useFrontComponentId()` | `string` | L'ID di questa istanza di componente |
| `useColorScheme()` | `'light'` o `'dark'` | Lo schema di colori attivo dell'interfaccia utente dell'host (`System` è già stato risolto) |
| `useFrontComponentExecutionContext(selector)` | varia | Accedi all'intero contesto di esecuzione con una funzione selettore |
## Variabili dell'applicazione
Le variabili dell'applicazione definite in [`defineApplication()`](/l/it/developers/extend/apps/config/application) con `isSecret: false` sono disponibili all'interno dei componenti front-end tramite l'utility `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>
Le variabili segrete (`isSecret: true`) **non** sono esposte ai componenti front-end. Sono disponibili solo nelle [funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions), che vengono eseguite lato server. Questo impedisce che valori sensibili come le chiavi API vengano inviati al browser.
</Warning>
`getApplicationVariable` restituisce sempre una **stringa** (o `undefined`), indipendentemente dal `type` dichiarato della variabile. La stringa viene serializzata in modo coerente in base al tipo (booleani come `"true"` / `"false"`, numeri come stringhe decimali, array / oggetti come JSON), lo stesso formato usato per la logic-function `process.env` — esegui il parsing manualmente (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Vedi [Tipi di variabili](/l/it/developers/extend/apps/config/application#variable-types).
Le seguenti variabili di sistema sono sempre disponibili tramite `process.env`:
| Variabile | Descrizione |
| ------------------------- | ---------------------------------------------------------------- |
| `TWENTY_API_URL` | URL di base dell'API Core di Twenty |
| `TWENTY_APP_ACCESS_TOKEN` | Token di breve durata con ambito limitato al ruolo della tua app |
### `TWENTY_FUNCTIONS_URL`
Twenty inserisce anche `TWENTY_FUNCTIONS_URL` nei front component e nelle funzioni di logica: la base URL da cui vengono servite le funzioni di logica attivate tramite HTTP della tua app.
Esiste perché quellURL non coincide sempre con il server di Twenty stesso. Su Twenty Cloud, le route dellapp sono servite su un dominio dedicato per ogni workspace (`https://\<your-workspace-subdomain>.withtwenty.com`, oppure il dominio pubblico primario dellapplicazione quando è configurato) in modo che le risposte create dallapp vengano eseguite in unorigine isolata invece che nellorigine dellapp Twenty. Le istanze self-hosted e locali servono le route dellapp con il prefisso `/s` direttamente sul server e potrebbero non impostare affatto la variabile. Dato che la base URL varia per workspace e per istanza, il tuo codice non può codificarla in modo statico — il server inserisce il valore corretto a runtime.
Raramente devi leggerla direttamente. Chiama le tue route tramite `RestApiClient` con un percorso con prefisso `/s/` e il client risolve lURL per te: rimuove il prefisso `/s` e punta a `TWENTY_FUNCTIONS_URL`, usando `\<TWENTY_API_URL>/s` come fallback quando la variabile non è impostata. Usa `resolveUrl('/s/\<path>')` per ottenere lURL assoluto senza inviare una richiesta, ad esempio per un link. Leggi direttamente la variabile solo quando costruisci manualmente un URL:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## API di comunicazione con l'host
I componenti front-end possono attivare navigazione, modali e notifiche utilizzando funzioni da `twenty-sdk`:
| Funzione | Descrizione |
| ----------------------------------------------- | ------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | Naviga a una pagina dell'app |
| `openSidePanelPage(params)` | Apri un pannello laterale |
| `closeSidePanel()` | Chiudi il pannello laterale |
| `openCommandConfirmationModal(params)` | Mostra una finestra di conferma |
| `enqueueSnackbar(params)` | Mostra una notifica toast |
| `unmountFrontComponent()` | Smonta il componente |
| `updateProgress(progress)` | Aggiorna un indicatore di avanzamento |
Ecco un esempio che usa l'API host per mostrare una snackbar e chiudere il pannello laterale dopo il completamento di un'azione:
```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,
});
```
### Lavorare con più record
Usa `useSelectedRecordIds()` per gestire più record selezionati. Questo è utile per operazioni in blocco:
```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,
});
```
Mostralo con una [voce del menu dei comandi](/l/it/developers/extend/apps/layout/command-menu-items) limitata alle selezioni di record:
```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',
});
```
## Asset pubblici
I componenti front-end possono accedere ai file dalla directory `public/` dell'app utilizzando `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,
});
```
Vedi la [sezione sugli asset pubblici](/l/it/developers/extend/apps/config/public-assets) per i dettagli.
## Stile
I componenti front-end supportano diversi approcci di styling. Puoi usare:
* **Stili inline** — `style={{ color: 'red' }}`
* **Componenti di Twenty UI** — la libreria di componenti di Twenty; vedi [Uso dei componenti di Twenty UI](#using-twenty-ui-components) di seguito
* **Emotion** — CSS-in-JS con `@emotion/react`
* **Styled-components** — pattern `styled.div`
* **Tailwind CSS** — classi di utilità
* **Qualsiasi libreria CSS-in-JS** compatibile con React
## Uso dei componenti di Twenty UI
Twenty distribuisce la sua libreria di componenti come pacchetto [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). I componenti front-end possono usarlo per pulsanti, tag, pillole di stato, chip, avatar, icone, tipografia e token del tema che si adattano automaticamente al tema chiaro e scuro dellarea di lavoro.
### Installazione
Aggiungi il pacchetto alla tua app, bloccato alla versione con cui la tua istanza di Twenty viene distribuita:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` è incluso nel tuo componente front-end in fase di build, quindi deve essere solo una dipendenza della tua app: non c’è nulla da configurare a runtime.
### Importare i componenti
Importa dal sottopercorso corrispondente invece che dalla root del pacchetto, in modo che solo i componenti che usi finiscano nel tuo bundle:
| Sottopercorso | Cosa esporta |
| --------------------------- | ------------------------------------------------ |
| `twenty-ui/input` | `Button` e campi di input del form |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` e altro |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` e altro |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` e altro |
| `twenty-ui/icon` | Componenti `Icon*` (ad es. `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,
});
```
### Icone
Importa le singole icone da `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Ogni icona nominata è sottoposta a tree-shaking, quindi importarne alcune aggiunge poco al tuo bundle. Evita `IconsProvider`, `useIcons` e `iconsState`: includono lintero set di icone Tabler (diversi MB).
### Temi e token del tema
I componenti Twenty UI si adattano automaticamente al tema chiaro e scuro dellarea di lavoro: il renderer applica lo schema di colori attivo sullhost e i componenti risolvono i loro colori rispetto ad esso.
Per usare gli stessi design token nei tuoi stili inline, chiama lhook `useTheme()`. Restituisce i token del tema di Twenty (spaziatura, colori, raggi, font) collegati al tema attivo, senza bisogno di configurare `ThemeProvider` nel tuo componente:
```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>
);
};
```
Poiché `useTheme()` è un hook, leggi i token allinterno del corpo del componente, così i valori riflettono sempre il tema attivo in tempo reale. La stessa mappa di token è anche esportata come costante `themeCssVariables`, ma nei componenti front-end è preferibile usare `useTheme()`: una costante a livello di modulo che dereferenzia `themeCssVariables` può essere undefined mentre il manifest dellapp viene estratto.
Per diramare esplicitamente in base allo schema attivo, leggilo con `useColorScheme()` da `twenty-sdk/front-component`, che restituisce `'light'` o `'dark'`.
## Limitazioni attuali
I componenti Front sono in fase di sviluppo attivo. Il rendering, lo styling e la gestione degli eventi funzionano bene. Qualsiasi cosa vada *oltre* il rendering (misurare un elemento, chiamare un metodo del DOM su una ref, creare un portale fuori dal tuo tree, accedere allo storage del browser) oggi è assente o incompleta, e per lo più fallisce in modo silenzioso: nessuna eccezione e nessun errore TypeScript, dato che limpalcatura è tipizzata rispetto al DOM completo del browser.
Se una di queste limitazioni ti blocca, [apri una issue](https://github.com/twentyhq/twenty/issues/new/choose) così la sua priorità aumenta.
### Layout e misurazione
Per ora nulla può ancora misurare se stesso.
| API | Cosa succede |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `getClientRects()` | Genera uneccezione |
| `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Silenziosamente `undefined`, quindi `width ?? 0` restituisce `0` e `width > 600` è sempre falso |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (le verifiche con `typeof` funzionano) |
| `window.matchMedia()`, `window.getComputedStyle()` | Genera uneccezione |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio` | Silenziosamente `undefined` |
| `new MutationObserver(fn)` | Viene costruito, poi `.observe()` genera uneccezione |
Quindi recharts `ResponsiveContainer`, Floating UI / Popper, la virtualizzazione delle liste e il ridimensionamento tramite trascinamento non funzionano ancora. Esegui invece il layout in CSS: il tuo stylesheet raggiunge la pagina reale, quindi flexbox, grid, `aspect-ratio`, `clamp()` e `@container` si comportano tutti normalmente.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` e `queueMicrotask` funzionano senza il prefisso `window.`. Solo `window.requestAnimationFrame(...)` e simili generano uneccezione.
</Note>
### Accesso al DOM
Un `ref` ti restituisce un elemento sandbox, non un `HTMLElement`.
| Cosa scrivi | Cosa succede | Usa invece |
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Genera uneccezione | Componenti controllati; leggi i valori da `event.target` |
| `element.classList.add(...)` | Genera uneccezione (`classList` è `undefined`) | Costruisci tu stesso la stringa `className` |
| `document.getElementById()`, `getElementsByClassName()`, `createTreeWalker()` | Genera uneccezione | `querySelector()` / `querySelectorAll()`, che funzionano |
| `document.activeElement` | Sempre `undefined` | Tieni traccia del focus con `onFocus` / `onBlur` |
| `\<canvas>` | Non renderizza nulla, nessun errore | SVG, oppure disegna offscreen e mostra un `<img src={dataUrl}>` |
| `createPortal(node, document.body)` | Non renderizza nulla, mentre `isConnected` segnala successo | Overlay inline con `position: absolute`, oppure passa alla libreria un tuo elemento container |
Il gap del portale è il motivo per cui i popover di Radix, Headless UI, MUI e react-select non renderizzano nulla per impostazione predefinita. La maggior parte accetta una prop container; indirizzala a un elemento che hai renderizzato.
### Eventi
Mouse, puntatore, touch, drag, tastiera, focus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` e `animationend`/`transitionend` passano all'host, più alcuni per elemento: `load`/`error` su `<img>`, appunti e composizione su `<input>`/`\<textarea>`, media su `\<video>`/`\<audio>`, `toggle` su `\<details>`/`\<dialog>`. Tutto il resto (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, capture del pointer, `onLoad` fuori da `<img>`) viene ignorato senza avviso.
`document.addEventListener()` e `window.addEventListener()` si registrano senza errori e non vengono mai attivati, motivo per cui un drag si interrompe non appena il puntatore lascia l'elemento da cui è partito. `event.preventDefault()` non viene propagato nemmeno; l'invio dei form, `dragover`/`drop` e i clic sui link sono già protetti per te.
### Attributi e stile
Ogni elemento inoltra le proprie proprietà al DOM host (`href` su `\<a>`, `src`/`alt` su `<img>`, `value`/`placeholder`/`disabled` su `<input>`, e così via), più un insieme comune su ogni elemento: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` e qualsiasi attributo `aria-*` / `data-*` (con trattino, quindi `ariaLabel` viene scartato). Qualsiasi cosa al di fuori di ciò viene silenziosamente scartata, quindi esprimi lo stato personalizzato come `data-*`.
Il CSS del componente, che provenga da `import './styles.css'`, da CSS-in-JS o da un elemento `\<style>`, viene iniettato nel `\<head>` della pagina host **senza ambito**. Quindi i nomi delle classi entrano in conflitto con quelli di Twenty (aggiungi un prefisso e non scrivere mai `div { ... }` come selettori), e `@media` corrisponde alla finestra del browser piuttosto che al tuo widget (usa `@container` con il tuo `container-type`). Le prop `style` inline non sono interessate.
### Storage e rete
`localStorage`, `sessionStorage`, IndexedDB, cookie, Cache API e `BroadcastChannel` non sono disponibili, poiché il componente viene eseguito in un worker con origine opaca. Per rendere persistente lo stato, chiama una [logic function](/l/it/developers/extend/apps/logic/logic-functions) e utilizza il suo [key-value store](/l/it/developers/extend/apps/logic/key-value-store).
`fetch` funziona, con alcune avvertenze:
* Le chiamate alla Twenty API e alle route della tua app sono proxyate dall'host, quindi preferisci [`RestApiClient`](#calling-the-twenty-rest-api). Nelle chiamate proxyate, `AbortSignal` e le altre opzioni di `RequestInit` vengono scartate, e sono supportati solo body di tipo `string` e `URLSearchParams`.
* Le altre origini escono dalla sandbox con `Origin: null`, quindi un'API di terze parti risponde solo se invia `Access-Control-Allow-Origin: *`. Invece, chiamala da una logic function.
* `fetch('/rest/people')` non viene mai associato alla Twenty API, perché la sandbox non ha un URL di pagina rispetto a cui risolvere un percorso relativo.
### Altre lacune
* **Contenuti dei file.** `<input type="file">` fornisce al tuo gestore solo i metadati del file, non i byte, quindi `FileReader` e gli upload non sono ancora possibili.
* **Payload di drag-and-drop.** Gli eventi di drag vengono attivati, ma `event.dataTransfer` è `undefined`.
* **Built-in di Node.** `fs`, `path` e `node:crypto` fanno fallire la build, quindi sposta quel lavoro in una [logic function](/l/it/developers/extend/apps/logic/logic-functions). Web Crypto, `fetch`, `TextEncoder` e `URL` sono disponibili.
* **`\<iframe>`** viene sempre nuovamente messo in sandbox senza `allow-same-origin`, quindi un embed che dipende dalla propria sessione viene renderizzato come disconnesso. Non ha nemmeno `onLoad`.