Vai al contenuto principale

Panoramica

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.
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.

Dove possono essere utilizzati i componenti front-end

I componenti front-end possono essere renderizzati in quattro 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. 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 — Indicate da una dichiarazione defineSettingsMenuItem(), il componente front-end visualizza la pagina associata a una delle voci del menu delle impostazioni dell’app. Un’app può dichiararne diverse.
  • Chat IA — Una funzione logica esposta come strumento IA può assegnare un nome a un componente front-end, che quindi renderizza le chiamate di quello strumento in linea nella chat al posto della riga dei passaggi predefinita.
Un componente front da solo non è raggiungibile dall’interfaccia utente — devi renderlo visibile. I quattro modi per farlo sono:

Esempio di base

Il modo più rapido per vedere un componente front in azione è associarlo a defineCommandMenuItem, così appare come pulsante di azione rapida nell’angolo in alto a destra della pagina:
src/front-components/hello-world.tsx
src/command-menu-items/hello-world.command-menu-item.ts
Dopo la sincronizzazione con twenty app dev (o eseguendo una sola volta twenty app apply), l’azione rapida appare nell’angolo in alto a destra della pagina:
Pulsante di azione rapida nell'angolo in alto a destra
Fai clic per renderizzare il componente in linea.

Campi di configurazione

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 per i dettagli. widgetAvailability determina quale selettore di widget offre il componente durante la modifica di un layout di pagina. Un componente front viene creato per le pagine del record, dove opera sul record visualizzato, oppure per le dashboard, pertanto dichiara un pageLayoutType:
Ometti widgetAvailability per un componente che viene aperto solo da un comando, da una voce del menu delle impostazioni o da una riga della timeline. Non limita mai dove viene visualizzato un componente: i comandi, le voci del menu delle impostazioni e i layout di pagina dichiarati dalla tua app lo visualizzano indipendentemente dal suo valore. La compilazione avvisa della presenza di un componente visibile che non ne dichiara alcuno e che non viene aperto da nessun’altra posizione.

Voci del menu delle impostazioni

Un’app può aggiungere le proprie voci al menu delle impostazioni, ciascuna delle quali visualizza un componente front-end. Dichiarane una con defineSettingsMenuItem() nel proprio file, indicandole il componente che la visualizza:
src/settings-menu-items/sync-settings.ts
La voce di menu indica il componente front-end anziché il componente che si dichiara come voce delle impostazioni. Un componente può quindi supportare diverse voci di menu e una voce può essere reindirizzata a un componente diverso senza modificare nessuno dei due. Ogni voce viene visualizzata come scheda nella pagina dell’app in Impostazioni > App, dopo Generali, nell’ordine di position. Dichiarane quante te ne servono. La voce Generale di Twenty — aggiornamento automatico, URL dell’app e connessioni — viene sempre prima e non può essere sovrascritta da un’app; l’URL dell’app viene visualizzato solo quando l’app dispone di funzioni attivate tramite HTTP.

Campi

position è un valore decimale, quindi una voce può essere inserita tra due voci esistenti senza rinumerarle. Le voci vengono ordinate per position crescente, con i pareggi risolti in base a universalIdentifier, così l’ordine è lo stesso in ogni workspace. Due voci della stessa app non possono condividere una posizione nello stesso ambito: la sincronizzazione lo rifiuta anziché lasciare che sia l’ordine di installazione a decidere quale venga visualizzata per prima.

URL delle schede

La scheda di ciascun elemento è identificata da un hash nell’URL della pagina, quindi puoi collegarti direttamente a essa, ad esempio /settings/applications/\<applicationId>#sync o /settings/app-preferences/\<applicationId>#sync. Imposta slug per scegliere quell’hash; senza di esso, la scheda è identificata da universalIdentifier dell’elemento. L’aggiunta successiva di uno slug mantiene funzionanti i link che utilizzano universalIdentifier: aprono la stessa scheda. Uno slug è in kebab-case minuscolo (lettere, cifre e trattini singoli, ad esempio sync o sync-settings) e contiene al massimo 64 caratteri. general e variables sono riservati alle schede integrate, uno slug in formato UUID viene rifiutato perché potrebbe oscurare il link di un’altra scheda e due elementi della stessa app non possono condividere uno slug, nemmeno in ambiti diversi. Sia la build sia la sincronizzazione rifiutano uno slug che viola queste regole. Dopo che un membro collega o ricollega un account in Impostazioni > Preferenze dell’app, viene indirizzato alla prima scheda dell’elemento USER dell’app, in ordine di position, oppure a Generali quando l’app non ne dichiara alcuno.

Ambito workspace e utente

scope indica da chi viene configurata una voce:
  • WORKSPACE (predefinito) — configurata una volta per l’intero workspace, insieme alle altre impostazioni del workspace. Queste sono le voci visualizzate come schede nella pagina dell’app.
  • USER — configurata da ciascun membro per sé stesso, nelle proprie impostazioni utente.
