snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
---
title: Éléments du menu de commande
description: Exposez les composants frontaux comme actions rapides et entrées du menu de commande (Cmd+K) avec defineCommandMenuItem.
icon: terminal
---
Un **élément de menu de commande** est le lien entre l’utilisateur et un [front component](/l/fr/developers/extend/apps/layout/front-components). Il enregistre le composant dans le menu de commande (Cmd+K) de Twenty et, éventuellement, comme bouton d’action rapide épinglé dans le coin supérieur droit de la page.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Champs de configuration
| Champ | Obligatoire | Description |
| --------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Oui | ID unique et stable pour la commande |
| `label` | Oui | Libellé complet affiché dans le menu de commande (Cmd+K) |
| `frontComponentUniversalIdentifier` | Oui | L'`universalIdentifier` du composant frontal que cette commande ouvre |
| `shortLabel` | Non | Libellé plus court affiché sur le bouton d'action rapide épinglé |
| `icon` | Non | **Obsolète** — ignoré au profit de l’icône de l’application ; la build émet un avertissement si elle est définie |
| `isPinned` | Non | Lorsque `true`, affiche la commande comme un bouton d'action rapide dans le coin supérieur droit de la page |
| `availabilityType` | Non | Contrôle l’emplacement d’apparition de la commande : `'GLOBAL'` (toujours disponible), `'GLOBAL_OBJECT_CONTEXT'` (uniquement sur les pages avec un contexte d’objet — pages d’index et d’enregistrement), `'RECORD_SELECTION'` (uniquement lorsque des enregistrements sont sélectionnés) ou `'FALLBACK'` (affichée lorsqu’aucune autre commande ne correspond) |
| `availabilityObjectUniversalIdentifier` | Non | Restreint la commande aux pages d’un type d’objet spécifique (p. ex., uniquement sur les enregistrements « Company ») |
| `conditionalAvailabilityExpression` | Non | Une expression booléenne qui contrôle dynamiquement la visibilité (voir ci-dessous) |
## Commandes sans interface
Un élément de menu de commande associé à un [headless front component](/l/fr/developers/extend/apps/layout/front-components#headless-vs-non-headless) est la manière idiomatique de proposer une action en un clic — exécuter du code, naviguer, ou confirmer puis exécuter. La page Front Components couvre les [SDK Command components](/l/fr/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) qui gèrent le modèle action-et-démontage.
Un flux typique : un composant headless affiche `<Command execute={...} />` (voir [l’exemple complet](/l/fr/developers/extend/apps/layout/front-components#sdk-command-components)), et l’élément de menu de commande y pointe :
```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',
});
```
## Expressions de disponibilité conditionnelle
Le champ `conditionalAvailabilityExpression` vous permet de contrôler quand une commande est visible en fonction du contexte de la page actuelle. Importez des variables typées et des opérateurs depuis `twenty-sdk` pour construire des expressions :
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` implique déjà une sélection non vide — utilisez `numberOfSelectedRecords` uniquement pour des décomptes spécifiques (par exemple `>= 2`).
</Note>
### Variables de contexte
Elles représentent l’état actuel de la page :
| Variable | Type | Description |
| ------------------------------ | --------- | -------------------------------------------------------------------- |
| `pageType` | `string` | Type de page actuel (p. ex. `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Indique si le composant est rendu dans un panneau latéral |
| `numberOfSelectedRecords` | `number` | Nombre d'enregistrements actuellement sélectionnés |
| `isSelectAll` | `boolean` | Indique si "tout sélectionner" est actif |
| `selectedRecords` | `array` | Les objets d'enregistrement sélectionnés |
| `favoriteRecordIds` | `array` | IDs des enregistrements ajoutés aux favoris |
| `objectPermissions` | `object` | Autorisations pour le type d'objet actuel |
| `targetObjectReadPermissions` | `object` | Autorisations de lecture pour l'objet cible |
| `targetObjectWritePermissions` | `object` | Autorisations d'écriture pour l'objet cible |
| `featureFlags` | `object` | Drapeaux de fonctionnalité actifs |
| `objectMetadataItem` | `object` | Métadonnées du type d'objet actuel |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Indique si la vue actuelle possède un filtre de suppression logique |
### Opérateurs
Combinez des variables en expressions booléennes :
| Opérateur | Description |
| ----------------------------------- | ---------------------------------------------------------------------- |
| `isDefined(value)` | `true` si la valeur n'est pas null/undefined |
| `isNonEmptyString(value)` | `true` si la valeur est une chaîne non vide |
| `includes(array, value)` | `true` si le tableau contient la valeur |
| `includesEvery(array, prop, value)` | `true` si la propriété de chaque élément inclut la valeur |
| `every(array, prop)` | `true` si la propriété est truthy sur chaque élément |
| `everyDefined(array, prop)` | `true` si la propriété est définie pour chaque élément |
| `everyEquals(array, prop, value)` | `true` si la propriété est égale à la valeur pour chaque élément |
| `some(array, prop)` | `true` si la propriété est truthy sur au moins un élément |
| `someDefined(array, prop)` | `true` si la propriété est définie sur au moins un élément |
| `someEquals(array, prop, value)` | `true` si la propriété est égale à la valeur sur au moins un élément |
| `someNonEmptyString(array, prop)` | `true` si la propriété est une chaîne non vide sur au moins un élément |
| `none(array, prop)` | `true` si la propriété est falsy sur chaque élément |
| `noneDefined(array, prop)` | `true` si la propriété est undefined sur chaque élément |
| `noneEquals(array, prop, value)` | `true` si la propriété n'est égale à la valeur sur aucun élément |

View File

@@ -0,0 +1,849 @@
---
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`.
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.
| 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 |
| `useTimelineActivityId()` | `string` ou `null` | L’ID de l’activité actuelle de la chronologie lors du rendu d’une ligne de chronologie personnalisée |
| `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 à courte durée de vie dont la portée correspond à l'intersection entre le rôle de la personne connectée et celui de votre application, de sorte qu'un composant ne peut jamais faire plus que la personne qui le consulte |
### `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 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Téléverse un `Blob` dans un champ FILES ; renvoie `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` ou `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }`. |
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,
});
```
### 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.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
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](/l/fr/developers/extend/apps/logic/key-value-store) 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 :
```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.
## 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:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Chaque composant importe ensuite ses dépendances exactement comme avant, rien ne change dans le code de votre composant:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
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 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, 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](https://github.com/twentyhq/twenty/issues/new/choose) 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.
| API | Ce qui se passe |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Fonctionne, depuis le miroir ; affecter `scrollTop` / `scrollLeft` est une opération nulle (no-op) |
| `getClientRects()`, `window.matchMedia()` | Lève une exception |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Fonctionnent, en rapportant le **viewport du navigateur** ; la taille propre de votre widget est `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Renvoie uniquement les styles inline de l’élément, jamais la cascade calculée de l’hôte |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (les gardes `typeof` fonctionnent) |
| `MutationObserver` | Fonctionne, y compris `subtree`, `attributeFilter`, les anciennes valeurs et `takeRecords()` |
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.
<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.createTreeWalker()` | Lève une exception | `querySelector()` / `querySelectorAll()` et `getElementById()` fonctionnent ; `getElementsByClassName()` fonctionne aussi, mais renvoie une collection statique, et non une `HTMLCollection` vivante |
| `document.activeElement` | Toujours `undefined` | Suivez le focus avec `onFocus` / `onBlur` |
| `canvas` | Ne rend rien, pas d’erreur | SVG, ou dessiner hors écran et afficher le résultat dans un élément `img` |
| `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, 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](#storage) 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](/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.
### 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](/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`.

View File

@@ -0,0 +1,46 @@
---
title: Éléments de menu de navigation
description: Ajoutez des entrées personnalisées à la barre latérale de l’espace de travail — des liens vers des vues enregistrées ou des URL externes.
icon: bars
---
Un **élément de menu de navigation** est une entrée dans la barre latérale gauche. Utilisez `defineNavigationMenuItem()` pour ajouter des liens personnalisés dans la barre latérale — généralement un par [vue](/l/fr/developers/extend/apps/layout/views) — ou pour pointer vers des URL externes.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
## Points clés
* `type` détermine la destination de l’élément de menu. Chaque type est associé à un champ d’identifiant spécifique :
| Type | Ce que cela fait | Champ obligatoire |
| ------------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `NavigationMenuItemType.VIEW` | Ouvre une vue enregistrée | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Ouvre une URL externe | `link` |
| `NavigationMenuItemType.FOLDER` | Regroupe les éléments imbriqués sous une étiquette | `name` (et les éléments enfants font référence au dossier via `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Ouvre la page d’index par défaut d’un objet | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Ouvre une mise en page autonome | `pageLayoutUniversalIdentifier` |
* `position` contrôle l’ordre dans la barre latérale.
* L’énumération contient également `NavigationMenuItemType.RECORD`, utilisé en interne pour les favoris d’enregistrements créés par l’utilisateur — il n’est pas utilisable depuis un manifeste d’application (il n’existe aucun champ pour référencer un enregistrement).
* `icon` et `color` sont facultatifs et personnalisent l’apparence de l’entrée.
* `folderUniversalIdentifier` est également disponible sur n’importe quel élément pour l’imbriquer dans un parent de type `FOLDER`.
<Note>
**Piège courant :** créer un objet sans élément de menu de navigation associé rend cet objet inatteignable depuis la barre latérale. À moins qu’il ne s’agisse d’un objet technique/interne, chaque objet personnalisé devrait avoir une entrée dans la barre latérale qui renvoie vers cet objet.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Vue d'ensemble
description: Placez votre application à l’intérieur de l’interface de Twenty — entrées de barre latérale, vues enregistrées, onglets de page d’enregistrement et composants React isolés.
icon: table-columns
---
La **couche de mise en page** d’une application Twenty est tout ce que l’utilisateur voit : où l’application apparaît dans la barre latérale, quelles vues de liste elle fournit, comment ses pages de détails d’enregistrement sont organisées et quels composants React personnalisés sont rendus à l’intérieur de ces pages.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Vues" icon="list" href="/l/fr/developers/extend/apps/layout/views">
`defineView` — configurations de listes enregistrées : colonnes visibles, filtres, groupes.
</Card>
<Card title="Éléments de menu de navigation" icon="bars" href="/l/fr/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — entrées de barre latérale pointant vers des vues ou des URL externes.
</Card>
<Card title="Mises en page" icon="table-columns" href="/l/fr/developers/extend/apps/layout/page-layouts">
`definePageLayout` et `definePageLayoutTab` — onglets et widgets sur la page de détails d’un enregistrement.
</Card>
<Card title="Composants frontaux" icon="window-maximize" href="/l/fr/developers/extend/apps/layout/front-components">
`defineFrontComponent` — composants React isolés rendus à l’intérieur de Twenty.
</Card>
<Card title="Éléments du menu de commande" icon="terminal" href="/l/fr/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — enregistre des composants frontaux comme entrées Cmd+K et actions rapides.
</Card>
</CardGroup>
## Où l’application apparaît
| Surface | Ce qu'il contrôle | Entité |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Barre latérale** | Une entrée personnalisée pointant vers une vue enregistrée ou une URL externe | `defineNavigationMenuItem` |
| **Liste des enregistrements** | Une configuration enregistrée pour un objet — colonnes visibles, ordre, filtres, groupes | `defineView` |
| **Page de détails de l’enregistrement** | Les onglets et widgets sur une page d’enregistrement (celle de votre propre objet ou une page standard) | `definePageLayout`, `definePageLayoutTab` |
| **À l’intérieur de n’importe lequel des éléments ci-dessus** | Un widget React personnalisé — boutons, formulaires, tableaux de bord, intégrations | `defineFrontComponent` |
| **Paramètres de l’application** | Une section de configuration personnalisée dans l’onglet Paramètres de l’application, au lieu de l’interface utilisateur par défaut des variables | `defineSettingsFrontComponent` |
| **Menu de commande (Cmd+K)** | Une action rapide épinglée ou une commande masquée | `defineCommandMenuItem` |
Les composants frontaux s’exécutent à l’intérieur d’un Web Worker isolé en utilisant Remote DOM — ils sont rendus *nativement* dans la page (et non dans une iframe), mais ne peuvent pas accéder directement à la page hôte ou au DOM. La communication avec Twenty se fait via une API hôte de passage de messages.
Elles sont encore en cours de développement actif : le bac à sable implémente un DOM partiel, donc les usages avancés peuvent échouer. Voir [Limitations actuelles](/l/fr/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Mises en page de la page
description: Personnalisez les pages de détail des enregistrements — onglets, widgets et emplacement de rendu des composants front-end — à l'aide de `definePageLayout` et `definePageLayoutTab`.
icon: table-columns
---
Une mise en page contrôle la façon dont la page de détail d'un enregistrement est organisée : quels onglets apparaissent et quels widgets ils contiennent. Utilisez `definePageLayout()` pour déclarer une mise en page pour un objet que vous possédez, ou `definePageLayoutTab()` pour ajouter un seul onglet à une mise en page qui existe déjà (la vôtre ou une mise en page standard de Twenty).
| Cas d'utilisation | Entité |
| -------------------------------------------------------------------------------------------------------------- | --------------------- |
| Définir l'intégralité de la mise en page de la page de détail des enregistrements d'un objet que vous possédez | `definePageLayout` |
| Ajouter un onglet à une mise en page existante (votre propre objet ou un objet standard) | `definePageLayoutTab` |
## definePageLayout
Utilisez ceci lorsque vous possédez l'intégralité de la page de détail — généralement pour un objet personnalisé que vous avez défini vous-même.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
### Points clés
* `type` est l'un de `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` ou `'STANDALONE_PAGE'`. Utilisez `'RECORD_PAGE'` pour personnaliser la vue détaillée d'un objet spécifique.
* `objectUniversalIdentifier` spécifie à quel objet cette mise en page s'applique.
* Chaque `tab` définit une section de la page avec un `title`, une `position` et un `layoutMode` : `VERTICAL_LIST` pour les pages d'enregistrement et les pages autonomes, `GRID` pour les tableaux de bord et `CANVAS` pour un seul widget qui doit remplir la zone d'affichage de l'onglet. Un onglet `VERTICAL_LIST` empile les widgets verticalement. Les widgets intégrés qui gèrent leur propre défilement, tels que les chronologies, les fichiers, les notes, les tâches et les workflows, remplissent une zone d'affichage ; les champs, les composants front, les graphiques et les autres widgets fit-content s'affichent à la hauteur de leur contenu ou à la hauteur configurée. Un onglet `GRID` dispose toujours ses widgets sous forme de cartes sur une grille de 12 colonnes. Un widget `CANVAS` n’a pas de position explicite ; si un onglet de canevas contient plusieurs widgets, ils s’affichent à la hauteur de leur contenu au lieu de remplir la fenêtre d’affichage.
* Définissez `layoutMode` explicitement. Si vous l’omettez, vous obtenez `VERTICAL_LIST` sur une `STANDALONE_PAGE` et `GRID` partout ailleurs, ce qui est rarement ce que vous voulez sur une page d’enregistrement.
* Chaque `widget` à l'intérieur d'un onglet peut afficher un [front component](/l/fr/developers/extend/apps/layout/front-components), une liste de relations ou d'autres types de widgets intégrés.
* Un widget `FRONT_COMPONENT` peut définir `headerCommandMenuItemUniversalIdentifiers` sur un tableau ordonné d’identifiants universels d’éléments du menu de commandes provenant de la même app. Ces actions apparaissent sous forme de boutons d’icône dans l’en-tête de la carte du widget et conservent leur disponibilité au niveau de la commande ainsi que leurs vérifications des autorisations. Les identifiants doivent être uniques et doivent pouvoir être résolus lors de l’installation de l’app.
* La `position` des onglets contrôle leur ordre. Utilisez des valeurs plus élevées (p. ex., 50) pour placer les onglets personnalisés après les onglets intégrés.
### Widgets des champs
Un widget `FIELD` affiche un champ de l’enregistrement. Pour les champs de relation, il peut également intégrer une liste d’enregistrements liés :
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` prend l’identifiant universel d’un champ sur l’objet de la mise en page.
* `fieldDisplayMode` est l’une des valeurs suivantes : `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` ou `'TABLE'`. `TABLE` intègre une vue listant les enregistrements d’un champ de relation un-à-plusieurs.
* `nestedRelationFieldMetadataId` est facultatif et prend l’identifiant universel d’un champ de relation un-à-plusieurs sur l’objet cible de la relation, afin de lister des enregistrements à deux sauts de relation (par exemple, une page Entreprise listant les opportunités des personnes de l’entreprise, ou une page Personne listant les opportunités de l’entreprise de la personne). Le premier saut peut être un champ de relation un-à-plusieurs ou plusieurs-à-un, le second doit être un-à-plusieurs (les relations de jonction ne sont pas prises en charge), et cela nécessite `fieldDisplayMode: 'TABLE'` — le combiner avec tout autre mode d’affichage constitue une erreur de validation, puisqu’un widget imbriqué est toujours rendu comme une vue intégrée.
## definePageLayoutTab
Utilisez ceci lorsque vous voulez uniquement ajouter un onglet à une mise en page existante — par exemple, un onglet d'analyse sur la page standard Company, ou un onglet de résumé d'IA rattaché à la mise en page de votre propre objet.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### Points clés
* `pageLayoutUniversalIdentifier` est **obligatoire** et doit pointer vers une mise en page de page qui existe déjà au moment de l'installation — soit une mise en page standard de Twenty, soit une mise en page définie par votre propre application. Les références inter-applications aux mises en page appartenant à une autre application installée ne sont pas prises en charge aujourd'hui. Lorsque la mise en page parente est manquante, l'installation échoue avec une erreur de validation explicite.
* Pour les mises en page standard de Twenty, importez les identifiants depuis `twenty-sdk/define` :
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Chaque entrée de mise en page expose également ses `tabs` et leurs `widgets`, afin que vous puissiez faire référence à n’importe quel niveau :
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Un alias court `STANDARD_PAGE_LAYOUT` est également disponible :
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* Les `widgets` sont limités à cet onglet uniquement — ils font référence aux [front components](/l/fr/developers/extend/apps/layout/front-components), aux vues, etc., exactement comme les widgets définis en ligne dans `definePageLayout`.
* `position` contrôle l'ordre par rapport aux onglets existants sur la mise en page ciblée. Choisissez une valeur qui place votre onglet à l'endroit souhaité par rapport aux onglets intégrés.
* Utilisez ceci plutôt que `definePageLayout` lorsque vous voulez seulement ajouter à une mise en page existante. Utilisez `definePageLayout` lorsque vous possédez l'intégralité de la mise en page.

View File

@@ -0,0 +1,113 @@
---
title: Vues
description: Livrez des vues enregistrées préconfigurées — ordre des colonnes, filtres, groupes — pour les objets de votre application.
icon: list
---
Une **vue** est une configuration enregistrée pour la façon dont les enregistrements d’un objet sont affichés : quels champs apparaissent, leur ordre, s’ils sont visibles, ainsi que les filtres ou groupes appliqués. Utilisez `defineView()` pour fournir des vues préconfigurées avec votre application.
Chaque objet obtient automatiquement sa vue de liste principale (le tableau « All ... » ouvert par un élément de navigation `OBJECT`), provisionnée par le serveur — y compris les objets que votre application crée, avec une colonne par champ que votre application déclare. Les vues que vous définissez avec `defineView()` sont toujours des vues **supplémentaires** en plus de cette vue par défaut.
```ts src/views/example-view.ts
import { defineView } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## Points clés
* `objectUniversalIdentifier` spécifie à quel objet cette vue s'applique. Il peut s’agir d’un objet personnalisé que vous avez défini ou d’un objet Twenty standard.
* La vue de liste principale de l’objet appartient au serveur : `key` est obsolète et ignorée, de sorte qu’une vue définie dans le manifeste ne peut jamais la revendiquer. Ajoutez un élément de navigation `VIEW` si vous voulez que votre vue soit à un clic dans la barre latérale.
* `fields` contrôle les colonnes affichées et leur ordre. Chaque champ référence un `fieldMetadataUniversalIdentifier`. Pour faire référence à un champ système créé automatiquement tel que `createdAt`, voir [Ciblage des champs système](/l/fr/developers/extend/apps/data/system-fields).
* Vous pouvez également définir `filters`, `filterGroups`, `sorts`, `groups` et `fieldGroups` pour des configurations plus avancées.
* `position` contrôle l’ordre lorsqu’il existe plusieurs vues pour le même objet.
## Propriétés optionnelles
| Propriété | Valeurs | Description |
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (par défaut), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Comment les enregistrements sont disposés. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` et `CALENDAR_WIDGET` existent également mais sont utilisés en interne par les widgets de mise en page.) |
| `visibility` | `ViewVisibility.WORKSPACE` (par défaut), `ViewVisibility.UNLISTED` | Indique si la vue est affichée pour l’ensemble de l’espace de travail ou masquée des sélecteurs. |
| `openRecordIn` | obsolète | N’est plus pris en compte : l’endroit où s’ouvrent les enregistrements est désormais une propriété de l’[objet](/l/fr/developers/extend/apps/data/objects) (`openRecordIn` sur `defineObject()`), avec repli sur la préférence de chaque membre. |
| `tris` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Ordre de tri par défaut. |
| `isCompact` | `boolean` | Affichage compact des lignes. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Regrouper les enregistrements (par exemple, les colonnes kanban) par un champ. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Agrégats et dimensionnement des colonnes Kanban. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Vues de calendrier : disposition, champ de date qui positionne les enregistrements et champ de date de fin facultatif. |
Tous les enums ci-dessus sont exportés depuis `twenty-sdk/define`.
## Filtres
Une vue peut être livrée avec des filtres préappliqués. Chaque filtre possède trois coordonnées : le **champ** faisant l’objet du filtrage, l’**opérateur** (comment comparer) et la **valeur** (par rapport à quoi comparer). Les trois doivent être alignées : l’utilisation d’un opérateur qui ne s’applique pas à un type de champ sera rejetée au moment de la synchronisation.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Opérateurs pris en charge par type de champ
| Type de champ | Opérandes pris en charge |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Les types de champs avec des noms similaires peuvent utiliser des opérandes complètement différents — `SELECT` et `MULTI_SELECT` étant un cas courant.
### Forme de valeur par opérande
Le champ `value` est toujours une valeur sérialisable en JSON, mais sa forme attendue dépend de l’opérande :
| Famille d’opérandes | Forme de la valeur | Exemple |
| ----------------------------------------------------------- | ------------------------------------ | ------------------------ |
| `IS`, `IS_NOT` sur `SELECT` | tableau de clés d’option (chaînes) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` sur `MULTI_SELECT` | tableau de clés d’option (chaînes) | `['TAG_A']` |
| `IS`, `IS_NOT` sur `RELATION` | tableau d’ID d’enregistrement (UUID) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` sur les champs de type texte | chaîne | `'acme'` |
| `IS`, `IS_NOT` sur `NUMBER` | chaîne (la valeur) | `'5'` |
| `IS` sur `RATING` / `UUID` | chaîne (la valeur) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | chaîne (la borne) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` sur `DATE` / `DATE_TIME` | chaîne ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | chaîne vide | `''` |
| `IS` sur `BOOLEAN` | `'true'` ou `'false'` | `'true'` |
## Comment les vues apparaissent dans l’interface utilisateur
Une vue seule n’est pas accessible à partir de la barre latérale. Pour la faire apparaître là, associez-la à un [élément de menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items) de type `VIEW` qui pointe vers le `universalIdentifier` de la vue. C’est le schéma canonique : chaque objet personnalisé obtient une vue d’index par défaut générée automatiquement lors de sa création ; il vous suffit donc d’inclure l’élément de la barre latérale qui l’ouvre.