2b3b2362db
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
546 lines
23 KiB
Plaintext
546 lines
23 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 est isolé mais s'affiche nativement dans la page, pas dans un 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](/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.
|
||
|
||
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/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.
|
||
|
||
## 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',
|
||
icon: 'IconBolt',
|
||
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 dev --once`), 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.
|
||
|
||
## 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 { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||
import { useEffect } from 'react';
|
||
|
||
const SyncTracker = () => {
|
||
const recordId = useRecordId();
|
||
|
||
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/command` :
|
||
|
||
* **`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 spécifique du panneau latéral. Props : `page`, `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/command';
|
||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||
|
||
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',
|
||
icon: 'IconPlayerPlay',
|
||
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/command';
|
||
|
||
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é (sandboxed), 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 exposée sous le point de terminaison `/s/` à `${TWENTY_API_URL}/s\<path>`. Votre composant front appelle cette route avec le `RestApiClient` de `twenty-client-sdk/rest`, qui s’authentifie avec le `TWENTY_APP_ACCESS_TOKEN` que Twenty injecte dans le worker.
|
||
|
||
Le `RestApiClient` est conçu exactement pour cela. Il lit `TWENTY_API_URL` et `TWENTY_APP_ACCESS_TOKEN` depuis l’environnement du worker, ajoute l’en-tête `Authorization: Bearer`, sérialise et analyse le JSON, et déclenche une `RestApiClientError` lorsque le jeton ou l’URL est manquant ou que la réponse n’est pas de type 2xx — afin que vous n’ayez pas à réimplémenter ce boilerplate dans chaque composant.
|
||
|
||
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 { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { Command } from 'twenty-sdk/command';
|
||
import { RestApiClient } from 'twenty-client-sdk/rest';
|
||
|
||
const SyncPrs = () => {
|
||
const execute = async () => {
|
||
const client = new RestApiClient();
|
||
|
||
await client.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 au client est le chemin public de la route — la propriété `httpRouteTriggerSettings.path` de la fonction logique, préfixée par `/s`. Conservez `isAuthRequired: true` ; le client fournit le jeton d’accès de l’application que Twenty émet pour votre composant :
|
||
|
||
```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_API_URL` et `TWENTY_APP_ACCESS_TOKEN` sont injectés 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>
|
||
|
||
### Référence de `RestApiClient`
|
||
|
||
Importez `RestApiClient` depuis `twenty-client-sdk/rest`. Il appartient à la même famille de clients que `CoreApiClient` et `MetadataApiClient`, mais cible les routes HTTP de votre application au lieu de l’API GraphQL.
|
||
|
||
| 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 |
|
||
|
||
`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://api.example.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 prs = await client.get('/s/github/fetch-prs', {
|
||
query: { state: 'open' },
|
||
});
|
||
} 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,
|
||
useRecordId,
|
||
useFrontComponentId,
|
||
} from 'twenty-sdk/front-component';
|
||
|
||
const RecordInfo = () => {
|
||
const userId = useUserId();
|
||
const recordId = useRecordId();
|
||
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>
|
||
|
||
Les variables système suivantes sont toujours disponibles via `process.env` :
|
||
|
||
| Variable | Description |
|
||
| ------------------------- | --------------------------------------------------------- |
|
||
| `TWENTY_API_URL` | URL de base de l’API Twenty |
|
||
| `TWENTY_APP_ACCESS_TOKEN` | Jeton 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` :
|
||
|
||
| 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 { useRecordId } from 'twenty-sdk/front-component';
|
||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||
|
||
const ArchiveRecord = () => {
|
||
const recordId = useRecordId();
|
||
|
||
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, numberOfSelectedRecords } from 'twenty-sdk/define';
|
||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||
|
||
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,
|
||
command: {
|
||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||
label: 'Bulk Export',
|
||
availabilityType: 'RECORD_SELECTION',
|
||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||
},
|
||
});
|
||
```
|
||
|
||
## 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 Twenty UI** — à importer depuis `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, etc.)
|
||
* **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
|
||
|
||
```tsx
|
||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||
|
||
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,
|
||
});
|
||
```
|