Un elemento USER viene visualizzato come scheda nella pagina dell’app in Impostazioni > Preferenze app, dove può vederlo solo il membro connesso e solo quando il suo ruolo concede l’autorizzazione App utente. Quella pagina non è ancora abilitata in ogni spazio di lavoro: dove non lo è, un elemento USER viene mantenuto e sincronizzato ma non viene visualizzato da nulla.
Una voce USER viene eseguita come il membro che la visualizza. Per archiviare qualcosa per ogni persona, dichiara la variabile con scope: 'USER': getApplicationVariable restituisce il valore del membro che visualizza la pagina e il tuo componente lo salva con updateMyUserApplicationVariable, che scrive solo il valore di quel membro e richiede SystemPermissionFlag.USER_APPS nel ruolo della tua app, se ne dichiara uno (vedi Lettura e aggiornamento delle variabili in fase di esecuzione). Come un elemento WORKSPACE, un elemento USER sostituisce il modulo Variabili generato, quindi dovrebbe consentire al membro di impostare tutte le variabili utente necessarie all’app. Una variabile dello spazio di lavoro contiene un valore condiviso da tutti, quindi il salvataggio di un membro sovrascriverebbe quello di un altro.
defineSettingsFrontComponent() è deprecato a favore di defineSettingsMenuItem(). Un’app che lo utilizza ancora continua a funzionare: il suo componente viene trattato come una singola voce di menu Impostazioni, ma può sempre e solo dichiararne una, limitazione che defineSettingsMenuItem() esiste per eliminare.

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.
src/front-components/sync-tracker.tsx
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 il contenuto nel pannello laterale. Le pagine instradate utilizzano le stesse props tipizzate to, params e queryParams di CommandLink, oltre alle props facoltative hash, pageTitle e resetNavigationStack. Il registro delle route dell’area di lavoro decide se l’AppPath richiesto supporta il pannello laterale. pageTitle sovrascrive il titolo dell’intestazione del pannello, mentre resetNavigationStack cancella le pagine precedenti del pannello prima di aprire la route. Gli strumenti del pannello progettati appositamente utilizzano le proprie prop specifiche della pagina.
Ecco un esempio completo di componente front-end headless che usa Command per eseguire un’azione dal menu comandi:
src/front-components/run-action.tsx
src/command-menu-items/run-action.command-menu-item.ts
E un esempio che usa CommandModal per chiedere conferma prima di eseguire:
src/front-components/delete-draft.tsx
Ed ecco un esempio che usa CommandOpenSidePanelPage per aprire il record corrente nel pannello laterale in una scheda specifica. hash è un id della scheda del layout della pagina senza il # iniziale (i layout predefiniti utilizzano id come company-tab-emails o company-tab-timeline; i layout personalizzati utilizzano l’id della scheda stessa). Se l’id non esiste nel layout del record, viene invece aperta la scheda predefinita:
src/front-components/open-company-emails.tsx

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 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 dell’app, li risolve nell’URL 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 l’URL esatto dalle impostazioni del trigger HTTP della funzione o dalla scheda Settings dell’applicazione.
Un front component headless può eseguire la chiamata al mount tramite il componente Command, quindi smontarsi automaticamente:
src/front-components/sync-prs.tsx
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:
src/logic-functions/fetch-prs.logic-function.ts
TWENTY_APP_ACCESS_TOKEN viene inserito automaticamente — vedi Variabili dell’applicazione. Poiché le variabili di applicazione segrete non vengono mai esposte ai componenti front-end, mantieni le chiavi API e altra logica sensibile nella funzione logica, non nel componente front-end.

Chiamare le Twenty REST API

Per chiamare le route HTTP dell’app 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. Agisce sempre come la persona che visualizza la pagina. runAs: 'application' è un’opzione esclusiva delle funzioni logiche: un componente non riceve mai il token della tua applicazione, quindi richiederlo qui genera un errore. Inserisci il lavoro che richiede l’accesso dell’app dietro una funzione logica e chiama invece quella. 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:
Le richieste non riuscite generano un RestApiClientError che espone status, statusText, url e il body analizzato:

Invio alla posta in arrivo

sendInboxMessage e gli helper per le menzioni della posta in arrivo sono esportati anche da twenty-sdk/front-component, quindi un componente può pubblicare un messaggio senza una funzione logica intermedia. Il messaggio proviene dalla tua app e la chiamata include il token del componente, quindi il membro che visualizza la pagina necessita dell’autorizzazione AI, come ogni destinatario. Per mostrare successivamente la conversazione, accedi a essa con il valore threadId restituito dalla chiamata:
  • workspaceMemberIds accetta gli ID dei membri dello spazio di lavoro, non l’ID utente restituito da useUserId().
  • Una conversazione si apre solo per i suoi partecipanti, quindi accedi a essa quando il membro che visualizza la pagina è uno dei destinatari.
  • La posta in arrivo è una route della pagina principale: navigate la apre lì e openSidePanelPage non può aprirla nel pannello laterale.

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:
src/front-components/record-info.tsx
Hook disponibili:

Metadati oggetto

