Skip to main content
Les composants frontaux sont des composants React qui s’affichent directement dans l’interface utilisateur de Twenty. Ils s’exécutent dans un Web Worker isolé en utilisant Remote DOM — votre code s’exécute dans un iframe à origine opaque et sandboxé, mais son interface utilisateur continue de s’afficher nativement dans la page plutôt que d’être confinée à cet iframe.
Les composants Front sont encore en cours de développement actif. Votre code s’exécute sur un DOM partiel, et non sur une véritable page de navigateur, de sorte que les utilisations avancées peuvent échouer, souvent sans message d’erreur. Voir Limitations actuelles.

Où les composants frontaux peuvent être utilisés

Les composants frontaux peuvent s’afficher à trois emplacements au sein de Twenty :
  • Panneau latéral — Les composants frontaux non-headless s’ouvrent dans le panneau latéral droit. Il s’agit du comportement par défaut lorsqu’un composant frontal est déclenché depuis le menu de commande.
  • Widgets (tableaux de bord et pages d’enregistrement) — Les composants frontaux peuvent être intégrés comme widgets dans les mises en page. Lors de la configuration d’un tableau de bord ou d’une page d’enregistrement, les utilisateurs peuvent ajouter un widget de composant frontal.
  • Paramètres de l’application — Défini avec defineSettingsFrontComponent(), le composant frontal s’affiche comme une section dans l’onglet Settings de l’application, à la place de l’interface utilisateur par défaut de configuration des variables.
Un composant frontal seul n’est pas accessible depuis l’interface utilisateur — vous devez l’exposer. Les trois façons de le faire sont :
  • L’associer à un élément de menu de commande — l’enregistre dans le menu de commande (Cmd+K) et, éventuellement, comme action rapide épinglée.
  • L’intégrer comme widget dans une mise en page — le place sur la page de détails d’un enregistrement ou sur un tableau de bord.
  • Le définir avec defineSettingsFrontComponent() — l’affiche comme une section dans l’onglet Settings de l’application, à la place de l’interface utilisateur par défaut de configuration des variables.

Exemple de base

La façon la plus rapide de voir un composant frontal en action est de l’associer à un defineCommandMenuItem, afin qu’il apparaisse comme bouton d’action rapide dans le coin supérieur droit de la page :
src/front-components/hello-world.tsx
src/command-menu-items/hello-world.command-menu-item.ts
Après la synchronisation avec yarn twenty dev (ou en exécutant une seule fois yarn twenty apply), l’action rapide apparaît dans le coin supérieur droit de la page :
Cliquez dessus pour afficher le composant en ligne.

Champs de configuration

Placer un composant frontal sur une page

Au-delà des commandes, vous pouvez intégrer un composant frontal directement dans une page d’enregistrement en l’ajoutant comme widget dans une mise en page. Voir mises en page pour plus de détails.

Composant de paramètres personnalisé

Pour remplacer l’interface utilisateur générée automatiquement pour la configuration des variables dans l’onglet Settings de votre application par votre propre composant, définissez-le avec defineSettingsFrontComponent au lieu de defineFrontComponent. Il utilise les mêmes champs de configuration (sauf isHeadless, qui n’est pas accepté puisqu’un composant de paramètres affiche toujours une interface utilisateur visible) et marque en plus le composant comme interface de paramètres de l’application. Le composant est affiché comme une section à l’intérieur de l’onglet Settings, et non comme un remplacement de l’onglet entier. Les sections gérées par le système de Twenty — mise à niveau automatique, App URL et connexions — sont toujours affichées au-dessus et ne peuvent pas être remplacées par l’application.
src/front-components/app-settings.tsx
Un seul composant frontal de paramètres est autorisé par application ; en déclarer plus d’un provoque l’échec de la compilation. Lorsqu’il est présent, l’onglet Settings de l’application affiche ce composant à la place de l’interface utilisateur de configuration des variables par défaut.

Headless vs non-headless

