Passer au contenu principal
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.

Où les composants frontaux peuvent être utilisés

Les composants frontaux peuvent s’afficher à deux 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.
Un composant frontal seul n’est pas accessible depuis l’interface utilisateur — vous devez l’exposer. Les deux 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.

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
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,
});
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',
});
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 :
Bouton d'action rapide dans le coin supérieur droit
Cliquez dessus pour afficher le composant en ligne.

Champs de configuration

ChampObligatoireDescription
universalIdentifierOuiID unique et stable pour ce composant
componentOuiUne fonction de composant React
nameNonNom d’affichage
descriptionNonDescription de ce que fait le composant
isHeadlessNonDéfinir sur true si le composant n’a pas d’interface visible (voir ci-dessous)

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.

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
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,
});
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, 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
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,
});
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',
});
Et un exemple utilisant CommandModal pour demander une confirmation avant l’exécution :
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,
});

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. Twenty injecte dans le worker l’URL de base à partir de laquelle vos fonctions sont servies sous la forme de TWENTY_FUNCTIONS_URL, ainsi que le TWENTY_APP_ACCESS_TOKEN qui authentifie l’appel. Il n’existe pas encore de client SDK dédié pour invoquer vos propres fonctions, donc appelez-les avec un simple fetch :
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> — c’est exactement ce à quoi TWENTY_FUNCTIONS_URL correspond. 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.
L’ancienne route de fonction /s/ est obsolète et sera désactivée le 2026-07-24. Utilisez plutôt TWENTY_FUNCTIONS_URL (ci-dessus), et migrez toutes les URL /s/ en dur avant cette date. La route /s/ reste disponible pour l’auto-hébergement.
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
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';

const SyncPrs = () => {
  const execute = async () => {
    await fetch(`${process.env.TWENTY_FUNCTIONS_URL}/github/fetch-prs`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.TWENTY_APP_ACCESS_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ 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,
});
Le chemin ajouté à TWENTY_FUNCTIONS_URL correspond au httpRouteTriggerSettings.path de la fonction logique. 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
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,
  },
});
TWENTY_FUNCTIONS_URL et TWENTY_APP_ACCESS_TOKEN sont injectés 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 lire ou écrire des enregistrements Twenty depuis un composant frontal, utilisez RestApiClient depuis twenty-client-sdk/rest. Il appartient à la même famille de clients que CoreApiClient et MetadataApiClient, mais cible l’API REST de Twenty (/rest/...) plutôt que l’API GraphQL, en lisant son URL de base depuis TWENTY_API_URL.
MéthodeDescription
get(path, options?)Envoie une requête GET
post(path, body?, options?)Envoie une requête POST
put(path, body?, options?)Envoie une requête PUT
patch(path, body?, options?)Envoie une requête PATCH
delete(path, options?)Envoie une requête DELETE
request(method, path, options?)Requête générique avec n’importe quelle méthode HTTP
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 :
const client = new RestApiClient({
  baseUrl: 'https://myworkspace.twenty.com',
  token: 'my-token',
});
Les requêtes ayant échoué lèvent une erreur RestApiClientError exposant status, statusText, url et le body analysé :
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);
  }
}

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
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,
});
Hooks disponibles :
HookRenvoieDescription
useUserId()string ou nullL’ID de l’utilisateur actuel
useSelectedRecordIds()string[]Tous les ID des enregistrements sélectionnés (tableau vide si aucun n’est sélectionné)
useRecordId()string ou nullObsolète. Utilisez useSelectedRecordIds() à la place
useFrontComponentId()stringL’ID de cette instance de composant
useColorScheme()'light' ou 'dark'Schéma de couleur actif de l’interface hôte (System est déjà résolu)
useFrontComponentExecutionContext(selector)variableAccédez au contexte d’exécution complet avec une fonction sélecteur

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
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,
});
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 :
VariableDescription
TWENTY_FUNCTIONS_URLURL de base à partir de laquelle les fonctions de logique HTTP de votre application sont servies
TWENTY_API_URLURL de base de l’API principale de Twenty
TWENTY_APP_ACCESS_TOKENJeton de courte durée limité au rôle de votre application

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 :
FonctionDescription
navigate(to, params?, queryParams?, options?)Naviguer vers une page de l’application
openSidePanelPage(params)Ouvrir un panneau latéral
closeSidePanel()Fermer le panneau latéral
openCommandConfirmationModal(params)Afficher une boîte de dialogue de confirmation
enqueueSnackbar(params)Afficher une notification toast
unmountFrontComponent()Démonter le composant
updateProgress(progress)Mettre à jour un indicateur de progression
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
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,
});

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
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,
});
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
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',
});

Ressources publiques

Les composants frontaux peuvent accéder aux fichiers du répertoire public/ de l’application à l’aide de getPublicAssetUrl :
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,
});
Voir la section sur les ressources publiques pour plus de détails.

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 :
yarn add twenty-ui@1.0.0-alpha.1
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 :
Sous-cheminCe qu’il exporte
twenty-ui/inputButton et champs de formulaire
twenty-ui/data-displayTag, Status, Chip, Avatar, et plus encore
twenty-ui/feedbackCallout, Banner, Info, et plus encore
twenty-ui/typographyH1Title, H2Title, H3Title, Label, et plus encore
twenty-ui/iconComposants Icon* (par exemple IconCheck)
twenty-ui/theme-constantsThemeProvider, themeCssVariables
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,
});

Icônes

Importez des icônes individuelles depuis twenty-ui/icon :
import { IconBox, IconCheck } from '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 :
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>
  );
};
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'.