Usa useSelectedObjectMetadata() per identificare l’oggetto associato a un widget della pagina del record, a una riga della timeline personalizzata, a un comando con ambito oggetto o a un componente front-end aperto nel pannello laterale con un oggetto (da un comando o tramite openSidePanelPage con un objectNameSingular). Ciò consente a un’integrazione di selezionare modelli o altre configurazioni per l’oggetto corrente senza cercare tra gli oggetti un ID record.
Il risultato contiene id (l’UUID dei metadati dell’oggetto del workspace), nameSingular e namePlural. I nomi sono nomi dei metadati, come person e people, anziché etichette visualizzate. I metadati dell’oggetto possono essere disponibili anche quando non è selezionato alcun record. Appartengono al contesto di questo componente front-end, quindi un pannello laterale può esporre un oggetto diverso dalla pagina principale. L’hook si aggiorna quando quel contesto cambia e restituisce null quando l’oggetto non può essere risolto, quando non viene fornito alcun contesto dell’oggetto o quando viene eseguito su un host meno recente che non fornisce metadati dell’oggetto.

Posizione host

Usa useLocation() per leggere la route che ospita il tuo componente front. Restituisce un oggetto FrontComponentLocation con quattro campi stringa obbligatori: pathname, search, hash e origin. Ad esempio, una pagina in https://myworkspace.twenty.com/page/page-layout-id?project=abc#details fornisce rispettivamente /page/page-layout-id, ?project=abc, #details e https://myworkspace.twenty.com. hash include il suo # iniziale ed è '' quando non è presente alcun frammento. pathname, search e hash seguono il contesto React Router del componente, inclusa la route di un pannello laterale con routing. origin proviene dall’app host. L’hook si aggiorna quando la route cambia. Gli host meno recenti potrebbero omettere executionContext.location, quindi useLocation() restituisce null quando la posizione non è disponibile. Per i parametri di query, leggi useLocation()?.search ?? '' e analizza il risultato con URLSearchParams. search include il suo ? iniziale ed è '' quando la route non ha una stringa di query. Il fallback fornisce anche '' quando l’host non fornisce la posizione.

Variabili dell’applicazione

Le variabili dell’applicazione definite in defineApplication() con isSecret: false sono disponibili all’interno dei componenti front-end tramite l’utility getApplicationVariable:
src/front-components/greeting.tsx
Le variabili segrete (isSecret: true) non sono esposte ai componenti front-end. Sono disponibili solo nelle funzioni logiche, che vengono eseguite lato server. Questo impedisce che valori sensibili come le chiavi API vengano inviati al browser.
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.

Lettura e aggiornamento delle variabili in fase di esecuzione

getApplicationVariable restituisce i valori inseriti al caricamento del componente, quindi un valore scritto successivamente verrà visualizzato solo al caricamento successivo. Per leggere i valori memorizzati o scriverne uno (ciò che fa una voce del menu delle impostazioni quando salva ciò che l’utente ha digitato), usa MetadataApiClient:
src/front-components/app-settings.tsx
Note:
  • Il token del componente è associato alla tua applicazione. updateOneApplicationVariable la interessa, quindi applicationId è facoltativo e specificare un’altra applicazione viene rifiutato. Anche la lettura delle applicationVariables di un’altra applicazione viene rifiutata allo stesso modo e un’applicazione appartenente a un altro workspace risulta non trovata.
  • La chiave deve essere una di quelle dichiarate dall’app in defineApplication(). La scrittura di una chiave non dichiarata risulta non trovata.
  • Le variabili segrete, quando vengono lette, vengono mascherate e non restituiscono mai il valore memorizzato. La loro scrittura funziona normalmente.
  • La lettura del value di una variabile e la sua scrittura richiedono l’autorizzazione alle impostazioni App workspace sia nel ruolo della tua app sia nel ruolo della persona connessa, poiché il token del componente è l’intersezione dei due. Aggiungi SystemPermissionFlag.WORKSPACE_APPS a permissionFlagUniversalIdentifiers nel tuo ruolo dell’applicazione.
  • Una variabile utente (scope: 'USER') viene iniettata con il valore del membro che visualizza, a meno che non sia segreta. updateOneApplicationVariable la rifiuta: salvala con updateMyUserApplicationVariable, come fa saveMyLanguage sopra. Scrive solo il valore del membro che sta visualizzando la pagina e rifiuta di specificare un’altra applicazione. Non richiede alcuna autorizzazione App workspace, ma richiede l’autorizzazione App utente nel ruolo del membro che visualizza la pagina e nel ruolo della tua app, se ne dichiara uno: aggiungi SystemPermissionFlag.USER_APPS a permissionFlagUniversalIdentifiers nel tuo ruolo dell’applicazione. Vedi Variabili dello spazio di lavoro e dell’utente.

Variabili di sistema

