cdeebb1a18
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
772 lines
43 KiB
Plaintext
772 lines
43 KiB
Plaintext
---
|
||
title: Composants frontaux
|
||
description: Créez des composants React qui s’affichent dans l’interface utilisateur (UI) de Twenty avec une isolation en bac à sable.
|
||
icon: window-maximize
|
||
---
|
||
|
||
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.
|
||
|
||
<Warning>
|
||
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](#current-limitations).
|
||
</Warning>
|
||
|
||
## 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](/l/fr/developers/extend/apps/layout/page-layouts). 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()`](#custom-settings-component), 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/fr/developers/extend/apps/layout/command-menu-items)** — 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](/l/fr/developers/extend/apps/layout/page-layouts)** — le place sur la page de détails d'un enregistrement ou sur un tableau de bord.
|
||
* **Le définir avec [`defineSettingsFrontComponent()`](#custom-settings-component)** — 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`](/l/fr/developers/extend/apps/layout/command-menu-items), afin qu'il apparaisse comme bouton d'action rapide dans le coin supérieur droit de la page :
|
||
|
||
```tsx src/front-components/hello-world.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
|
||
const HelloWorld = () => {
|
||
return (
|
||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||
<h1>Hello from my app!</h1>
|
||
<p>This component renders inside Twenty.</p>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||
name: 'hello-world',
|
||
description: 'A simple front component',
|
||
component: HelloWorld,
|
||
});
|
||
```
|
||
|
||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||
|
||
export default defineCommandMenuItem({
|
||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||
shortLabel: 'Hello',
|
||
label: 'Hello World',
|
||
isPinned: true,
|
||
availabilityType: 'GLOBAL',
|
||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||
});
|
||
```
|
||
|
||
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 :
|
||
|
||
<div style={{textAlign: 'center'}}>
|
||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Bouton d'action rapide dans le coin supérieur droit" />
|
||
</div>
|
||
|
||
Cliquez dessus pour afficher le composant en ligne.
|
||
|
||
## Champs de configuration
|
||
|
||
| Champ | Obligatoire | Description |
|
||
| --------------------- | ----------- | -------------------------------------------------------------------------------- |
|
||
| `universalIdentifier` | Oui | ID unique et stable pour ce composant |
|
||
| `component` | Oui | Une fonction de composant React |
|
||
| `name` | Non | Nom d'affichage |
|
||
| `description` | Non | Description de ce que fait le composant |
|
||
| `isHeadless` | Non | Dé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](/l/fr/developers/extend/apps/layout/page-layouts) 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](#configuration-fields) (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.
|
||
|
||
```tsx src/front-components/app-settings.tsx
|
||
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
|
||
|
||
const AppSettings = () => {
|
||
return (
|
||
<div style={{ padding: '20px' }}>
|
||
<h2>My app settings</h2>
|
||
{/* render your own configuration UI here */}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default defineSettingsFrontComponent({
|
||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||
name: 'app-settings',
|
||
description: "Custom UI for the app's Settings tab",
|
||
component: AppSettings,
|
||
});
|
||
```
|
||
|
||
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.
|
||
|
||
```tsx src/front-components/sync-tracker.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||
import { useEffect } from 'react';
|
||
|
||
const SyncTracker = () => {
|
||
const [recordId] = useSelectedRecordIds();
|
||
|
||
useEffect(() => {
|
||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||
}, [recordId]);
|
||
|
||
return null;
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '...',
|
||
name: 'sync-tracker',
|
||
description: 'Tracks record views silently',
|
||
isHeadless: true,
|
||
component: SyncTracker,
|
||
});
|
||
```
|
||
|
||
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 :
|
||
|
||
```tsx src/front-components/run-action.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { Command } from 'twenty-sdk/front-component';
|
||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||
|
||
const RunAction = () => {
|
||
const execute = async () => {
|
||
const client = new CoreApiClient();
|
||
|
||
await client.mutation({
|
||
createTask: {
|
||
__args: { data: { title: 'Created by my app' } },
|
||
id: true,
|
||
},
|
||
});
|
||
};
|
||
|
||
return <Command execute={execute} />;
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||
name: 'run-action',
|
||
description: 'Creates a task from the command menu',
|
||
component: RunAction,
|
||
isHeadless: true,
|
||
});
|
||
```
|
||
|
||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||
|
||
export default defineCommandMenuItem({
|
||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||
label: 'Run my action',
|
||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||
});
|
||
```
|
||
|
||
Et un exemple utilisant `CommandModal` pour demander une confirmation avant l'exécution :
|
||
|
||
```tsx src/front-components/delete-draft.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { CommandModal } from 'twenty-sdk/front-component';
|
||
|
||
const DeleteDraft = () => {
|
||
const execute = async () => {
|
||
// perform the deletion
|
||
};
|
||
|
||
return (
|
||
<CommandModal
|
||
title="Delete draft?"
|
||
subtitle="This action cannot be undone."
|
||
execute={execute}
|
||
confirmButtonText="Delete"
|
||
confirmButtonAccent="danger"
|
||
/>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||
name: 'delete-draft',
|
||
description: 'Deletes a draft with confirmation',
|
||
component: DeleteDraft,
|
||
isHeadless: true,
|
||
});
|
||
```
|
||
|
||
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 :
|
||
|
||
```tsx src/front-components/open-company-emails.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import {
|
||
CommandOpenSidePanelPage,
|
||
SidePanelPages,
|
||
useSelectedRecordIds,
|
||
} from 'twenty-sdk/front-component';
|
||
|
||
const OpenCompanyEmails = () => {
|
||
const selectedRecordIds = useSelectedRecordIds();
|
||
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
|
||
|
||
if (!recordId) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<CommandOpenSidePanelPage
|
||
page={SidePanelPages.ViewRecord}
|
||
recordId={recordId}
|
||
objectNameSingular="company"
|
||
tab="company-tab-emails"
|
||
resetNavigationStack={false}
|
||
/>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||
name: 'open-company-emails',
|
||
description: 'Opens the current company on its Emails tab',
|
||
component: OpenCompanyEmails,
|
||
isHeadless: true,
|
||
});
|
||
```
|
||
|
||
## 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](/l/fr/developers/extend/apps/logic/logic-functions) 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 :
|
||
|
||
```tsx src/front-components/sync-prs.tsx
|
||
import { RestApiClient } from 'twenty-client-sdk/rest';
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { Command } from 'twenty-sdk/front-component';
|
||
|
||
const SyncPrs = () => {
|
||
const execute = async () => {
|
||
await new RestApiClient().post('/s/github/fetch-prs', {
|
||
owner: 'twentyhq',
|
||
repo: 'twenty',
|
||
});
|
||
};
|
||
|
||
return <Command execute={execute} />;
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '...',
|
||
name: 'sync-prs',
|
||
description: 'Triggers the fetch-prs logic function',
|
||
isHeadless: true,
|
||
component: SyncPrs,
|
||
});
|
||
```
|
||
|
||
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 :
|
||
|
||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||
|
||
const handler = async (event: RoutePayload) => {
|
||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||
// ...fetch from GitHub and persist records...
|
||
return { ok: true };
|
||
};
|
||
|
||
export default defineLogicFunction({
|
||
universalIdentifier: '...',
|
||
name: 'fetch-prs',
|
||
handler,
|
||
httpRouteTriggerSettings: {
|
||
path: '/github/fetch-prs',
|
||
httpMethod: 'POST',
|
||
isAuthRequired: true,
|
||
},
|
||
});
|
||
```
|
||
|
||
<Note>
|
||
`TWENTY_APP_ACCESS_TOKEN` est injecté automatiquement — voir [Variables d’application](#application-variables). 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.
|
||
</Note>
|
||
|
||
### 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`.
|
||
|
||
| Méthode | Description |
|
||
| --------------------------------- | ------------------------------------------------------------------------------- |
|
||
| `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 |
|
||
| `resolveUrl(path, options?)` | Résout un chemin vers son URL complète sans envoyer de requête (pour les liens) |
|
||
|
||
`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 :
|
||
|
||
```ts
|
||
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é :
|
||
|
||
```tsx
|
||
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
|
||
|
||
const client = new RestApiClient();
|
||
|
||
try {
|
||
const people = await client.get('/rest/people', {
|
||
query: { limit: 10 },
|
||
});
|
||
} catch (error) {
|
||
if (error instanceof RestApiClientError) {
|
||
console.error(error.status, error.body);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 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 :
|
||
|
||
```tsx src/front-components/record-info.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import {
|
||
useUserId,
|
||
useSelectedRecordIds,
|
||
useFrontComponentId,
|
||
} from 'twenty-sdk/front-component';
|
||
|
||
const RecordInfo = () => {
|
||
const userId = useUserId();
|
||
const [recordId] = useSelectedRecordIds();
|
||
const componentId = useFrontComponentId();
|
||
|
||
return (
|
||
<div>
|
||
<p>User: {userId}</p>
|
||
<p>Record: {recordId ?? 'No record context'}</p>
|
||
<p>Component: {componentId}</p>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
||
name: 'record-info',
|
||
component: RecordInfo,
|
||
});
|
||
```
|
||
|
||
Hooks disponibles :
|
||
|
||
| Hook | Renvoie | Description |
|
||
| --------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------- |
|
||
| `useUserId()` | `string` ou `null` | L'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 `null` | **Obsolète.** Utilisez `useSelectedRecordIds()` à la place |
|
||
| `useFrontComponentId()` | `string` | L'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)` | variable | Accédez au contexte d'exécution complet avec une fonction sélecteur |
|
||
|
||
## Variables d'application
|
||
|
||
Les variables d'application définies dans [`defineApplication()`](/l/fr/developers/extend/apps/config/application) avec `isSecret: false` sont disponibles dans les composants front via l'utilitaire `getApplicationVariable` :
|
||
|
||
```tsx src/front-components/greeting.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||
|
||
const Greeting = () => {
|
||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||
|
||
return <p>Hello, {recipientName}!</p>;
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '...',
|
||
name: 'greeting',
|
||
component: Greeting,
|
||
});
|
||
```
|
||
|
||
<Warning>
|
||
Les variables secrètes (`isSecret: true`) ne sont **pas** exposées aux composants front. Elles sont uniquement disponibles dans les [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions), qui s'exécutent côté serveur. Cela empêche l’envoi au navigateur de valeurs sensibles comme les clés d’API.
|
||
</Warning>
|
||
|
||
`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](/l/fr/developers/extend/apps/config/application#variable-types).
|
||
|
||
Les variables système suivantes sont toujours disponibles via `process.env` :
|
||
|
||
| Variable | Description |
|
||
| ------------------------- | --------------------------------------------------------- |
|
||
| `TWENTY_API_URL` | URL de base de l’API principale de Twenty |
|
||
| `TWENTY_APP_ACCESS_TOKEN` | Jeton de courte durée limité au rôle de votre application |
|
||
|
||
### `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 :
|
||
|
||
```ts
|
||
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
|
||
```
|
||
|
||
## 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` :
|
||
|
||
| Fonction | Description |
|
||
| ----------------------------------------------- | ---------------------------------------------- |
|
||
| `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 :
|
||
|
||
```tsx src/front-components/archive-record.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||
|
||
const ArchiveRecord = () => {
|
||
const [recordId] = useSelectedRecordIds();
|
||
|
||
const handleArchive = async () => {
|
||
const client = new CoreApiClient();
|
||
|
||
await client.mutation({
|
||
updateTask: {
|
||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||
id: true,
|
||
},
|
||
});
|
||
|
||
await enqueueSnackbar({
|
||
message: 'Record archived',
|
||
variant: 'success',
|
||
});
|
||
|
||
await closeSidePanel();
|
||
};
|
||
|
||
return (
|
||
<div style={{ padding: '20px' }}>
|
||
<p>Archive this record?</p>
|
||
<button onClick={handleArchive}>Archive</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||
name: 'archive-record',
|
||
description: 'Archives the current record',
|
||
component: ArchiveRecord,
|
||
});
|
||
```
|
||
|
||
### Travailler avec plusieurs enregistrements
|
||
|
||
Utilisez `useSelectedRecordIds()` pour gérer plusieurs enregistrements sélectionnés. C'est utile pour les opérations groupées :
|
||
|
||
```tsx src/front-components/bulk-export.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||
|
||
const BulkExport = () => {
|
||
const selectedRecordIds = useSelectedRecordIds();
|
||
|
||
const handleExport = async () => {
|
||
const client = new CoreApiClient();
|
||
|
||
for (const recordId of selectedRecordIds) {
|
||
await client.mutation({
|
||
updateTask: {
|
||
__args: { id: recordId, data: { exported: true } },
|
||
id: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
await enqueueSnackbar({
|
||
message: `Exported ${selectedRecordIds.length} records`,
|
||
variant: 'success',
|
||
});
|
||
|
||
await closeSidePanel();
|
||
};
|
||
|
||
return (
|
||
<div style={{ padding: '20px' }}>
|
||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||
<button onClick={handleExport}>Export</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||
name: 'bulk-export',
|
||
description: 'Export selected records',
|
||
component: BulkExport,
|
||
});
|
||
```
|
||
|
||
Affichez-la avec un [élément de menu de commande](/l/fr/developers/extend/apps/layout/command-menu-items) limité aux sélections d'enregistrements :
|
||
|
||
```ts src/command-menu-items/bulk-export.command-menu-item.ts
|
||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||
|
||
export default defineCommandMenuItem({
|
||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||
label: 'Bulk Export',
|
||
availabilityType: 'RECORD_SELECTION',
|
||
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||
});
|
||
```
|
||
|
||
## Ressources publiques
|
||
|
||
Les composants frontaux peuvent accéder aux fichiers du répertoire `public/` de l'application à l'aide de `getPublicAssetUrl` :
|
||
|
||
```tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||
|
||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '...',
|
||
name: 'logo',
|
||
component: Logo,
|
||
});
|
||
```
|
||
|
||
Voir la [section sur les ressources publiques](/l/fr/developers/extend/apps/config/public-assets) pour plus de détails.
|
||
|
||
## Stylisation
|
||
|
||
Les composants frontaux prennent en charge plusieurs approches de stylisation. Vous pouvez utiliser :
|
||
|
||
* **Styles en ligne** — `style={{ color: 'red' }}`
|
||
* **Composants d’interface utilisateur Twenty** — la bibliothèque de composants propre à Twenty ; voir [Utilisation des composants d’interface utilisateur Twenty](#using-twenty-ui-components) 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`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). 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 :
|
||
|
||
```bash
|
||
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-chemin | Ce qu’il exporte |
|
||
| --------------------------- | -------------------------------------------------------- |
|
||
| `twenty-ui/input` | `Button` et champs de formulaire |
|
||
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar`, et plus encore |
|
||
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info`, et plus encore |
|
||
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label`, et plus encore |
|
||
| `twenty-ui/icon` | Composants `Icon*` (par exemple `IconCheck`) |
|
||
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
|
||
|
||
```tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { Status, Tag } from 'twenty-ui/data-display';
|
||
import { Button } from 'twenty-ui/input';
|
||
|
||
const StyledWidget = () => {
|
||
return (
|
||
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
||
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
||
<Tag text="Active" color="green" />
|
||
<Status color="green" text="Online" />
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
||
name: 'styled-widget',
|
||
component: StyledWidget,
|
||
});
|
||
```
|
||
|
||
### Icônes
|
||
|
||
Importez des icônes individuelles depuis `twenty-ui/icon` :
|
||
|
||
```tsx
|
||
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 :
|
||
|
||
```tsx
|
||
import { useTheme } from 'twenty-ui/theme-constants';
|
||
|
||
const Card = () => {
|
||
const theme = useTheme();
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
padding: theme.spacing[4],
|
||
background: theme.background.secondary,
|
||
color: theme.font.color.primary,
|
||
}}
|
||
>
|
||
Themed card
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
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 et la gestion des événements fonctionnent bien. Tout ce qui va *au-delà* du rendu (mesurer un élément, appeler une méthode du DOM sur une ref, créer un portail en dehors de votre arbre, accéder au stockage du navigateur) 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](https://github.com/twentyhq/twenty/issues/new/choose) afin qu’il soit priorisé.
|
||
|
||
### Disposition et mesure
|
||
|
||
Rien ne peut encore se mesurer soi-même.
|
||
|
||
| API | Ce qui se passe |
|
||
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||
| `getBoundingClientRect()`, `getClientRects()` | Lève une exception |
|
||
| `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Silencieusement `undefined`, donc `width ?? 0` donne `0` et `width > 600` est toujours faux |
|
||
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (les gardes `typeof` fonctionnent) |
|
||
| `window.matchMedia()`, `window.getComputedStyle()` | Lève une exception |
|
||
| `window.innerWidth`, `innerHeight`, `devicePixelRatio` | Silencieusement `undefined` |
|
||
| `new MutationObserver(fn)` | Se construit, puis `.observe()` lève une exception |
|
||
|
||
Ainsi, recharts `ResponsiveContainer`, Floating UI / Popper, la virtualisation de liste et le redimensionnement par glisser-déposer ne fonctionnent pas encore. Réalisez plutôt la mise en page en CSS : votre feuille de style atteint la vraie page, donc flexbox, grid, `aspect-ratio`, `clamp()` et `@container` se comportent tous normalement.
|
||
|
||
<Note>
|
||
`requestAnimationFrame`, `fetch`, `setTimeout` et `queueMicrotask` fonctionnent sans le préfixe `window.`. Seuls `window.requestAnimationFrame(...)` et les fonctions similaires lèvent une exception.
|
||
</Note>
|
||
|
||
### Accès au DOM
|
||
|
||
Une `ref` vous donne un élément du bac à sable, pas un `HTMLElement`.
|
||
|
||
| Ce que vous écrivez | Ce qui se passe | À utiliser à la place |
|
||
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Lève une exception | Composants contrôlés ; lisez les valeurs à partir de `event.target` |
|
||
| `element.classList.add(...)` | Lève une exception (`classList` est `undefined`) | Construisez vous-même la chaîne `className` |
|
||
| `document.getElementById()`, `getElementsByClassName()`, `createTreeWalker()` | Lève une exception | `querySelector()` / `querySelectorAll()`, qui fonctionnent |
|
||
| `document.activeElement` | Toujours `undefined` | Suivez le focus avec `onFocus` / `onBlur` |
|
||
| `\<canvas>` | Ne rend rien, pas d’erreur | SVG, ou dessinez hors écran et affichez une `<img src={dataUrl}>` |
|
||
| `createPortal(node, document.body)` | Ne rend rien, tandis que `isConnected` signale un succès | Superpositions en ligne avec `position: absolute`, ou passez à la bibliothèque votre propre élément conteneur |
|
||
|
||
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-déposer, le clavier, le focus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` et `animationend`/`transitionend` sont transmis à l’hôte, plus quelques événements spécifiques à 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`, `sessionStorage`, IndexedDB, les cookies, l’API Cache et `BroadcastChannel` ne sont pas disponibles, puisque le composant s’exécute dans un worker avec une origine opaque. Pour conserver l’état, appelez une [fonction logique](/l/fr/developers/extend/apps/logic/logic-functions) et utilisez son [magasin clé-valeur](/l/fr/developers/extend/apps/logic/key-value-store).
|
||
|
||
`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`](#calling-the-twenty-rest-api). 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.
|
||
|
||
### Autres lacunes
|
||
|
||
* **Contenu de fichier.** `<input type="file">` fournit uniquement à votre gestionnaire les métadonnées du fichier, pas les octets, donc `FileReader` et les téléversements ne sont pas encore possibles.
|
||
* **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](/l/fr/developers/extend/apps/logic/logic-functions). 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`.
|