Les composants frontaux existent en deux modes de rendu contrôlés par l’option isHeadless : Non-headless (par défaut) — Le composant affiche une interface visible. Lorsqu’il est déclenché depuis le menu de commande, il s’ouvre dans le panneau latéral. Il s’agit du comportement par défaut lorsque isHeadless est false ou omis. Headless (isHeadless: true) — Le composant se monte de façon invisible en arrière-plan. Il n’ouvre pas le panneau latéral. Les composants headless sont conçus pour des actions qui exécutent une logique puis se démontent — par exemple, lancer une tâche asynchrone, naviguer vers une page ou afficher une fenêtre modale de confirmation. Ils s’associent naturellement aux composants Command du SDK décrits ci-dessous.
src/front-components/sync-tracker.tsx
Comme le composant retourne null, Twenty n’affiche pas de conteneur pour celui-ci — aucun espace vide n’apparaît dans la mise en page. Le composant a toujours accès à tous les hooks et à l’API de communication de l’hôte.

Composants Command du SDK

Le package twenty-sdk fournit quatre composants utilitaires Command conçus pour les composants frontaux headless. Chaque composant exécute une action au montage, gère les erreurs en affichant une notification snackbar et démonte automatiquement le composant frontal une fois terminé. Importez-les depuis twenty-sdk/front-component :
  • Command — Exécute un callback asynchrone via la prop execute.
  • CommandLink — Navigue vers un chemin d’application. Props : to, params, queryParams, options.
  • CommandModal — Ouvre une fenêtre modale de confirmation. Si l’utilisateur confirme, exécute le callback execute. Props : title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — Ouvre une page du panneau latéral. Les propriétés dépendent de page — par exemple, ViewRecord accepte recordId + objectNameSingular (plus un identifiant tab facultatif pour ouvrir l’enregistrement dans un onglet spécifique), les autres pages acceptent pageTitle + pageIcon.
Voici un exemple complet d’un composant frontal headless utilisant Command pour exécuter une action depuis le menu de commande :
src/front-components/run-action.tsx
src/command-menu-items/run-action.command-menu-item.ts
Et un exemple utilisant CommandModal pour demander une confirmation avant l’exécution :
src/front-components/delete-draft.tsx
Et un exemple utilisant CommandOpenSidePanelPage pour ouvrir l’enregistrement actuel dans le panneau latéral sur un onglet spécifique. tab est un identifiant d’onglet de mise en page (les mises en page par défaut utilisent des identifiants comme company-tab-emails ou company-tab-timeline ; les mises en page personnalisées utilisent l’identifiant propre de l’onglet). Si l’identifiant n’existe pas dans la mise en page de l’enregistrement, l’onglet par défaut s’ouvre à la place :
src/front-components/open-company-emails.tsx

Appel d’une fonction logique

Les composants front s’exécutent côté navigateur dans un Web Worker isolé (sandboxé) à l’intérieur d’un iframe à origine opaque, tandis que les fonctions logiques s’exécutent côté serveur. Il n’y a aucun appel intra-processus direct entre les deux — à la place, un composant front appelle une fonction logique via HTTP. Une fonction logique déclarée avec httpRouteTriggerSettings est accessible via HTTP à son chemin de route. RestApiClient traite les chemins commençant par /s/ comme des routes d’application, les résout vers l’URL à partir de laquelle vos fonctions sont servies et les authentifie avec TWENTY_APP_ACCESS_TOKEN.
Sur Twenty Cloud, les fonctions logiques déclenchées par HTTP sont servies sur un domaine dédié par espace de travail à l’adresse https://\<your-workspace-subdomain>.withtwenty.com\<path>. Pour les appelants externes, copiez l’URL exacte à partir des paramètres HTTP trigger de la fonction ou de l’onglet Settings de l’application.
Un composant front sans interface (headless) peut effectuer l’appel au montage via le composant Command, puis se démonter automatiquement :
src/front-components/sync-prs.tsx
Le chemin transmis à RestApiClient est la propriété httpRouteTriggerSettings.path de la fonction logique, préfixée par /s. Conservez isAuthRequired: true ; le TWENTY_APP_ACCESS_TOKEN que Twenty génère pour votre composant authentifie la requête :
src/logic-functions/fetch-prs.logic-function.ts
TWENTY_APP_ACCESS_TOKEN est injecté automatiquement — voir Variables d’application. Comme les variables d’application secrètes ne sont jamais exposées aux composants front, conservez les clés d’API et les autres éléments sensibles dans la fonction logique, et non dans le composant front.