Le seguenti variabili di sistema sono sempre disponibili tramite process.env:

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é quell’URL non coincide sempre con il server di Twenty stesso. Su Twenty Cloud, le route dell’app sono servite su un dominio dedicato per ogni workspace (https://\<your-workspace-subdomain>.withtwenty.com, oppure il dominio pubblico primario dell’applicazione quando è configurato) in modo che le risposte create dall’app vengano eseguite sulla propria origine anziché sull’origine dell’app Twenty. Le istanze self-hosted e locali servono le route dell’app 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 l’URL 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 l’URL assoluto senza inviare una richiesta, ad esempio per un link. Leggi direttamente la variabile solo quando costruisci manualmente un URL:

API di comunicazione con l’host

I componenti front-end possono attivare navigazione, modali e notifiche utilizzando funzioni da twenty-sdk/front-component: Utilizza openCommandConfirmationModal da twenty-sdk/front-component per le finestre di dialogo. Accetta opzioni strutturate per titolo, sottotitolo e pulsanti e restituisce 'confirm' o 'cancel'. Twenty gestisce la modale, la gestione del focus, la chiusura e lo scorrimento della pagina. I componenti front-end non devono eseguire direttamente il rendering di Dialog, AlertDialog, elementi dialog nativi o popover nativi del browser. Fornire un contenitore portal locale non rende supportata la modalità gestita direttamente dall’app. Ecco un esempio che usa l’API host per mostrare una snackbar e chiudere il pannello laterale dopo il completamento di un’azione:
src/front-components/archive-record.tsx

Archiviazione

localStorage e sessionStorage funzionano come in una pagina normale, con l’API sincrona standard. Le tue chiavi sono limitate all’installazione della tua app e all’utente connesso: nessun’altra app può leggerle e un altro utente che effettua l’accesso dallo stesso browser parte da un archivio vuoto. I valori scritti in localStorage rimangono sul dispositivo tra un ricaricamento e l’altro; sessionStorage dura per l’intera sessione del browser.
src/front-components/note-draft.tsx
Twenty memorizza i valori per conto della tua app, quindi una scrittura viene applicata immediatamente in locale e salvata in background. Le letture non attendono mai l’host. Non viene sincronizzato nulla: i valori non seguono l’utente su un altro browser o macchina, quindi utilizza il key-value store di una logic function per qualsiasi cosa che debba sopravvivere a un cambio di dispositivo. Le scritture sono limitate e ogni limite conta i caratteri anziché i byte: le chiavi sono lunghe al massimo 512 caratteri, un singolo valore al massimo 262.144 caratteri e ogni archivio al massimo 1.048.576 caratteri per app e utente. Una scrittura che supera un limite genera un QuotaExceededError, come nell’API del browser.

Lavorare con più record

Usa useSelectedRecordIds() per gestire più record selezionati. Questo è utile per operazioni in blocco:
src/front-components/bulk-export.tsx
Mostralo con una voce del menu dei comandi limitata alle selezioni di record:
src/command-menu-items/bulk-export.command-menu-item.ts

Seleziona tutti

Quando l’utente seleziona tutti i record di una vista, useSelectedRecordIds() restituisce un array vuoto: la selezione è definita dai filtri, dalla ricerca e dalle righe non selezionate della vista e può contenere molti più record di quanti ne possano stare in memoria. useSelectedRecordsFilter() restituisce un filtro che corrisponde esattamente ai record selezionati in ogni caso, quindi esegui la query con esso invece di elencare gli ID:
Per selezioni di grandi dimensioni, passa il filtro a una funzione logica ed elabora i record lì, pagina per pagina.

Asset pubblici

I componenti front-end possono accedere ai file dalla directory public/ dell’app utilizzando getPublicAssetUrl:
Vedi la sezione sugli asset pubblici per i dettagli.

Condivisione delle dipendenze tra front component

Per impostazione predefinita, ogni front component include nel bundle la propria copia delle librerie che importa, quindi un’app con cinque componenti distribuisce React cinque volte. Dichiara le dipendenze condivise nel package.json della tua app per creare il bundle di queste librerie una sola volta e fare in modo che ogni componente dell’app le carichi da un unico file in cache:
package.json
Ogni componente importa quindi le sue dipendenze esattamente come prima — non cambia nulla nel codice del tuo componente:
src/front-components/counter.tsx
Alcune cose da sapere:
  • Un solo bundle di dipendenze condivise per app. Il bundle è costruito a partire dalle dipendenze della tua app, così mantieni il pieno controllo delle versioni che distribuisci.
  • Elenca gli specificatori esatti che importi. twenty-ui/primitives/input e twenty-ui/primitives/data-display sono due voci; il solo nome di un pacchetto non include i relativi sottopercorsi. L’elenco di react copre automaticamente react/jsx-runtime.
  • Condividi react-dom/client insieme a react. Ogni componente effettua il rendering tramite createRoot, quindi ometterlo significa che ogni componente include ancora nel bundle React DOM.
  • Il bundle è in cache. È servito tramite un URL con content-hash con una cache immutabile a lunga durata, quindi viene scaricato una sola volta e riutilizzato in tutti i componenti dell’app finché una delle sue dipendenze non cambia.
  • I componenti che non importano nessuno dei pacchetti condivisi non lo scaricano mai.

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 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. Preferisci i relativi controlli e token del tema per conferire ai componenti front lo stesso aspetto e le stesse interazioni del resto di Twenty. Esplora i componenti condivisi e le primitive per esempi, prop e API di composizione.

Installazione

Le nuove app create con twenty app init includono già twenty-ui. Per un’app esistente, aggiungilo come dipendenza di sviluppo, vincolata alla stessa versione di twenty-sdk e twenty-client-sdk. Ad esempio, se i tuoi pacchetti SDK usano 2.51.0:
twenty-ui viene incluso nel bundle in fase di build, in ciascun componente front oppure nel bundle delle dipendenze condivise della tua app. Twenty carica i fogli di stile dei componenti e del tema e applica lo schema di colori attivo dell’area di lavoro. Non è necessario importare twenty-ui/style.css, i fogli di stile del tema o aggiungere un ThemeProvider radice come faresti in un’applicazione React autonoma.

Importare i componenti

Importa dal sotto-percorso pubblico della famiglia corrispondente per mantenere mirate le dipendenze: I controlli fondamentali sono raggruppati in twenty-ui/primitives/\<family>. Le preimpostazioni di design condivise e i blocchi di base dell’app sono raggruppati in twenty-ui/components/\<family>. Il punto di ingresso aggregato twenty-ui/components rimane disponibile, ma le importazioni delle famiglie semplificano la condivisione solo delle dipendenze utilizzate. Usa MainButton per le azioni principali e LightButton per le azioni leggere. Usa Button, Field, Input, Select e altre primitive da twenty-ui/primitives/input per controlli e moduli di base. Segui le parti documentate di ciascun controllo composto, come Field.Root, Field.Label e Field.Description. Passa alignItemWithTrigger={false} a Select.Positioner. L’allineamento dell’elemento selezionato con l’attivatore, impostazione predefinita di Base UI, richiede misurazioni sincrone del layout che il sandbox non può fornire, quindi il popup si comprime quando viene riaperto. Usa Dropdown da twenty-ui/components/navigation per menu di azioni, selettori di opzioni e pannelli di moduli non modali. Usa enqueueSnackbar da twenty-sdk/front-component per le notifiche dell’host. Le API autonome Dialog e AlertDialog sono anch’esse esportate da Twenty UI, ma l’uso diretto nei componenti front-end è vietato. Utilizza l’API di comunicazione con l’host per le finestre di dialogo. Questi punti di ingresso non richiedono Monaco. Il punto di ingresso separato twenty-ui/components/code-editor dipende da Monaco, il cui caricatore di script non è supportato nel sandbox dei componenti front-end, quindi CodeEditor non può diventare interattivo in tale ambiente.

Icone

Importa le singole icone da twenty-ui/icon:
Ogni icona nominata è sottoposta a tree-shaking, quindi importarne alcune aggiunge poco al tuo bundle. Evita twenty-ui/icon/provider (IconsProvider, useIcons e Icon per nome) nei componenti front. I componenti front vengono raggruppati senza code splitting, quindi il catalogo dinamico di icone del provider (l’intero set Tabler, circa 2 MB minificato) finisce nel tuo bundle. Consulta il riferimento delle icone per le esportazioni disponibili e le props supportate.

Temi e token del tema

I componenti UI di Twenty corrispondono automaticamente al tema chiaro e scuro dell’area di lavoro. Il renderer applica lo schema di colori attivo sull’host e i componenti risolvono le proprie variabili CSS rispetto a esso. Per usare gli stessi design token nei tuoi stili inline, chiama useTheme() all’interno del corpo del componente. Nei componenti front-end restituisce per impostazione predefinita riferimenti a variabili CSS, quindi gli stili seguono il tema attivo dell’host senza un ThemeProvider locale:
Gli stessi riferimenti CSS vengono esportati come themeCssVariables da twenty-ui/theme. Leggili all’interno del corpo del componente: la CLI simula twenty-ui durante l’estrazione del manifesto, quindi il dereferenziamento delle costanti del tema importate nell’ambito del modulo può causare il fallimento della build. Le stringhe di riferimento CSS non sono numeri JavaScript risolti, neanche per token come theme.icon.size.md. Non eseguire operazioni aritmetiche su di esse né passarle ad API che richiedono numeri. Consulta Temi e Token per i percorsi semantici e i nomi delle variabili CSS. 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, la gestione degli eventi, la misurazione degli elementi, i portali del body e lo storage del browser funzionano bene. Qualsiasi cosa che vada oltre questi aspetti (chiamare un metodo del DOM su un ref, per esempio) oggi manca o è incompleta, e la maggior parte di queste operazioni fallisce silenziosamente: nessuna eccezione e nessun errore TypeScript, poiché lo scaffold è tipizzato rispetto al DOM completo del browser. Se una di queste limitazioni ti blocca, apri una issue così la sua priorità aumenta.

Layout e misurazione

Gli elementi possono misurare sé stessi: l’host rispecchia la geometria nella sandbox, quindi le letture vengono gestite localmente ma possono essere non aggiornate fino a un frame, e la prima lettura di un elemento mai misurato restituisce zeri. Dopo la scrittura, rileggi in una callback di requestAnimationFrame o in un effetto. Il posizionamento da getBoundingClientRect e l’osservazione delle dimensioni tramite ResizeObserver funzionano entrambi dallo stesso mirror, quindi i callback di ridimensionamento seguono l’host con un ritardo di circa un frame. Preferisci comunque il CSS per il layout: il tuo stylesheet raggiunge la pagina reale, quindi flexbox, grid, aspect-ratio, clamp() e @container si comportano normalmente, senza lag del frame. matchMedia() valuta width, height, orientation, -webkit-device-pixel-ratio, resolution, hover, pointer e prefers-color-scheme, nella forma min-/max-, nella forma di intervallo di livello 4 ((width >= 600px), (400px \<= width \<= 800px)) e nella forma senza valore ((width)). Larghezze, altezze e orientation descrivono il riquadro del tuo widget, non il viewport del browser, quindi seguono document.body.clientWidth anziché window.innerWidth, e em e rem contano come 16px. hover e pointer descrivono l’input principale del dispositivo dell’utente. Qualsiasi altra condizione è considerata sconosciuta, come i browser trattano le funzionalità sconosciute: l’elemento separato da virgole in cui compare viene valutato come false, anche con not, a meno che il resto di quell’elemento non determini già il risultato, come in not print and (prefers-reduced-motion: reduce). Questo comprende le funzionalità sconosciute e la sintassi che questo parser non implementa all’interno di una condizione, come gruppi annidati quali ((min-width: 600px)), not all’interno di un gruppo e valori calc(). Un or tra condizioni rende il relativo elemento false. Anche i costruttori MediaQueryList e MediaQueryListEvent non sono esposti, quindi i controlli instanceof su di essi generano un ReferenceError. Poiché quel foglio di stile raggiunge la pagina reale, una regola @media (min-width: ...) viene valutata dall’host rispetto al viewport del browser, mentre matchMedia('(min-width: ...)') risponde in base al riquadro del tuo widget. Per fare in modo che il CSS segua lo stesso riquadro visto da JavaScript, dichiara il tuo container-type su un wrapper ed esegui la query su quello: una regola @container senza uno viene risolta rispetto al contenitore host più vicino, non al tuo widget. La stessa distinzione si applica alla combinazione di colori, dove un foglio di stile @media (prefers-color-scheme: dark) segue la preferenza del browser e del sistema operativo, mentre matchMedia('(prefers-color-scheme: dark)') segue il tema selezionato in Twenty, quindi applica gli stili dai token di useTheme() quando i due devono coincidere. Anche l’altezza che ricevi è quella del tuo riquadro e, a seconda di dove l’host ti monta, quel riquadro può essere dimensionato dai tuoi contenuti. La diramazione del layout in base a min-height, max-height o orientation può quindi influire su sé stessa e oscillare. Per le decisioni di layout, preferisci query basate sulla larghezza oppure @container con container-type: inline-size.
requestAnimationFrame funziona, con o senza il prefisso window.. Solo Chrome esegue un vero frame di animazione all’interno di un worker; Firefox e Safari ricevono un timer di 16 ms, quindi usalo per posticipare una nuova lettura dopo una scrittura, non per eseguire un’ animazione.

Scorrimento

L’assegnazione di scrollTop o scrollLeft, la chiamata di scrollTo() o del suo alias scroll(), e la chiamata di scrollIntoView() fanno scorrere l’elemento corrispondente nella pagina:
  • Le modifiche allo scorrimento vengono applicate dopo il rendering successivo, nell’ordine in cui sono state effettuate dal codice. All’interno di un rendering, le modifiche alla selezione dell’input vengono applicate per prime, quindi setSelectionRange() seguito da una scrittura su scrollLeft mantiene visibile il cursore.
  • Le letture restituiscono il valore scritto finché la pagina non segnala la posizione reale, che il browser limita all’intervallo scorrevole. Nei contenitori da destra a sinistra, scrollLeft è zero o negativo, come nei browser.
  • scrollIntoView() accetta block e inline con start, center, end e nearest, allineando block verticalmente e inline orizzontalmente, quindi le modalità di scrittura verticale non vengono mappate. Fa scorrere solo i contenitori di scorrimento del tuo componente, mai il frame del widget, la pagina o un altro componente. focus() continua a mostrare l’elemento attivo come fa il browser.
  • Lo scorrimento è istantaneo: behavior e l’opzione container vengono ignorati e scrollBy() non è disponibile.
  • Riscrivere una posizione appena letta, ad esempio element.scrollTop = element.scrollTop, non ha alcun effetto, quindi non può annullare uno scorrimento effettuato dall’utente nel frattempo. Questo vale solo dopo che la pagina ha misurato l’elemento; fino ad allora, la scrittura viene applicata.
  • Lo scorrimento di document.documentElement, document.body o window rimane all’interno della sandbox e non fa mai scorrere la pagina; window.scrollTo(), scroll() e scrollBy() non hanno alcun effetto.
  • La prima lettura di un contenitore che non è mai stato misurato restituisce zeri (vedi Layout e misurazione), quindi un controllo della possibilità di scorrimento eseguito subito dopo la comparsa del contenitore può segnalarlo come non scorrevole.

Ambito globale

Gli elementi nativi del worker sono anch’essi associati a window, quindi funzionano con o senza il prefisso window.: fetch, setTimeout/clearTimeout, setInterval/clearInterval, queueMicrotask, console, crypto, performance, structuredClone, btoa/atob, URL/URLSearchParams, AbortController/AbortSignal, Headers/Request/Response, Blob, File, FormData e TextEncoder/TextDecoder.

Accesso al DOM

Un ref ti restituisce un elemento sandbox, non un HTMLElement. element.classList funziona, supportato dall’attributo class: add, remove, toggle, replace, contains, item, length, value e l’iterazione si comportano tutti come in un browser e className rimane sincronizzato. element.dataset funziona allo stesso modo, supportato dagli attributi data-*; scrivi i nomi data-* in minuscolo, poiché il sandbox mantiene i nomi degli attributi così come sono scritti invece di convertirli in minuscolo come farebbe un browser. I portali del body possono estendersi fino a 200px oltre ciascun bordo del componente, anche al di fuori dell’area di scorrimento di un widget. Gli overlay più grandi vengono ritagliati in quest’area, quindi assegna ai menu lunghi il proprio contenitore di scorrimento. La navigazione tramite tastiera negli elenchi dell’interfaccia utente Twenty, come Autocomplete e Select, mostra l’opzione evidenziata all’interno di quel contenitore. Le API native del livello superiore come showModal(), showPopover() e requestFullscreen() sono bloccate.

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, acquisizione del puntatore, onLoad su img) viene scartato senza alcun avviso. I gestori ricevono eventi tipizzati creati a partire dall’evento della pagina, PointerEvent, MouseEvent, KeyboardEvent, InputEvent, WheelEvent o FocusEvent, e questi costruttori sono presenti nel sandbox per dispatchEvent(). Ogni evento inoltrato include anche la superficie dell’evento sintetico di React (nativeEvent, isDefaultPrevented(), isPropagationStopped(), persist()), quindi il codice scritto per gli eventi React continua a funzionare anche se il sandbox passa direttamente al tuo gestore l’oggetto evento. Un evento della pagina attraversa quando l’elemento con cui l’utente ha interagito, o uno dei suoi elementi antenati nel tuo componente, ha un gestore per quel tipo di evento, oppure quando si tratta di una pressione che i tuoi listener di document stanno aspettando (vedi sotto). Attraversa il confine una sola volta e poi si propaga all’interno del sandbox come avviene nella pagina: event.target è l’elemento con cui l’utente ha interagito, currentTarget è l’elemento il cui gestore è in esecuzione, stopPropagation() impedisce l’esecuzione dei gestori degli antenati e relatedTarget punta all’elemento del sandbox da cui o verso cui si è spostato il focus o il puntatore, oppure è null quando tale elemento è esterno al tuo componente. Come in React, anche onFocus e onBlur vengono eseguiti per le modifiche del focus all’interno dell’elemento, mentre onClickCapture e gli altri gestori di cattura vengono eseguiti durante la fase di cattura. I gestori che una libreria aggiunge con React.cloneElement, come quelli associati alla prop render di Base UI, vengono eseguiti dopo i gestori dell’elemento stesso invece di sostituirli, quindi passare onClick: undefined a cloneElement non rimuove il gestore dell’elemento. Un gestore su un elemento render di Base UI può comunque chiamare event.preventBaseUIHandler() per ignorare il gestore di Base UI. event.target.value e checked riflettono il controllo una volta che il browser ha applicato la modifica dell’utente: nei gestori input, change, click, blur e focusout, e nei gestori keyup per value. Nei gestori keydown, beforeinput e del puntatore, contengono il valore e lo stato selezionato del controllo a partire dalla sua ultima modifica, indipendentemente da ciò che il tuo componente ascolta: un gestore Enter legge il testo digitato finora, ma un gestore per un tasto stampabile non vede ancora quel tasto; pertanto, leggi i valori in tempo reale da un input controllato o da un gestore onChange. I listener di document ricevono gli eventi che attraversano dal tuo componente, con un’eccezione per le pressioni. Mentre il tuo componente rimane in ascolto di pointerdown, pointerup, pointercancel, mousedown, mouseup o click su document, come fa un popup aperto per chiudersi in caso di pressioni esterne, ogni pressione raggiunge tali listener: una pressione all’interno del tuo componente viene inviata all’elemento premuto dall’utente anche quando nulla la gestisce, e una pressione in un’altra parte della pagina viene inviata a document.body con lo stato del pulsante, il numero di clic e il tipo di puntatore, ma senza coordinate né tasti modificatori. I clic senza una pressione, come quando la tastiera attiva un controllo esterno al tuo componente, e le pressioni sulle barre di scorrimento della pagina non vengono inviati; inoltre, uno scorrimento con tocco esterno al tuo componente arriva solo come pointerdown e pointercancel, quindi non chiude un popup aperto. Nient’altro dal resto della pagina attraversa, quindi per intercettare le scorciatoie con document.addEventListener('keydown', ...), inserisci un onKeyDown su un elemento che racchiude il tuo componente. I listener window ricevono solo error e unhandledrejection. Gli spostamenti del puntatore attraversano il confine solo sugli elementi che hanno un gestore di spostamento, e la cattura del puntatore rimane nella pagina; per questo motivo, un trascinamento si interrompe non appena il puntatore lascia tali elementi. 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 nell’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 e sessionStorage sono forniti da Twenty anziché dal browser: il componente viene eseguito in un worker con un’origine opaca, quindi l’host memorizza i valori per conto della tua app. Vedi storage per il loro ambito e i relativi limiti. IndexedDB, i cookie, la Cache API e BroadcastChannel restano non disponibili. Per rendere persistente lo stato tra i dispositivi, chiama una logic function e utilizza il suo 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. 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.

Selezione del file

Gli input file forniscono oggetti File nativi con metadati e contenuti leggibili. Usa text() o arrayBuffer() per leggere un file selezionato oppure passalo alla funzione host uploadFile. L’apertura del selettore nativo richiede di chiamare input.click() entro un secondo da un clic attendibile all’interno del componente. I flussi React e Preact creati con l’SDK sono verificati in Chromium. Gli URL degli oggetti creati dal worker assegnati a img.src supportano le anteprime. Revoca i tuoi URL quando sostituisci l’immagine o smonti il componente. La tua applicazione gestisce la convalida dei file, l’avanzamento del caricamento e l’annullamento.

Acquisizione multimediale

Dichiara microphone, camera o entrambi nell’array requestedCapabilities passato a defineApplication() affinché queste capacità vengano visualizzate durante l’installazione. L’approvazione è valida per l’intero spazio di lavoro e richiede un membro che possa gestire le app. Se un’app installata chiama navigator.mediaDevices.getUserMedia() senza l’accesso richiesto, Twenty sospende l’acquisizione e mostra una finestra di dialogo per l’approvazione. Questo supporta anche le app meno recenti senza dichiarazioni delle capacità. I membri che non possono gestire le app devono chiedere a un amministratore dello spazio di lavoro di approvare l’accesso. L’annullamento della finestra di dialogo rifiuta la richiesta prima che vengano richieste le autorizzazioni del browser o del sistema operativo. Gli aggiornamenti del pacchetto mantengono le autorizzazioni esistenti dello spazio di lavoro e non concedono automaticamente ulteriore accesso ai contenuti multimediali. Le app di sviluppo sincronizzate localmente ricevono le capacità dichiarate nel relativo manifest. Una volta approvato l’accesso, Twenty non chiede nuovamente l’autorizzazione per ogni acquisizione, ma continuano ad applicarsi le autorizzazioni del browser e del sistema operativo. Un punto arancione nell’intestazione dello spazio di lavoro indica un’acquisizione attiva. Passa il puntatore sul punto o selezionalo per visualizzare le app che utilizzano il microfono o la fotocamera e interrompere la relativa acquisizione. Le richieste in sospeso vengono visualizzate come un punto delineato con un controllo di annullamento. getUserMedia() e MediaRecorder vengono eseguiti tramite polyfill sandbox, pertanto il codice di registrazione standard e MediaRecorder.isTypeSupported() funzionano senza modifiche dopo l’approvazione. Gli oggetti di vincolo dettagliati sono accettati ma non inoltrati. L’host acquisisce usando le impostazioni predefinite per i tipi richiesti. Applicazioni diverse possono acquisire contemporaneamente e l’arresto di un’app influisce solo sulle proprie sessioni. Ogni componente front supporta un’acquisizione in sospeso o attiva alla volta. Archivia un Blob registrato con la funzione host uploadFile.