Appeler l’API REST de Twenty

Pour appeler des routes HTTP d’application ou lire et écrire des enregistrements Twenty depuis un composant frontal, utilisez RestApiClient depuis twenty-client-sdk/rest. Il envoie les chemins /s/... vers l’URL de base des fonctions de votre espace de travail et tous les autres chemins, y compris /rest/..., vers TWENTY_API_URL. Il agit toujours en tant que la personne qui consulte la page. runAs: 'application' est une option réservée aux fonctions logiques : un composant ne reçoit jamais le jeton propre à votre application, donc le demander ici génère une erreur. Placez le travail nécessitant l’accès propre à l’application derrière une fonction logique et appelez-la à la place. options accepte headers, query (un enregistrement de paramètres de chaîne de requête ; les valeurs nullish sont ignorées), et un AbortSignal via signal. Un objet body qui n’est pas de type FormData est automatiquement sérialisé en JSON. Sur un 401, le client actualise une fois le jeton d’accès via l’hôte puis retente la requête. Par défaut, l’URL de base et le jeton sont résolus à partir de l’environnement. Passez des valeurs de remplacement (overrides) au constructeur lorsque nécessaire — par exemple dans les tests :
Les requêtes ayant échoué lèvent une erreur RestApiClientError exposant status, statusText, url et le body analysé :

Accéder au contexte d’exécution

Dans votre composant, utilisez les hooks du SDK pour accéder à l’utilisateur actuel, à l’enregistrement et à l’instance du composant :
src/front-components/record-info.tsx
Hooks disponibles :

Variables d’application

Les variables d’application définies dans defineApplication() avec isSecret: false sont disponibles dans les composants front via l’utilitaire getApplicationVariable :
src/front-components/greeting.tsx
Les variables secrètes (isSecret: true) ne sont pas exposées aux composants front. Elles sont uniquement disponibles dans les fonctions logiques, qui s’exécutent côté serveur. Cela empêche l’envoi au navigateur de valeurs sensibles comme les clés d’API.
getApplicationVariable renvoie toujours une chaîne (ou undefined), quel que soit le type déclaré de la variable. La chaîne est sérialisée de manière cohérente selon le type (booléens sous la forme "true" / "false", nombres sous forme de chaînes décimales, tableaux / objets en JSON), dans le même format utilisé pour la fonction logique process.env — analysez-la vous-même (Number(...), JSON.parse(...), === 'true'). Voir Types de variables. Les variables système suivantes sont toujours disponibles via process.env :

TWENTY_FUNCTIONS_URL