Siti web incorporati con accesso ai media

Per incorporare uno strumento telefonico o video, dichiara la relativa origine HTTPS in requestedIframeMediaPermissions della tua applicazione, con l’accesso al microfono e alla fotocamera impostati separatamente. L’installazione dell’app concede questi siti web allo spazio di lavoro. Gli aggiornamenti alle app pubblicate mantengono le autorizzazioni esistenti e non concedono automaticamente ulteriore accesso ai media. Le app di sviluppo sincronizzate localmente ricevono automaticamente i siti web dichiarati nel relativo manifest. Un membro che può gestire le app può approvare nuove richieste dalla scheda Generale dell’app installata, in Accesso ai media dei siti web incorporati > Esamina l’accesso ai media. Questa approvazione utilizza una conferma nativa del browser che mostra l’app, le origini dei siti web e l’accesso richiesto al microfono o alla fotocamera. Twenty legge i siti web autorizzati al caricamento di una scheda, quindi ricarica le schede Twenty aperte dopo l’installazione o l’approvazione. Richiedi le funzionalità corrispondenti nell’iframe:
Per il video, utilizza allow="microphone; camera" e dichiara entrambe per l’origine. Twenty delega solo i media concessi alla tua app per quell’origine. I membri devono selezionare il controllo Abilita dell’elemento incorporato e approvare una conferma nativa del browser che mostra l’app, l’origine del sito web e l’accesso richiesto al microfono o alla fotocamera prima che riceva l’accesso ai media. Le autorizzazioni del browser e del sistema operativo sono ancora applicabili. srcDoc e gli elementi incorporati all’origine di Twenty non possono ricevere questa delega. I widget di layout di pagina IFRAME definiti dall’app utilizzano le origini concesse all’app proprietaria e lo stesso flusso di conferma. Gli elementi incorporati abilitati appaiono nello stesso indicatore nell’intestazione dell’acquisizione nativa, con Accesso al microfono abilitato o Accesso alla fotocamera abilitato anziché indicare che la registrazione è attiva. L’indicatore rimane visibile finché l’accesso è abilitato, anche se l’elemento incorporato è nascosto. Disabilita scarica l’iframe e lo ricarica con i media bloccati. L’acquisizione dell’app tramite getUserMedia() continua a segnalare l’acquisizione effettiva e mantiene il proprio controllo Arresta. Il contenuto dell’iframe rimane in sandbox. Gli elementi incorporati di media esterni approvati mantengono la propria origine dopo che il membro abilita l’accesso, consentendo al sito web di utilizzare la propria sessione e richiedere le autorizzazioni del browser per i media. Gli altri iframe dei componenti front-end mantengono un’origine sandbox opaca.

Altre lacune

  • 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. Web Crypto sostituisce node:crypto e sono disponibili anche fetch, TextEncoder e URL; consulta l’ambito globale — ma nulla sostituisce fs: il sandbox non dispone di un filesystem.