Twenty injecte également TWENTY_FUNCTIONS_URL dans les composants frontaux et les fonctions logiques : l’URL de base à partir de laquelle les fonctions logiques déclenchées par HTTP de votre application sont servies. Elle existe parce que cette URL n’est pas toujours le serveur Twenty lui-même. Sur Twenty Cloud, les routes d’application sont servies sur un domaine dédié par espace de travail (https://\<your-workspace-subdomain>.withtwenty.com, ou le domaine public principal de l’application lorsqu’il est configuré) afin que les réponses créées par l’application s’exécutent sur une origine isolée plutôt que sur l’origine de l’application Twenty. Les instances auto-hébergées et locales servent les routes d’application sous le préfixe /s sur le serveur lui-même et peuvent ne pas définir la variable du tout. Comme l’URL de base varie selon l’espace de travail et l’instance, votre code ne peut pas la coder en dur — le serveur injecte la bonne valeur à l’exécution. Vous avez rarement besoin de la lire directement. Appelez vos routes via RestApiClient avec un chemin préfixé par /s/ et le client résout l’URL pour vous : il retire le préfixe /s et cible TWENTY_FUNCTIONS_URL, en revenant à \<TWENTY_API_URL>/s lorsque la variable n’est pas définie. Utilisez resolveUrl('/s/\<path>') pour obtenir l’URL absolue sans envoyer de requête, par exemple pour un lien. Lisez la variable directement uniquement lorsque vous construisez une URL manuellement :

API de communication de l’hôte

Les composants frontaux peuvent déclencher la navigation, des modales et des notifications en utilisant des fonctions de twenty-sdk : Voici un exemple qui utilise l’API hôte pour afficher une snackbar et fermer le panneau latéral après la fin d’une action :
src/front-components/archive-record.tsx

Stockage

localStorage et sessionStorage fonctionnent comme sur une page normale, avec l’API synchrone standard. Vos clés sont limitées à votre installation de l’application et à l’utilisateur connecté : aucune autre application ne peut les lire, et un autre utilisateur se connectant dans le même navigateur commence avec un stockage vide. Les valeurs écrites dans localStorage restent sur l’appareil entre les rechargements ; sessionStorage dure pendant la session du navigateur.
src/front-components/note-draft.tsx
Twenty stocke les valeurs pour le compte de votre application, de sorte qu’une écriture est appliquée localement immédiatement et sauvegardée en arrière-plan. Les lectures ne bloquent jamais sur l’hôte. Rien n’est synchronisé : les valeurs ne suivent pas l’utilisateur vers un autre navigateur ou une autre machine. Utilisez donc le magasin clé-valeur d’une fonction logique pour tout ce qui doit survivre à un changement d’appareil. Les écritures sont plafonnées, et chaque limite compte les caractères plutôt que les octets : les clés comportent au maximum 512 caractères, une valeur unique au maximum 262 144 caractères, et chaque stockage au maximum 1 048 576 caractères par application et par utilisateur. Une écriture qui dépasse une limite lève une QuotaExceededError, comme avec l’API du navigateur.

Travailler avec plusieurs enregistrements

Utilisez useSelectedRecordIds() pour gérer plusieurs enregistrements sélectionnés. C’est utile pour les opérations groupées :
src/front-components/bulk-export.tsx
Affichez-la avec un élément de menu de commande limité aux sélections d’enregistrements :
src/command-menu-items/bulk-export.command-menu-item.ts

Ressources publiques

Les composants frontaux peuvent accéder aux fichiers du répertoire public/ de l’application à l’aide de getPublicAssetUrl :
Voir la section sur les ressources publiques pour plus de détails.

Partage des dépendances entre les composants frontaux

Par défaut, chaque composant frontal contient sa propre copie des bibliothèques qu’il importe, donc une application avec cinq composants envoie React cinq fois. Déclarer les dépendances partagées dans le package de votre application. son pour construire ces bibliothèques une fois et que chaque composant de l’application les charge à partir d’un fichier mis en cache:
package.json
Chaque composant importe ensuite ses dépendances exactement comme avant, rien ne change dans le code de votre composant:
src/front-components/counter.tsx
Quelques points à connaître :
  • Un lot de dépendances partagées par application. Le bundle est construit à partir des propres dépendances de votre application, de sorte que vous gardez le contrôle total des versions que vous envoyez.
  • Lister les spécifications exactes que vous importez. vingt-ui/input et vingt-ui/display sont deux entrées; un nom de paquet seul ne couvre pas ses sous-chemins. La liste react couvre automatiquement react/jsx-runtime.
  • Partagez react-dom/client avec react. Chaque composant est rendu via createRoot, donc le laisser dehors signifie que chaque composant contient toujours React DOM.
  • **Le bundle est mis en cache. * Il est servi sous une URL de hachage de contenu avec une cache immuable de longue durée, ainsi il est téléchargé une fois et réutilisé dans tous les composants de l’application jusqu’à ce que l’une de ses dépendances change.
  • Les composants qui n’importent aucun des paquets partagés ne le téléchargent jamais.

Stylisation

Les composants frontaux prennent en charge plusieurs approches de stylisation. Vous pouvez utiliser :
  • Styles en lignestyle={{ color: 'red' }}
  • Composants d’interface utilisateur Twenty — la bibliothèque de composants propre à Twenty ; voir Utilisation des composants d’interface utilisateur Twenty ci-dessous
  • Emotion — CSS-in-JS avec @emotion/react
  • Styled-components — modèles styled.div
  • Tailwind CSS — classes utilitaires
  • Toute bibliothèque CSS-in-JS compatible avec React

Utilisation des composants d’interface utilisateur Twenty

Twenty fournit sa bibliothèque de composants sous forme de package twenty-ui. Les composants frontaux peuvent l’utiliser pour les boutons, tags, pastilles de statut, chips, avatars, icônes, typographie et jetons de thème qui s’adaptent automatiquement aux thèmes clair et sombre de l’espace de travail.

Installation

Ajoutez le package à votre application, épinglé à la version livrée avec votre instance Twenty :
twenty-ui est intégré à votre composant front au moment du build, il n’a donc besoin d’être qu’une dépendance de votre application — il n’y a rien à configurer à l’exécution.

Importation de composants

Importez depuis le sous-chemin correspondant plutôt que depuis la racine du package, afin que seuls les composants que vous utilisez se retrouvent dans votre bundle :

Icônes

Importez des icônes individuelles depuis twenty-ui/icon :
Chaque icône nommée bénéficie du tree-shaking ; en importer quelques-unes n’ajoute donc que très peu à votre bundle. Évitez IconsProvider, useIcons et iconsState — ils intègrent l’ensemble complet d’icônes Tabler (plusieurs Mo).

Thématisation et jetons de thème

Les composants Twenty UI correspondent automatiquement au thème clair et sombre de l’espace de travail — le moteur de rendu applique le jeu de couleurs actif sur l’hôte, et les composants résolvent leurs couleurs par rapport à celui-ci. Pour utiliser les mêmes jetons de design dans vos propres styles inline, appelez le hook useTheme(). Il renvoie les jetons de thème de Twenty (espacement, couleurs, rayons, polices) connectés au thème actif, sans qu’aucune configuration de ThemeProvider ne soit nécessaire dans votre composant :
Comme useTheme() est un hook, vous lisez les jetons à l’intérieur du corps du composant, de sorte que les valeurs reflètent toujours le thème en cours. La même table de jetons est également exportée en tant que constante themeCssVariables, mais privilégiez useTheme() dans les composants front — une constante au niveau du module qui déréférence themeCssVariables peut être indéfinie pendant l’extraction du manifeste de l’application. Pour bifurquer explicitement selon le jeu de couleurs actif, lisez-le avec useColorScheme() depuis twenty-sdk/front-component, qui renvoie 'light' ou 'dark'.

Limitations actuelles

Les composants Front sont en cours de développement actif. Le rendu, le style, la gestion des événements, la mesure des éléments et le stockage du navigateur fonctionnent bien. Tout ce qui va au-delà de ces éléments (appeler une méthode du DOM sur une ref, observer les redimensionnements d’éléments, créer un portail en dehors de votre arbre) est manquant ou incomplet aujourd’hui, et la plupart de ces opérations échouent silencieusement : aucune exception, et aucune erreur TypeScript non plus, puisque l’échafaudage est typé par rapport au DOM complet du navigateur. Si l’un de ces points vous bloque, ouvrez un ticket afin qu’il soit priorisé.

Disposition et mesure

Les éléments peuvent se mesurer eux-mêmes : l’hôte reflète la géométrie dans le bac à sable, de sorte que les lectures sont traitées localement mais peuvent avoir jusqu’à une image de retard, et la première lecture d’un élément jamais mesuré renvoie des zéros. Après l’écriture, relisez dans un callback requestAnimationFrame ou dans un effet. Le positionnement via getBoundingClientRect fonctionne désormais, mais tout ce qui surveille les changements de taille via ResizeObserver (le ResponsiveContainer de recharts, autoUpdate de Floating UI) ne fonctionne toujours pas. Préférez de toute façon le CSS pour la mise en page : votre feuille de style atteint la vraie page, donc flexbox, grid, aspect-ratio, clamp() et @container se comportent normalement, sans latence de trame.
requestAnimationFrame, fetch, setTimeout et queueMicrotask fonctionnent sans le préfixe window.. Seuls window.requestAnimationFrame(...) et les fonctions similaires lèvent une exception.

Accès au DOM

Une ref vous donne un élément du bac à sable, pas un HTMLElement. Cet écart lié au portail est la raison pour laquelle les popovers de Radix, Headless UI, MUI et react-select ne rendent rien par défaut. La plupart acceptent une prop de conteneur ; faites-la pointer vers un élément que vous avez rendu.

Événements

La souris, le pointeur, le toucher, le glisser, le clavier, le focus, input/change/submit, scroll/wheel/contextmenu et animationend/transitionend sont transmis à l’hôte, plus quelques événements propres à chaque élément : load/error sur img, le presse-papiers et la composition sur input/textarea, les médias sur video/audio, toggle sur details/dialog. Tout le reste (onAuxClick, onSelect, onInvalid, onReset, onAnimationStart, la capture de pointeur, onLoad sur img) est ignoré sans avertissement. document.addEventListener() et window.addEventListener() s’enregistrent sans erreur mais ne se déclenchent jamais, c’est pourquoi un glisser-déposer s’arrête dès que le pointeur quitte l’élément sur lequel il a commencé. event.preventDefault() ne traverse pas non plus ; l’envoi de formulaire, dragover/drop et les clics sur les liens sont déjà protégés pour vous.

Attributs et styles

Chaque élément transfère ses propres propriétés au DOM hôte (href sur a, src/alt sur img, value/placeholder/disabled sur input, etc.), plus un ensemble commun sur chaque élément : id, className, style, title, tabIndex, role, draggable et tout attribut aria-* / data-* (avec tiret, donc ariaLabel est ignoré). Tout ce qui sort de ce cadre est silencieusement supprimé, donc exprimez l’état personnalisé sous forme de data-*. Le CSS du composant, qu’il provienne de import './styles.css', de CSS-in-JS ou d’un élément style, est injecté dans le head de la page hôte sans portée. Ainsi, les noms de classes entrent en collision avec ceux de Twenty (préfixez-les, et n’écrivez jamais de div { ... } sélecteurs), et @media correspond à la fenêtre du navigateur plutôt qu’à votre widget (utilisez @container avec votre propre container-type). Les props style en ligne ne sont pas affectées.

Stockage et réseau

localStorage et sessionStorage sont fournis par Twenty plutôt que par le navigateur : le composant s’exécute dans un worker avec une origine opaque, donc l’hôte stocke les valeurs pour le compte de votre application. Voir stockage pour leur portée et leurs limites. IndexedDB, les cookies, l’API Cache et BroadcastChannel restent indisponibles. Pour conserver l’état entre les appareils, appelez une fonction logique et utilisez son magasin clé-valeur. fetch fonctionne, avec quelques réserves :
  • Les appels à l’API Twenty et aux routes de votre application sont proxifiés par l’hôte, donc privilégiez RestApiClient. Sur les appels proxifiés, AbortSignal et les autres options RequestInit sont ignorées, et seuls les corps string et URLSearchParams sont pris en charge.
  • Les autres origines quittent le bac à sable avec Origin: null, donc une API tierce répond uniquement si elle envoie Access-Control-Allow-Origin: *. Appelez-la plutôt depuis une fonction logique.
  • fetch('/rest/people') n’est jamais associé à l’API Twenty, car le bac à sable n’a pas d’URL de page pour résoudre un chemin relatif.

Capture de médias

navigator.mediaDevices.getUserMedia() et MediaRecorder fonctionnent à l’intérieur des composants front-end grâce à des polyfills de sandbox, de sorte que le code d’enregistrement standard s’exécute sans modification et que MediaRecorder.isTypeSupported indique si les combinaisons courantes de conteneur/codec sont prises en charge. Des objets de contraintes getUserMedia détaillés sont acceptés mais non transmis — l’hôte capture avec ses valeurs par défaut pour les types demandés — et une seule capture peut être active à la fois entre les applications. Stockez un Blob enregistré avec la fonction hôte uploadFile.

Autres lacunes

  • Contenu de fichier. Un input de type file fournit uniquement à votre gestionnaire les métadonnées du fichier, pas les octets, donc FileReader n’est pas disponible. Pour téléverser un Blob dont votre code dispose déjà — par exemple celui produit par MediaRecorder — utilisez la fonction hôte uploadFile.
  • Charges utiles de glisser-déposer. Les événements de glisser-déposer se déclenchent, mais event.dataTransfer est undefined.
  • Modules natifs Node. fs, path et node:crypto font échouer la construction, donc déplacez ce travail dans une fonction logique. Web Crypto, fetch, TextEncoder et URL sont disponibles.
  • iframe est toujours à nouveau placé dans un bac à sable sans allow-same-origin, donc une intégration qui repose sur sa propre session s’affiche comme déconnectée. Il n’a pas non plus de onLoad.