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 2s

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,22 @@
---
title: Meilleures pratiques
---
Ce document décrit les meilleures pratiques à suivre lors de travaux sur le backend.
## Suivez une approche modulaire
Le backend suit une approche modulaire, qui est un principe fondamental lors de l'utilisation de NestJS. Assurez-vous de décomposer votre code en modules réutilisables pour maintenir une base de code propre et organisée.
Chaque module doit encapsuler une fonctionnalité particulière et avoir un périmètre bien défini. Cette approche modulaire permet une séparation claire des préoccupations et supprime les complexités inutiles.
## Exposez des services à utiliser dans les modules
Créez toujours des services avec une responsabilité claire et unique, ce qui améliore la lisibilité et la maintenabilité du code. Nommez les services de manière descriptive et cohérente.
Vous devez également exposer les services que vous souhaitez utiliser dans d'autres modules. Exposer des services à d'autres modules est possible grâce au puissant système d'injection de dépendance de NestJS, et favorise un couplage lâche entre les composants.
## Évitez d'utiliser le type `any`
Lorsque vous déclarez une variable comme `any`, le vérificateur de types de TypeScript ne procède à aucune vérification de type, ce qui permet d'affecter n'importe quel type de valeur à la variable. TypeScript utilise l'inférence de type pour déterminer le type d'une variable en fonction de sa valeur. En le déclarant comme `any`, TypeScript ne peut plus inférer le type. Cela rend difficile la détection des erreurs liées aux types pendant le développement, conduisant à des erreurs d'exécution et rendant le code moins maintenable, moins fiable et plus difficile à comprendre pour d'autres.
C'est pourquoi tout devrait avoir un type. Donc si vous créez un nouvel objet avec un prénom et un nom, vous devriez créer une interface ou un type contenant un prénom et un nom qui définit la forme de l'objet que vous manipulez.

View File

@@ -0,0 +1,39 @@
---
title: Objets personnalisés
---
Les objets sont des structures qui vous permettent de stocker des données (enregistrements, attributs et valeurs) spécifiques à une organisation. Twenty fournit à la fois des objets standard et personnalisés.
Les objets standard sont des objets intégrés avec un ensemble d'attributs disponibles pour tous les utilisateurs. Les exemples d'objets standard dans Twenty incluent Société et Personne. Les objets standard ont des champs standard également disponibles pour tous les utilisateurs de Twenty, comme Company.displayName.
Les objets personnalisés sont des objets que vous pouvez créer pour stocker des informations uniques à votre organisation. Ils ne sont pas intégrés ; les membres de votre espace de travail peuvent créer et personnaliser des objets personnalisés pour contenir des informations que les objets standard ne conviennent pas.
## Schéma de haut niveau
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="Schéma de haut niveau" />
</div>
<br />
## Comment ça marche
Les objets personnalisés proviennent de tables de métadonnées qui déterminent la forme, le nom et le type des objets. Toutes ces informations sont présentes dans la base de données des schémas de métadonnées, composée de tables :
* **DataSource** : Détaille où les données sont présentes.
* **Objet** : Décrit l'objet et lie à une DataSource.
* **Champ** : Décrit les champs d'un objet et se connecte à l'objet.
Pour ajouter un objet personnalisé, le workspaceMember interrogera l'API /metadata. Cela met à jour les métadonnées en conséquence et calcule un schéma GraphQL basé sur les métadonnées, en le stockant dans un cache GQL pour une utilisation ultérieure.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/add-custom-objects.jpeg" alt="Interroger l'API /metadata pour ajouter des objets personnalisés" />
</div>
<br />
Pour obtenir des données, le processus implique de faire des requêtes via le point de terminaison /graphql et de les transmettre via le Query Resolver.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="Interroger le point de terminaison /graphql pour obtenir des données" />
</div>

View File

@@ -0,0 +1,46 @@
---
title: Drapeaux de fonctionnalité
---
Les drapeaux de fonctionnalité sont utilisés pour masquer les fonctionnalités expérimentales. Pour Twenty, ils sont définis au niveau de l'espace de travail et non au niveau de l'utilisateur.
## Ajout d'un nouveau drapeau de fonctionnalité
Dans `FeatureFlagKey.ts` ajoutez l'indicateur de fonctionnalité :
```ts
type FeatureFlagKey =
| 'IS_FEATURENAME_ENABLED'
| ...;
```
Ajoutez-le également à l'énumération dans `feature-flag.entity.ts` :
```ts
enum FeatureFlagKeys {
IsFeatureNameEnabled = 'IS_FEATURENAME_ENABLED',
...
}
```
Pour appliquer un drapeau de fonctionnalité à une fonctionnalité **back-end**, utilisez :
```ts
@Gate({
featureFlag: 'IS_FEATURENAME_ENABLED',
})
```
Pour appliquer un drapeau de fonctionnalité à une fonctionnalité **front-end**, utilisez :
```ts
const isFeatureNameEnabled = useIsFeatureEnabled('IS_FEATURENAME_ENABLED');
```
## Configurer les drapeaux de fonctionnalité pour le déploiement
Modifiez l'enregistrement correspondant dans la Table `core.featureFlag` :
| iD | clé | workspaceId | valeur |
| --------- | ------------------------ | ----------- | ------ |
| Aléatoire | `IS_FEATURENAME_ENABLED` | WorkspaceID | `vrai` |

View File

@@ -0,0 +1,125 @@
---
title: Architecture des Dossiers
info: Un regard détaillé sur l'architecture des dossiers de notre serveur
---
La structure du répertoire backend est la suivante :
```
server
└───ability
└───constants
└───core
└───database
└───decorators
└───filters
└───guards
└───health
└───integrations
└───metadata
└───workspace
└───utils
```
## Compétence
Définit les permissions et inclut des gestionnaires pour chaque entité.
## Décorateurs
Définit des décorateurs personnalisés dans NestJS pour des fonctionnalités supplémentaires.
Voir [décorateurs personnalisés](https://docs.nestjs.com/custom-decorators) pour plus de détails.
## Filtres
Inclut des filtres d'exception pour gérer les exceptions qui pourraient se produire dans les points de terminaison GraphQL.
## Gardes
Voir [gardiens](https://docs.nestjs.com/guards) pour plus de détails.
## Santé
Inclut une API REST disponible publiquement (healthz) qui renvoie un JSON pour confirmer si la base de données fonctionne comme prévu.
## Métadonnées
Définit des objets personnalisés et met une API GraphQL à disposition (graphql/metadata).
## Espace de travail
Génère et sert un schéma GraphQL personnalisé basé sur les métadonnées.
### Structure du répertoire de l'espace de travail
```
workspace
└───workspace-schema-builder
└───factories
└───graphql-types
└───database
└───interfaces
└───object-definitions
└───services
└───storage
└───utils
└───workspace-resolver-builder
└───factories
└───interfaces
└───workspace-query-builder
└───factories
└───interfaces
└───workspace-query-runner
└───interfaces
└───utils
└───workspace-datasource
└───workspace-manager
└───workspace-migration-runner
└───utils
└───workspace.module.ts
└───workspace.factory.spec.ts
└───workspace.factory.ts
```
La racine du répertoire de l'espace de travail inclut le fichier `workspace.factory.ts`, qui contient la fonction `createGraphQLSchema`. Cette fonction génère un schéma spécifique à l'espace de travail en utilisant les métadonnées pour adapter un schéma pour chaque espace de travail. En séparant la construction du schéma et du résolveur, nous utilisons la fonction `makeExecutableSchema`, qui combine ces éléments distincts.
Cette stratégie n'est pas seulement une question d'organisation, mais aide également à l'optimisation, comme la mise en cache des définitions de type générées pour améliorer les performances et la scalabilité.
### Constructeur de schéma d'espace de travail
Génère le schéma GraphQL, et inclut :
#### Usines :
Constructeurs spécialisés pour générer des constructions liées à GraphQL.
* La type.factory traduit les métadonnées de champs en types GraphQL en utilisant le `TypeMapperService`.
* La type-definition.factory crée des objets d'entrée ou de sortie GraphQL dérivés de `objectMetadata`.
#### Types GraphQL
Inclut des énumérations, des entrées, des objets et des scalaires, et sert de blocs de construction pour la construction du schéma.
#### Interfaces et définitions d'objets
Contient les plans pour les entités GraphQL, et inclut à la fois des types prédéfinis et personnalisés comme `MONEY` ou `URL`.
#### Services
Contient le service responsable de l'association du FieldMetadataType avec son type scalaire ou modificateur de requête GraphQL approprié.
#### Stockage
Inclut la classe `TypeDefinitionsStorage` qui contient des définitions de type réutilisables, empêchant la duplication des types GraphQL.
### Constructeur de résolveur d'espace de travail
Crée des fonctions de résolveur pour interroger et modifier le schéma GraphQL.
Chaque usine de ce répertoire est responsable de la production d'un type de résolveur distinct, comme le `FindManyResolverFactory`, conçu pour une application adaptable à plusieurs tables.
### Exécuteur de requêtes d'espace de travail
Exécute les requêtes générées sur la base de données et analyse le résultat.

View File

@@ -0,0 +1,41 @@
---
title: File d'attente de messages
---
Les files d'attente facilitent les opérations asynchrones. Elles peuvent être utilisées pour effectuer des tâches de fond telles que l'envoi d'un e-mail de bienvenue lors de l'enregistrement.
Chaque cas d'utilisation aura sa propre classe de file d'attente étendue à partir de `MessageQueueServiceBase`.
Actuellement, nous ne supportons que `bull-mq`[bull-mq](https://bullmq.io/) comme pilote de file d'attente.
## Étapes pour créer et utiliser une nouvelle file d'attente
1. Ajoutez un nom de file d'attente pour votre nouvelle file sous l'énumération `MESSAGE_QUEUES`.
2. Fournissez l'implémentation de la fabrique de la file d'attente avec le nom de la file comme dépendance.
3. Injectez la file d'attente que vous avez créée dans le module/service requis en utilisant le nom de la file comme dépendance.
4. Ajoutez une classe worker avec une injection basée sur des jetons, comme pour le producteur.
### Exemple d'utilisation
```ts
class Resolver {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {}
async onSomeAction() {
//business logic
await this.queue.add(someData);
}
}
//async worker
class CustomWorker {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {
this.initWorker();
}
async initWorker() {
await this.queue.work(async ({ id, data }) => {
//worker logic
});
}
}
```

View File

@@ -0,0 +1,87 @@
---
title: Commandes Backend
icon: terminal
---
## Commandes utiles
Ces commandes doivent être exécutées depuis le dossier packages/twenty-server.
Depuis n'importe quel autre dossier, vous pouvez exécuter `npx nx {command} twenty-server` (ou `npx nx run twenty-server:{command}`).
### Configuration initiale
```
npx nx database:reset twenty-server # setup the database with dev seeds
```
### Démarrer le serveur
```
npx nx run twenty-server:start
```
### Analyse
```
npx nx run twenty-server:lint # pass --fix to fix lint errors
```
### Test
```
npx nx run twenty-server:test:unit # run unit tests
npx nx run twenty-server:test:integration # run integration tests
```
Remarque: vous pouvez exécuter `npx nx run twenty-server:test:integration:with-db-reset` si vous avez besoin de réinitialiser la base de données avant de lancer les tests d'intégration.
### Réinitialiser la base de données
Si vous souhaitez réinitialiser et peupler la base de données, vous pouvez exécuter la commande suivante :
```bash
npx nx run twenty-server:database:reset
```
### Migrations
#### Pour les objets dans les schémas Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:database:migrate:generate
```
## Écosystème Tech
Twenty utilise principalement NestJS pour le backend.
Prisma a été le premier ORM que nous avons utilisé. Mais pour permettre aux utilisateurs de créer des champs et objets personnalisés, un niveau inférieur avait plus de sens car nous avons besoin d'un contrôle granulaire. Le projet utilise maintenant TypeORM.
Voici à quoi ressemble maintenant la pile technologique.
**Noyau**
* [NestJS](https://nestjs.com/)
* [TypeORM](https://typeorm.io/)
* [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
**Base de données**
* [Postgres](https://www.postgresql.org/)
**Intégrations tierces**
* [Sentry](https://sentry.io/welcome/) pour suivre les bugs.
**Tests**
* [Jest](https://jestjs.io/)
**Outils**
* [Yarn](https://yarnpkg.com/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
**Développement**
* [AWS EKS](https://aws.amazon.com/eks/)

View File

@@ -0,0 +1,85 @@
---
title: Application Zapier
---
Synchronisez facilement Twenty avec plus de 3000 applications à l'aide de [Zapier](https://zapier.com/). Automatisez les tâches, boostez la productivité et dynamisez vos relations client !
## À propos de Zapier
Zapier est un outil qui vous permet d'automatiser des flux de travail en connectant les applications que votre équipe utilise quotidiennement. Le concept fondamental de Zapier est l'automatisation des flux de travail, appelés Zaps, et inclut des déclencheurs et des actions.
Vous pouvez en savoir plus sur le fonctionnement de Zapier [ici](https://zapier.com/how-it-works).
## Installation
### Étape 1 : Installez les packages Zapier
```bash
cd packages/twenty-zapier
yarn
```
### Étape 2 : Connectez-vous avec le CLI
Utilisez vos identifiants Zapier pour vous connecter en utilisant le CLI :
```bash
zapier login
```
### Étape 3 : Configurer les variables d'environnement
Depuis le dossier `packages/twenty-zapier`, exécutez :
```bash
cp .env.example .env
```
Exécutez l'application localement, rendez-vous sur [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks) et générez une clé API.
Remplacez la valeur **YOUR_API_KEY** dans le fichier `.env` par la clé API que vous venez de générer.
## Développement
<Warning>
Assurez-vous d'exécuter `yarn build` avant toute commande `zapier`.
</Warning>
### Test
```bash
yarn test
```
### Analyse
```bash
yarn format
```
### Surveillez et compilez pendant que vous éditez le code
```bash
yarn watch
```
### Validez votre application Zapier
```bash
yarn validate
```
### Déployez votre application Zapier
```bash
yarn deploy
```
### Liste de toutes les commandes CLI de Zapier
```bash
zapier
```

View File

@@ -0,0 +1,79 @@
---
title: Signalements de bogues, demandes et pull requests
icon: bug
info: Signalez des problèmes, demandez des fonctionnalités et contribuez au code
---
## Signaler des bogues
Pour signaler un bogue, veuillez [créer un problème sur GitHub](https://github.com/twentyhq/twenty/issues/new).
Vous pouvez également demander de l'aide sur [Discord](https://discord.gg/cx5n4Jzs57).
## Demandes de fonctionnalités
Si vous n'êtes pas sûr qu'il s'agisse d'un bogue, et que vous pensez que cela ressemble plus à une demande de fonctionnalité, alors vous devriez probablement [ouvrir une discussion à la place](https://github.com/twentyhq/twenty/discussions/new).
## Soumettre une pull request
La contribution au code de Twenty commence par une pull request (PR).
### Avant de démarrer
1. Consultez les [problèmes existants](https://github.com/twentyhq/twenty/issues) pour repérer les travaux liés
2. Pour les nouvelles fonctionnalités, ouvrez d’abord un problème pour en discuter
3. Consultez notre [Code de conduite](https://github.com/twentyhq/twenty/blob/main/.github/CODE_OF_CONDUCT.md)
### Forker et cloner
1. Forkez le dépôt sur GitHub
2. Clonez votre fork:
```bash
git clone https://github.com/YOUR_USERNAME/twenty.git
cd twenty
```
3. Ajoutez le remote upstream:
```bash
git remote add upstream https://github.com/twentyhq/twenty.git
```
### Créer une branche
```bash
git checkout -b feature/your-feature-name
```
Utilisez des noms de branche descriptifs:
* `feature/add-export-button`
* `fix/login-redirect-issue`
* `docs/update-api-guide`
### Apportez vos modifications
1. Écrivez un code propre et bien documenté
2. Suivez le style de code existant
3. Ajoutez des tests pour les nouvelles fonctionnalités
4. Mettez à jour la documentation si nécessaire
### Soumettez votre PR
1. Poussez votre branche:
```bash
git push origin feature/your-feature-name
```
2. Ouvrez une PR sur GitHub
3. Remplissez le modèle de PR
4. Liez les problèmes associés
### Liste de contrôle de la PR
* [ ] Le code respecte les directives de style du projet
* [ ] Les tests réussissent localement
* [ ] La documentation est à jour
* [ ] La description de la PR explique les modifications

View File

@@ -0,0 +1,329 @@
---
title: Meilleures pratiques
icon: star
---
Ce document décrit les meilleures pratiques à suivre lors de votre travail sur l'interface frontend.
## Gestion de l'état
React et Jotai gèrent la gestion de l'état dans la base de code.
### Utilisez les atomes Jotai pour stocker l'état
C'est une bonne pratique de créer autant d'atomes que nécessaire pour stocker votre état.
<Warning>
Il vaut mieux utiliser des atomes supplémentaires plutôt que d'essayer d'être trop concis en perçant les propriétés.
</Warning>
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
<input
value={myAtom}
onChange={(e) => setMyAtom(e.target.value)}
/>
</div>
);
}
```
### Ne pas utiliser `useRef` pour stocker l'état
Évitez d'utiliser `useRef` pour stocker l'état.
Si vous souhaitez stocker un état, vous devez utiliser `useState` ou des atomes Jotai avec `useAtomState`.
Consultez [comment gérer les re-rendus](#managing-re-renders) si vous avez l'impression d'avoir besoin de `useRef` pour empêcher certains re-rendus.
## Gestion des re-rendus
Les re-rendus peuvent être difficiles à gérer dans React.
Voici quelques règles à suivre pour éviter les re-rendus inutiles.
Gardez à l'esprit que vous pouvez **toujours** éviter les re-rendus en comprenant leur cause.
### Travaillez au niveau racine
Éviter les re-rendus dans les nouvelles fonctionnalités est désormais facile en les éliminant au niveau racine.
Le composant latéral `PageChangeEffect` contient juste un `useEffect` qui contient toute la logique à exécuter sur un changement de page.
De cette manière, vous savez qu'il n'y a qu'un seul endroit qui peut déclencher un re-rendu.
### Réfléchissez toujours à deux fois avant d'ajouter `useEffect` dans votre code de base
Les re-rendus sont souvent causés par un `useEffect` inutile.
Vous devriez vous demander si vous avez besoin de `useEffect`, ou si vous pouvez déplacer la logique dans une fonction de gestion d'événements.
Vous trouverez généralement facile de déplacer la logique dans une fonction `handleClick` ou `handleChange`.
Vous pouvez également les trouver dans des bibliothèques comme Apollo : `onCompleted`, `onError`, etc.
### Utilisez un composant frère pour extraire `useEffect` ou une logique de récupération de données
Si vous avez le sentiment que vous avez besoin d'ajouter un `useEffect` à votre composant racine, vous devriez envisager de l'extraire dans un composant de voiture latérale.
Vous pouvez appliquer la même chose à la logique de récupération de données, avec les hooks Apollo.
```tsx
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <div>{data}</div>;
};
export const App = () => (
<PageComponent />
);
```
```tsx
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <></>;
};
export const App = () => (
<>
<PageData />
<PageComponent />
</>
);
```
### Utilisez les familles d'atomes et les sélecteurs
Les familles d'atomes et les sélecteurs sont un excellent moyen d'éviter les re-rendus.
Ils sont utiles lorsque vous devez stocker une liste d'articles.
### Vous ne devriez pas utiliser `React.memo(MyComponent)`
Évitez d'utiliser `React.memo()` car cela ne résout pas la cause du re-rendu, mais brise plutôt la chaîne de re-rendu, ce qui peut entraîner un comportement inattendu et rendre le code très difficile à refactoriser.
### Limitez l'utilisation de `useCallback` ou `useMemo`
Ils ne sont souvent pas nécessaires et rendront le code plus difficile à lire et à maintenir pour un gain de performance imperceptible.
## Console.logs
Les déclarations `console.log` sont précieuses pendant le développement, offrant des insights en temps réel sur les valeurs de variables et le flux de code. Mais, les laisser dans le code de production peut entraîner plusieurs problèmes :
1. **Performance** : Un journalisation excessive peut affecter les performances d'exécution, notamment sur les applications côté client.
2. **Sécurité** : La journalisation de données sensibles peut exposer des informations critiques à toute personne qui inspecte la console du navigateur.
3. **Propreté** : Remplir la console de logs peut masquer les avertissements ou erreurs importants que les développeurs ou outils doivent voir.
4. **Professionnalisme** : Les utilisateurs finaux ou clients vérifiant la console et voyant une myriade de déclarations de log pourraient remettre en question la qualité et la finition du code.
Assurez-vous de supprimer tous les `console.logs` avant de pousser le code en production.
## Nommage
### Nom des variables
Les noms de variables doivent décrire précisément l'objectif ou la fonction de la variable.
#### Le problème avec les noms génériques
Les noms génériques en programmation ne sont pas idéaux car ils manquent de spécificité, ce qui conduit à une ambiguïté et réduit la lisibilité du code. De tels noms ne parviennent pas à transmettre l'objectif de la variable ou de la fonction, rendant difficile pour les développeurs de comprendre l'intention du code sans une enquête plus approfondie. Cela peut entraîner un temps de débogage accru, une plus grande vulnérabilité aux erreurs et des difficultés de maintenance et de collaboration. Pendant ce temps, des noms descriptifs rendent le code explicite et plus facile à naviguer, améliorant la qualité du code et la productivité des développeurs.
```tsx
// ❌ Bad, uses a generic name that doesn't communicate its
// purpose or content clearly
const [value, setValue] = useState('');
```
```tsx
// ✅ Good, uses a descriptive name
const [email, setEmail] = useState('');
```
#### Certains mots à éviter dans les noms de variables
* factice
### Gestionnaires d'événements
Les noms des gestionnaires d'événements doivent commencer par `handle`, tandis que `on` est un préfixe utilisé pour nommer les événements dans les propriétés des composants.
```tsx
// ❌ Bad
const onEmailChange = (val: string) => {
// ...
};
```
```tsx
// ✅ Good
const handleEmailChange = (val: string) => {
// ...
};
```
## Props optionnels
Évitez de passer la valeur par défaut pour un prop optionnel.
**EXEMPLE**
Prenez le composant`EmailField` défini ci-dessous :
```tsx
type EmailFieldProps = {
value: string;
disabled?: boolean;
};
const EmailField = ({ value, disabled = false }: EmailFieldProps) => (
<TextInput value={value} disabled={disabled} fullWidth />
);
```
**Utilisation**
```tsx
// ❌ Bad, passing in the same value as the default value adds no value
const Form = () => <EmailField value="username@email.com" disabled={false} />;
```
```tsx
// ✅ Good, assumes the default value
const Form = () => <EmailField value="username@email.com" />;
```
## Composant en tant que props
Essayez autant que possible de transmettre des composants non instanciés comme props, afin que les enfants puissent décider eux-mêmes des props qu'ils ont besoin de passer.
L'exemple le plus courant pour cela est les composants icône :
```tsx
const SomeParentComponent = () => <MyComponent Icon={MyIcon} />;
// In MyComponent
const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
const theme = useTheme();
return (
<div>
<MyIcon size={theme.icon.size.md}>
</div>
)
};
```
Pour que React comprenne qu'un composant est un composant, vous devez utiliser PascalCase, pour l'instancier plus tard avec `<MyIcon>`
## Forage de Props : Gardez-le Minimal
Le forage de props, dans le contexte de React, fait référence à la pratique consistant à passer des variables d'état et leurs setters à travers de nombreuses couches de composants, même si les composants intermédiaires ne les utilisent pas. Bien que parfois nécessaire, un forage de props excessif peut entraîner :
1. **Lisibilité Réduite** : Retrouver l'origine d'un prop ou l'endroit où il est utilisé peut devenir complexe dans une structure de composants profondément imbriquée.
2. **Défis de Maintenance** : Des modifications dans la structure des props d'un composant peuvent nécessiter des ajustements dans plusieurs composants, même s'ils n'utilisent pas directement le prop.
3. **Réutilisabilité Réduite du Composant** : Un composant recevant beaucoup de props uniquement pour les transmettre devient moins polyvalent et plus difficile à réutiliser dans différents contextes.
Si vous sentez que vous utilisez un forage de props excessif, voir [les meilleures pratiques de gestion d'état](#state-management).
## Imports
Lors de l'importation, optez pour les alias désignés plutôt que de spécifier des chemins complets ou relatifs.
**Les Alias**
```js
{
alias: {
"~": path.resolve(__dirname, "src"),
"@": path.resolve(__dirname, "src/modules"),
"@testing": path.resolve(__dirname, "src/testing"),
},
}
```
**Utilisation**
```tsx
// ❌ Bad, specifies the entire relative path
import {
CatalogDecorator
} from '../../../../../testing/decorators/CatalogDecorator';
import {
ComponentDecorator
} from '../../../../../testing/decorators/ComponentDecorator';
```
```tsx
// ✅ Good, utilises the designated aliases
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from 'twenty-ui/testing';
```
## Validation de Schéma
[Zod](https://github.com/colinhacks/zod) est le validateur de schéma pour les objets non typés:
```js
const validationSchema = z
.object({
exist: z.boolean(),
email: z
.string()
.email('Email must be a valid email'),
password: z
.string()
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
})
.required();
type Form = z.infer<typeof validationSchema>;
```
## Changements Radicaux
Effectuez toujours des tests manuels approfondis avant de continuer pour garantir que les modifications n'ont pas causé de perturbations ailleurs, étant donné que les tests n'ont pas encore été intégrés de manière extensive.

View File

@@ -0,0 +1,110 @@
---
title: Architecture des Dossiers
icon: folder-tree
info: Un aperçu détaillé de notre architecture de dossiers
---
Dans ce guide, vous explorerez les détails de la structure du répertoire de projet et comment elle contribue à l'organisation et à la maintenabilité de Twenty.
En suivant cette convention d'architecture de dossiers, il est plus facile de trouver les fichiers liés à des fonctionnalités spécifiques et de s'assurer que l'application est évolutive et maintenable.
```
front
└───modules
│ └───module1
│ │ └───submodule1
│ └───module2
│ └───ui
│ │ └───display
│ │ └───inputs
│ │ │ └───buttons
│ │ └───...
└───pages
└───...
```
## Pages
Comprend les composants de haut niveau définis par les routes de l'application. Ils importent des composants de plus bas niveau depuis le dossier `modules` (plus de détails ci-dessous).
## Modules
Chaque module représente une fonctionnalité ou un groupe de fonctionnalités, comprenant ses composants spécifiques, ses états, et sa logique opérationnelle.
Ils doivent tous suivre la structure ci-dessous. Vous pouvez imbriquer des modules dans des modules (appelés sous-modules) et les mêmes règles s'appliqueront.
```
module1
└───components
│ └───component1
│ └───component2
└───constants
└───contexts
└───graphql
│ └───fragments
│ └───queries
│ └───mutations
└───hooks
│ └───internal
└───states
│ └───selectors
└───types
└───utils
```
### Contextes
Un contexte est un moyen de transmettre des données à travers l'arborescence de composants sans avoir à transmettre les propriétés manuellement à chaque niveau.
Voir [React Context](https://react.dev/reference/react#context-hooks) pour plus de détails.
### GraphQL
Comprend des fragments, des requêtes et des mutations.
Voir [GraphQL](https://graphql.org/learn/) pour plus de détails.
* Fragments
Un fragment est une partie réutilisable d'une requête, que vous pouvez utiliser dans différents endroits. En utilisant des fragments, il est plus facile d'éviter de dupliquer du code.
Voir [GraphQL Fragments](https://graphql.org/learn/queries/#fragments) pour plus de détails.
* Requêtes
Voir [GraphQL Queries](https://graphql.org/learn/queries/) pour plus de détails.
* Mutations
Voir [GraphQL Mutations](https://graphql.org/learn/queries/#mutations) pour plus de détails.
### Hooks
Voir [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) pour plus de détails.
### États
Contient la logique de gestion des états. [Jotai](https://jotai.org) gère cela.
* Sélecteurs : Les atomes dérivés (à l'aide de `createAtomSelector`) calculent des valeurs à partir d'autres atomes et sont automatiquement mémoïsés.
La gestion de l'état intégrée de React gère toujours l'état au sein d'un composant.
### Utilitaires
Devrait juste contenir des fonctions pures réutilisables. Autrement, créez des hooks personnalisés dans le dossier `hooks`.
## UI
Contient tous les composants d'interface utilisateur réutilisables utilisés dans l'application.
Ce dossier peut contenir des sous-dossiers, comme `data`, `display`, `feedback`, et `input` pour des types de composants spécifiques. Chaque composant doit être autonome et réutilisable, de sorte que vous puissiez l'utiliser dans différentes parties de l'application.
En séparant les composants UI des autres composants dans le dossier `modules`, il est plus facile de maintenir un design cohérent et d'effectuer des changements de l'interface utilisateur sans affecter d'autres parties (logique métier) de la base de code.
## Interface et dépendances
Vous pouvez importer le code d'autres modules depuis n'importe quel module, sauf le dossier `ui`. Cela permettra de garder son code facile à tester.
### Interne
Chaque partie (hooks, états, ...) d'un module peut avoir un dossier `internal`, qui contient des parties utilisées uniquement au sein du module.

View File

@@ -0,0 +1,91 @@
---
title: Commandes Frontend
icon: terminal
---
## Commandes utiles
### Lancement de l'application
```bash
npx nx start twenty-front
```
### Régénérer le schéma GraphQL basé sur le schéma API GraphQL
```bash
npx nx run twenty-front:graphql:generate --configuration=metadata
```
OU
```bash
npx nx run twenty-front:graphql:generate
```
### Analyse
```bash
npx nx run twenty-front:lint # pass --fix to fix lint errors
```
## Traductions
```bash
npx nx run twenty-front:lingui:extract
npx nx run twenty-front:lingui:compile
```
### Test
```bash
npx nx run twenty-front:test # run jest tests
npx nx run twenty-front:storybook:serve:dev # run storybook
npx nx run twenty-front:storybook:test # run tests # (needs yarn storybook:serve:dev to be running)
npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to be running)
```
## Écosystème Tech
Le projet a une stack simple et propre, avec un code boilerplate minimal.
**Application**
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Tests**
* [Jest](https://jestjs.io/)
* [Storybook](https://storybook.js.org/)
**Outils**
* [Yarn](https://yarnpkg.com/)
* [Craco](https://craco.js.org/docs/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
## Architecture
### Routage
[React Router](https://reactrouter.com/) gère le routage.
Pour éviter les [re-renders](/l/fr/developers/contribute/capabilities/frontend-development/best-practices-front#managing-re-renders) inutiles toute la logique de routage est dans un `useEffect` dans `PageChangeEffect`.
### Gestion de l'État
[Jotai](https://jotai.org/) gère la gestion de l'état.
Voir [les meilleures pratiques](/l/fr/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) pour plus d'informations sur la gestion de l'état.
## Tests
[Jest](https://jestjs.io/) sert de guide pour les tests unitaires tandis que [Storybook](https://storybook.js.org/) est utilisé pour les tests de composants.
Jest est principalement utilisé pour tester les fonctions utilitaires, et non les composants eux-mêmes.
Storybook est utilisé pour tester le comportement des composants isolés, ainsi que pour afficher le système de design.

View File

@@ -0,0 +1,178 @@
---
title: Raccourcis clavier
---
## Introduction
Lorsque vous devez écouter une touche de raccourci, vous utilisez normalement l'événement `onKeyDown`.
Cependant, dans `twenty-front`, vous pourriez avoir des conflits entre les mêmes raccourcis utilisés dans différents composants, montés en même temps.
Par exemple, si vous avez une page qui écoute la touche Entrée, et une fenêtre modale qui écoute aussi la touche Entrée, avec un composant Select à l'intérieur de cette fenêtre qui écoute également la touche Entrée, vous risquez d'avoir un conflit lorsque tous sont montés en même temps.
## Le hook `useScopedHotkeys`
Pour résoudre ce problème, nous avons un hook personnalisé qui permet d'écouter les raccourcis sans aucun conflit.
Vous l'insérez dans un composant et il écoutera les raccourcis uniquement lorsque le composant est monté ET lorsque le **périmètre du raccourci** spécifié est actif.
## Comment écouter les raccourcis en pratique ?
Deux étapes sont nécessaires pour configurer l'écoute des raccourcis :
1. Définir le [périmètre du raccourci](#what-is-a-hotkey-scope-) qui écoutera les raccourcis
2. Utiliser le hook `useScopedHotkeys` pour écouter les raccourcis
La configuration des périmètres de raccourcis est nécessaire même sur des pages simples, car d'autres éléments de l'interface utilisateur comme le menu de gauche ou le menu de commandes pourraient également écouter les raccourcis.
## Cas d'utilisation des raccourcis
En général, vous aurez deux cas d'utilisation nécessitant des raccourcis :
1. Dans une page ou un composant monté dans une page
2. Dans un composant de type modal qui prend le focus à la suite d'une action utilisateur
Le deuxième cas d'utilisation peut se produire de manière récursive : un menu déroulant dans une fenêtre modale par exemple.
### Écouter les raccourcis dans une page
Exemple :
```tsx
const PageListeningEnter = () => {
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
// 1. Set the hotkey scope in a useEffect
useEffect(() => {
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleEnterPage,
);
// Revert to the previous hotkey scope when the component is unmounted
return () => {
goBackToPreviousHotkeyScope();
};
}, [goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope]);
// 2. Use the useScopedHotkeys hook
useScopedHotkeys(
Key.Enter,
() => {
// Some logic executed on this page when the user presses Enter
// ...
},
ExampleHotkeyScopes.ExampleEnterPage,
);
return <div>My page that listens for Enter</div>;
};
```
### Écouter les raccourcis dans un composant de type modal
Pour cet exemple, nous allons utiliser un composant modal qui écoute la touche Échap pour informer son parent de le fermer.
Ici, l'interaction utilisateur change le périmètre.
```tsx
const ExamplePageWithModal = () => {
const [showModal, setShowModal] = useState(false);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const handleOpenModalClick = () => {
// 1. Set the hotkey scope when user opens the modal
setShowModal(true);
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleModal,
);
};
const handleModalClose = () => {
// 1. Revert to the previous hotkey scope when the modal is closed
setShowModal(false);
goBackToPreviousHotkeyScope();
};
return <div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>;
};
```
Ensuite, dans le composant modal :
```tsx
const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
// 2. Use the useScopedHotkeys hook to listen for Escape.
// Note that escape is a common hotkey that could be used by many other components
// So it's important to use a hotkey scope to avoid conflicts
useScopedHotkeys(
Key.Escape,
() => {
onClose()
},
ExampleHotkeyScopes.ExampleModal,
);
return <div>My modal component</div>;
};
```
Il est important d'utiliser ce masque lorsque vous n'êtes pas sûr que le simple fait d'utiliser un useEffect avec mount/unmount sera suffisant pour éviter les conflits.
Ces conflits peuvent être difficiles à déboger, et cela peut se produire plus souvent avec useEffects.
## Qu'est-ce qu'un périmètre de raccourci ?
Un périmètre de raccourci est une chaîne de caractères qui représente un contexte dans lequel les raccourcis sont actifs. Il est généralement encodé sous forme d'enum.
Lorsque vous modifiez le périmètre de raccourci, les raccourcis qui écoutent ce périmètre seront activés et ceux qui écoutent d'autres périmètres seront désactivés.
Vous ne pouvez définir qu'un seul périmètre à la fois.
Par exemple, les périmètres de raccourcis pour chaque page sont définis dans l'`enum PageHotkeyScope` :
```tsx
export enum PageHotkeyScope {
Settings = 'settings',
CreateWorkspace = 'create-workspace',
SignInUp = 'sign-in-up',
CreateProfile = 'create-profile',
PlanRequired = 'plan-required',
ShowPage = 'show-page',
PersonShowPage = 'person-show-page',
CompanyShowPage = 'company-show-page',
CompaniesPage = 'companies-page',
PeoplePage = 'people-page',
OpportunitiesPage = 'opportunities-page',
ProfilePage = 'profile-page',
WorkspaceMemberPage = 'workspace-member-page',
TaskPage = 'task-page',
}
```
En interne, le périmètre actuellement sélectionné est stocké dans un atome Jotai qui est partagé dans toute l'application :
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
key: 'currentHotkeyScopeState',
defaultValue: INITIAL_HOTKEYS_SCOPE,
});
```
Mais cet atome ne doit jamais être manipulé manuellement ! Nous verrons comment l'utiliser dans la prochaine section.
## Comment cela fonctionne-t-il en interne ?
Nous avons créé un léger emballage au-dessus de [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) qui le rend plus performant et évite les rendus inutiles.
Nous créons également un atome Jotai pour gérer l'état du périmètre des raccourcis et le rendre disponible partout dans l'application.

View File

@@ -0,0 +1,8 @@
---
title: Storybook
description: Parcourir la bibliothèque de composants UI de Twenty
---
Consultez notre bibliothèque de composants complète et la documentation dans Storybook.
[Ouvrir Storybook →](https://storybook.twenty.com)

View File

@@ -0,0 +1,291 @@
---
title: Guide de style
icon: paintbrush
---
Ce document inclut les règles à suivre lors de l'écriture de code.
L'objectif ici est d'avoir une base de code cohérente, facile à lire et à maintenir.
Pour cela, il vaut mieux être un peu plus verbeux que trop concis.
Gardez toujours à l'esprit que les gens lisent le code plus souvent qu'ils ne l'écrivent, surtout dans un projet open source où tout le monde peut contribuer.
Il existe de nombreuses règles qui ne sont pas définies ici, mais qui sont vérifiées automatiquement par des linters.
## React
### Utilisez des composants fonctionnels
Utilisez toujours des composants fonctionnels TSX.
N'utilisez pas `import` par défaut avec `const`, car c'est plus difficile à lire et plus difficile à importer avec l'autocomplétion du code.
```tsx
// ❌ Bad, harder to read, harder to import with code completion
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good, easy to read, easy to import with code completion
export function MyComponent() {
return <div>Hello World</div>;
};
```
### Propriétés
Créez le type des props et nommez-le `(NomDuComposant)Props` s'il n'est pas nécessaire de l'exporter.
Utilisez la déstructuration des props.
```tsx
// ❌ Bad, no type
export const MyComponent = (props) => <div>Hello {props.name}</div>;
// ✅ Good, type
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
#### Évitez d'utiliser `React.FC` ou `React.FunctionComponent` pour définir les types de props.
```tsx
/* ❌ - Bad, defines the component type annotations with `FC`
* - With `React.FC`, the component implicitly accepts a `children` prop
* even if it's not defined in the prop type. This might not always be
* desirable, especially if the component doesn't intend to render
* children.
*/
const EmailField: React.FC<{
value: string;
}> = ({ value }) => <TextInput value={value} disabled fullWidth />;
```
```tsx
/* ✅ - Good, a separate type (OwnProps) is explicitly defined for the
* component's props
* - This method doesn't automatically include the children prop. If
* you want to include it, you have to specify it in OwnProps.
*/
type EmailFieldProps = {
value: string;
};
const EmailField = ({ value }: EmailFieldProps) => (
<TextInput value={value} disabled fullWidth />
);
```
#### Pas de propagation de props à variable unique dans les éléments JSX
Évitez d'utiliser la propagation de props à variable unique dans les éléments JSX, comme `{...props}`. Cette pratique résulte souvent en un code moins lisible et plus difficile à maintenir car il n'est pas clair quels props le composant reçoit.
```tsx
/* ❌ - Bad, spreads a single variable prop into the underlying component
*/
const MyComponent = (props: OwnProps) => {
return <OtherComponent {...props} />;
}
```
```tsx
/* ✅ - Good, Explicitly lists all props
* - Enhances readability and maintainability
*/
const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
return <OtherComponent {...{ prop1, prop2, prop3 }} />;
};
```
Raisonnement :
* D'un coup d'œil, il est plus clair quels props le code transmet, le rendant plus facile à comprendre et à maintenir.
* Cela aide à éviter le couplage serré entre les composants via leurs props.
* Les outils de linting facilitent l'identification des props mal orthographiés ou inutilisées lorsque vous listez explicitement les props.
## JavaScript
### Utilisez l'opérateur de coalescence nulle `??`
```tsx
// ❌ Bad, can return 'default' even if value is 0 or ''
const value = process.env.MY_VALUE || 'default';
// ✅ Good, will return 'default' only if value is null or undefined
const value = process.env.MY_VALUE ?? 'default';
```
### Utilisez la chaîne facultative `?.`
```tsx
// ❌ Bad
onClick && onClick();
// ✅ Good
onClick?.();
```
## TypeScript
### Utilisez `type` au lieu de `interface`
Utilisez toujours `type` au lieu de `interface`, car ils se chevauchent presque toujours, et `type` est plus flexible.
```tsx
// ❌ Bad
interface MyInterface {
name: string;
}
// ✅ Good
type MyType = {
name: string;
};
```
### Utilisez des littéraux de chaîne au lieu d'enums
[Les littéraux de chaîne](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) sont la méthode de référence pour gérer des valeurs semblables à des enums dans TypeScript. Ils sont plus faciles à étendre avec Pick et Omit, et offrent une meilleure expérience développeur, en particulier avec l'auto-complétion du code.
Vous pouvez voir pourquoi TypeScript recommande d'éviter les enums [ici](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
```tsx
// ❌ Bad, utilizes an enum
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
let color = Color.Red;
```
```tsx
// ✅ Good, utilizes a string literal
let color: "red" | "green" | "blue" = "red";
```
#### GraphQL et bibliothèques internes
Vous devriez utiliser les enums générés par le codegen GraphQL.
Il est également préférable d'utiliser un enum lors de l'utilisation d'une bibliothèque interne, afin que la bibliothèque interne n'ait pas à exposer un type de littéral de chaîne qui n'est pas lié à l'API interne.
Exemple :
```TSX
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
```
## Stylisme
### Utilisez StyledComponents
Styliser les composants avec [Linaria styled](https://github.com/callstack/linaria).
```tsx
// ❌ Bad
<div className="my-class">Hello World</div>
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
Préfixez les composants stylisés avec "Styled" pour les différencier des composants "réels".
```tsx
// ❌ Bad
const Title = styled.div`
color: red;
`;
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
### Thematisation
Utiliser le thème pour la majorité du stylisme des composants est l'approche préférée.
#### Unités de mesure
Évitez d'utiliser des valeurs `px` ou `rem` directement dans les composants stylisés. Les valeurs nécessaires sont généralement déjà définies dans le thème, il est donc recommandé d'utiliser le thème à ces fins.
#### Couleurs
Évitez d'introduire de nouvelles couleurs ; utilisez plutôt la palette existante du thème. Si la palette ne correspond pas, veuillez laisser un commentaire pour que l'équipe puisse rectifier cela.
```tsx
// ❌ Bad, directly specifies style values without utilizing the theme
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
font-weight: 400;
margin-left: 4px;
border-radius: 50px;
`;
```
```tsx
// ✅ Good, utilizes the theme
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.rounded};
`;
```
## Application d'interdiction d'importations de type
Évitez les importations de type. Pour appliquer cette norme, une règle Oxlint vérifie et signale toutes les importations de type. Cela aide à maintenir la cohérence et la lisibilité dans le code TypeScript.
```tsx
// ❌ Bad
import { type Meta, type StoryObj } from '@storybook/react';
// ❌ Bad
import type { Meta, StoryObj } from '@storybook/react';
// ✅ Good
import { Meta, StoryObj } from '@storybook/react';
```
### Pourquoi éviter les importations de type
* **Cohérence** : En évitant les importations de type et en utilisant une seule approche pour les importations de type et de valeur, la base de code reste cohérente dans son style d'importation de module.
* **Lisibilité** : Les importations sans type améliorent la lisibilité du code en clarifiant quand vous importez des valeurs ou des types. Cela réduit l'ambiguïté et facilite la compréhension de l'objectif des symboles importés.
* **Maintenabilité** : Cela améliore la maintenabilité de la base de code car les développeurs peuvent identifier et localiser les importations uniquement de type lors de la révision ou de la modification du code.
### Règle Oxlint
Une règle Oxlint, `typescript/consistent-type-imports`, impose la convention des imports "type-only". Cette règle génère des erreurs ou des avertissements pour toutes les violations d'importations de type.
Veillez à ce que cette règle aborde spécifiquement les rares cas particuliers où se produisent des importations de type involontaires. TypeScript lui-même déconseille cette pratique, comme mentionné dans les [notes de version de TypeScript 3.8](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). Dans la majorité des situations, vous ne devriez pas avoir besoin d'utiliser des importations uniquement de type.
Pour garantir la conformité de votre code à cette règle, assurez-vous d'exécuter Oxlint dans le cadre de votre flux de travail de développement.

View File

@@ -0,0 +1,61 @@
---
title: Travailler avec Figma
info: Apprenez comment vous pouvez collaborer avec le Figma de Twenty
---
Figma est un outil de conception d'interface collaboratif qui aide à surmonter la barrière de communication entre designers et développeurs.
Ce guide explique comment vous pouvez collaborer avec Figma.
## Accès
1. **Accédez au lien partagé :** Vous pouvez accéder au fichier Figma du projet [ici](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty).
2. **Se connecter :** Si vous n'êtes pas déjà connecté, Figma vous invitera à le faire.
Les fonctionnalités clés ne sont disponibles que pour les utilisateurs connectés, telles que le mode développeur et la possibilité de sélectionner un cadre dédié.
<Warning>
Vous ne pourrez pas collaborer efficacement sans compte.
</Warning>
## Structure de Figma
Sur la barre latérale gauche, vous pouvez accéder aux différentes pages du Figma de Twenty. Voici comment elles sont organisées :
* **Page des composants :** C'est la première page. Le designer l'utilise pour créer et organiser les éléments de design réutilisables utilisés dans tout le fichier design. Par exemple, boutons, icônes, symboles ou tout autre composant réutilisable. Elle sert à maintenir la cohérence dans le design.
* **Page principale :** La deuxième page est la page principale, qui montre l'interface utilisateur complète du projet. Vous pouvez appuyer sur ***Play*** pour utiliser le prototype complet de l'application.
* **Pages des fonctionnalités :** Les autres pages sont généralement dédiées aux fonctionnalités en cours de développement. Elles contiennent le design de fonctionnalités ou modules spécifiques de l'application ou du site web. Elles sont généralement encore en cours de développement.
## Conseils utiles
Avec un accès en lecture seule, vous ne pouvez pas modifier le design, mais vous pouvez accéder à toutes les fonctionnalités utiles pour convertir les designs en code.
### Utiliser le mode Dev
Le Mode Dev de Figma améliore la productivité des développeurs en fournissant une navigation simple dans le design, une gestion efficace des ressources, des outils de communication performants, des intégrations de boîte à outils, des extraits de code rapides et des informations clés sur les calques, comblant ainsi le fossé entre design et développement. Vous pouvez en savoir plus sur le mode Dev [ici](https://www.figma.com/dev-mode/).
Basculez vers le mode « Développeur » dans la partie droite de la barre d'outils pour voir les spécifications du design, copier du CSS et accéder aux ressources.
### Utilisez le Prototype
Cliquez sur n'importe quel élément du canvas et appuyez sur le bouton « Play » en haut à droite de l'interface pour accéder à la vue du prototype. Le mode Prototype vous permet d'interagir avec le design comme s'il s'agissait du produit final. Il montre le flux entre les écrans et comment les éléments de l'interface tels que les boutons, les liens ou les menus se comportent lorsqu'ils sont interactifs.
1. **Comprendre les transitions et animations :** En mode Prototype, vous pouvez voir toutes les transitions ou animations ajoutées par un designer entre écrans ou éléments UI, fournissant des instructions visuelles claires aux développeurs sur le comportement et le style attendus.
2. **Clarification de l'implémentation :** Un prototype peut aussi aider à réduire les ambiguïtés. Les développeurs peuvent interagir avec lui pour mieux comprendre la fonctionnalité ou l'apparence de certains éléments.
Pour des détails et instructions plus complets sur l'apprentissage de la plateforme Figma, vous pouvez visiter la [Documentation officielle de Figma](https://help.figma.com/hc/fr).
### Mesurer les distances
Sélectionnez un élément, maintenez la touche `Option` (Mac) ou `Alt` (Windows), puis survolez un autre élément pour voir la distance entre eux.
### Extension Figma pour VSCode (Recommandée)
[Figma pour VS Code](https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension)
vous permet de naviguer et d'inspecter les fichiers design, de collaborer avec les designers, de suivre les changements et d'accélérer la mise en œuvre - le tout sans quitter votre éditeur de texte.
Cela fait partie de nos extensions recommandées.
## Collaboration
1. **Utilisation des commentaires :** N'hésitez pas à utiliser la fonction commentaire en cliquant sur l'icône de bulle dans la partie gauche de la barre d'outils.
2. **Conversation par curseur :** Une fonctionnalité intéressante de Figma est la conversation par curseur. Il suffit d'appuyer sur `;` sur Mac et `/` sur Windows pour envoyer un message si vous voyez quelqu'un d'autre utiliser Figma en même temps que vous.

View File

@@ -0,0 +1,318 @@
---
title: Configuration Locale
icon: laptop-code
description: Le guide pour les contributeurs (ou les développeurs curieux) qui souhaitent exécuter Twenty localement.
---
## Prérequis
<Tabs>
<Tab title="Linux et macOS">
Avant de pouvoir installer et utiliser Twenty, assurez-vous d'installer ce qui suit sur votre ordinateur :
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
<Warning>
`npm` ne fonctionnera pas, vous devez utiliser `yarn` à la place. Yarn est maintenant intégré avec Node.js, vous n'avez donc pas besoin de l'installer séparément.
Vous devez uniquement exécuter `corepack enable` pour activer Yarn si vous ne l'avez pas encore fait.
</Warning>
</Tab>
<Tab title="Windows (WSL)">
1. Installer WSL
Ouvrez PowerShell en tant qu'administrateur et exécutez :
```powershell
wsl --install
```
Vous devriez maintenant voir une invite pour redémarrer votre ordinateur. Si ce n'est pas le cas, redémarrez-le manuellement.
Au redémarrage, une fenêtre PowerShell s'ouvrira et installera Ubuntu. Cela peut prendre un peu de temps.
Une invite pour créer un nom d'utilisateur et un mot de passe pour votre installation Ubuntu s'affichera.
2. Installez et configurez Git
```bash
sudo apt-get install git
git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"
```
3. Installez nvm, Node.js et Yarn
<Warning>
Utilisez `nvm` pour installer la version correcte de `node`. Le fichier `.nvmrc` garantit que tous les contributeurs utilisent la même version.
</Warning>
```bash
sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
Fermez et rouvrez votre terminal pour utiliser nvm. Ensuite, exécutez les commandes suivantes.
```bash
nvm install # installs recommended node version
nvm use # use recommended node version
corepack enable
```
</Tab>
</Tabs>
---
## Étape 1 : Cloner le dépôt Git
Dans votre terminal, exécutez la commande suivante.
<Tabs>
<Tab title="SSH (Recommandé)">
Si vous n'avez pas encore configuré les clés SSH, vous pouvez apprendre à le faire [ici](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
```bash
git clone git@github.com:twentyhq/twenty.git
```
</Tab>
<Tab title="HTTPS">
```bash
git clone https://github.com/twentyhq/twenty.git
```
</Tab>
</Tabs>
## Étape 2 : Positionnez-vous à la racine
```bash
cd twenty
```
Vous devez exécuter toutes les commandes des étapes suivantes depuis la racine du projet.
## Étape 3 : Configurez une base de données PostgreSQL
<Tabs>
<Tab title="Linux">
**Option 1 (préférée) :** Pour préparer votre base de données localement :
Utilisez le lien suivant pour installer PostgreSQL sur votre machine Linux : [Installation de PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Remarque : Vous devrez peut-être ajouter `sudo -u postgres` à la commande précédente `psql` pour éviter les erreurs de permission.
**Option 2 :** Si vous avez installé Docker :
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Mac OS">
**Option 1 (préférée):** Pour préparer votre base de données localement avec `brew` :
```bash
brew install postgresql@16
export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"
brew services start postgresql@16
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Vous pouvez vérifier si le serveur PostgreSQL fonctionne en exécutant :
```bash
brew services list
```
L'installateur peut ne pas créer l'utilisateur `postgres` par défaut lors de l'installation
via Homebrew sur macOS. Au lieu de cela, il crée un rôle PostgreSQL correspondant à votre nom d'utilisateur macOS
(par exemple, "john").
Pour vérifier et créer l'utilisateur `postgres` si nécessaire, suivez ces étapes :
```bash
# Connect to PostgreSQL
psql postgres
or
psql -U $(whoami) -d postgres
```
Une fois à l'invite psql (postgres=#), exécutez :
```bash
# List existing PostgreSQL roles
\du
```
Vous verrez une sortie similaire à :
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
john | Superuser | {}
```
Si vous ne voyez pas de rôle `postgres` listé, passez à l'étape suivante.
Créez le rôle `postgres` manuellement :
```bash
CREATE ROLE postgres WITH SUPERUSER LOGIN;
```
Cela crée un rôle superutilisateur nommé `postgres` avec un accès de connexion.
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
postgres | Superuser | {}
john | Superuser | {}
```
**Option 2 :** Si vous avez installé Docker :
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
Toutes les étapes suivantes doivent être exécutées dans le terminal WSL (dans votre machine virtuelle)
**Option 1 :** Pour provisionner PostgreSQL localement :
Utilisez le lien suivant pour installer PostgreSQL sur votre machine virtuelle Linux : [Installation de PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Remarque : Vous devrez peut-être ajouter `sudo -u postgres` à la commande précédente `psql` pour éviter les erreurs de permission.
**Option 2 :** Si vous avez installé Docker :
Exécuter Docker sur WSL ajoute une couche de complexité supplémentaire.
N'utilisez cette option que si vous êtes à l'aise avec les étapes supplémentaires impliquées, y compris l'activation de [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl).
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
</Tabs>
Vous pouvez maintenant accéder à la base de données à l'adresse `localhost:5432`.
Si vous avez utilisé l'option Docker ci-dessus, les identifiants par défaut sont l'utilisateur `postgres` et le mot de passe `postgres`. Pour les installations natives de PostgreSQL, utilisez les identifiants et les rôles configurés sur votre machine.
## Étape 4 : Configuration d'une base de données Redis (cache)
Twenty nécessite un cache Redis pour offrir les meilleures performances.
<Tabs>
<Tab title="Linux">
**Option 1** : Pour provisionner votre Redis localement :
Utilisez le lien suivant pour installer Redis sur votre machine Linux : [Installation de Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**Option 2 :** Si vous avez installé Docker :
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Mac OS">
**Option 1 (préférée) :** Pour provisionner votre Redis localement avec `brew` :
```bash
brew install redis
```
Démarrez votre serveur Redis :
```bash
brew services start redis
```
**Option 2 :** Si vous avez installé Docker :
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
**Option 1 :** Pour provisionner votre Redis localement :
Utilisez le lien suivant pour installer Redis sur votre machine virtuelle Linux : [Installation de Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**Option 2 :** Si vous avez installé Docker :
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
</Tabs>
Si vous avez besoin d'une interface graphique cliente, nous recommandons [Redis Insight](https://redis.io/insight/) (version gratuite disponible).
## Étape 5 : Configurer les variables d'environnement
Utilisez des variables d'environnement ou des fichiers `.env` pour configurer votre projet. Plus d'informations [ici](/l/fr/developers/self-host/capabilities/setup).
Copiez les fichiers `.env.example` dans `/front` et `/server` :
```bash
cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env
cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
```
<Info>
**Mode multi-espaces de travail:** Par défaut, Twenty s'exécute en mode mono-espace de travail, où un seul espace de travail peut être créé. Pour activer la prise en charge multi-espaces de travail (utile pour tester des fonctionnalités basées sur des sous-domaines), définissez `IS_MULTIWORKSPACE_ENABLED=true` dans le fichier `.env` de votre serveur. Voir [Mode multi-espaces de travail](/l/fr/developers/self-host/capabilities/setup#multi-workspace-mode) pour plus de détails.
</Info>
## Étape 6 : Installation des dépendances
Pour construire le serveur Twenty et injecter des données dans votre base de données, exécutez la commande suivante :
```bash
yarn
```
Notez que `npm` ou `pnpm` ne fonctionneront pas
## Étape 7 : Exécution du projet
<Tabs>
<Tab title="Linux">
Selon votre distribution Linux, le serveur Redis peut être démarré automatiquement.
Sinon, consultez le guide d'installation de [Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) pour votre distribution.
</Tab>
<Tab title="Mac OS">
Redis devrait déjà être en cours d'exécution. Sinon, exécutez :
```bash
brew services start redis
```
</Tab>
<Tab title="Windows (WSL)">
Selon votre distribution Linux, le serveur Redis peut être démarré automatiquement.
Sinon, vérifiez le [guide d'installation de Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) pour votre distribution.
</Tab>
</Tabs>
Configurez votre base de données avec la commande suivante :
```bash
npx nx database:reset twenty-server
```
Démarrez le serveur, le worker et les services frontend :
```bash
npx nx start twenty-server
npx nx worker twenty-server
npx nx start twenty-front
```
Alternativement, vous pouvez démarrer tous les services en même temps :
```bash
npx nx start
```
## Étape 8 : Utiliser Twenty
**Frontend**
Le frontend de Twenty sera en cours d'exécution à [http://localhost:3001](http://localhost:3001).
Vous pouvez vous connecter en utilisant le compte démo par défaut : `tim@apple.dev` (mot de passe : `tim@apple.dev`)
**Backend**
* Le serveur de Twenty sera en fonctionnement à [http://localhost:3000](http://localhost:3000)
* L'API GraphQL est accessible à [http://localhost:3000/graphql](http://localhost:3000/graphql)
* L'API REST est joignable à [http://localhost:3000/rest](http://localhost:3000/rest)
## Résolution des problèmes
Si vous rencontrez un problème, consultez [Dépannage](/l/fr/developers/self-host/capabilities/troubleshooting) pour des solutions.

View File

@@ -0,0 +1,77 @@
---
title: Commandes
icon: terminal
description: Commandes utiles pour développer Twenty.
---
Les commandes peuvent être exécutées depuis la racine du dépôt à l'aide de `npx nx`. Utilisez `npx nx run {project}:{command}` pour un ciblage explicite.
## Lancement de l'application
```bash
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
npx nx start twenty-server # Backend server (http://localhost:3000)
npx nx run twenty-server:worker # Background worker
```
## Base de données
```bash
npx nx database:reset twenty-server # Reset and seed database
npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
```
## Analyse de code
```bash
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
npx nx lint:diff-with-main twenty-server
npx nx lint twenty-front --configuration=fix # Auto-fix
```
## Vérification des types
```bash
npx nx typecheck twenty-front
npx nx typecheck twenty-server
```
## Tests
```bash
# Frontend
npx nx test twenty-front # Jest unit tests
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:test twenty-front # Storybook tests
# Backend
npx nx run twenty-server:test:unit # Unit tests
npx nx run twenty-server:test:integration # Integration tests
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
# Single file (fastest)
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
```
## GraphQL
```bash
npx nx run twenty-front:graphql:generate # Regenerate types
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
```
## Traductions
```bash
npx nx run twenty-front:lingui:extract # Extract strings
npx nx run twenty-front:lingui:compile # Compile translations
```
## Compilation
```bash
npx nx build twenty-shared # Must be built first
npx nx build twenty-front
npx nx build twenty-server
```

View File

@@ -0,0 +1,31 @@
---
title: Contribuer
description: Contribuez au développement open source de Twenty.
---
<Frame>
<img src="/images/user-guide/github/github-header.png" alt="IA" />
</Frame>
## Vue d'ensemble
Twenty est open source et accueille les contributions de la communauté. Que vous corrigiez des bogues, ajoutiez des fonctionnalités ou amélioriez la documentation, vos contributions aident à améliorer Twenty pour tout le monde.
## Comment contribuer
* **Signaler des bogues**: Aidez à identifier et à documenter les problèmes
* **Proposer des fonctionnalités**: Proposez et mettez en œuvre de nouvelles fonctionnalités
* **Améliorer la documentation**: Rendez notre documentation plus claire et plus utile
* **Développement front-end**: Travaillez sur l'interface utilisateur basée sur React
* **Développement back-end**: Contribuez au serveur NestJS
## Prise en main
<CardGroup cols={2}>
<Card title="Signalements de bogues et demandes" icon="bug" href="/l/fr/developers/contribute/capabilities/bug-and-requests">
Signalez des problèmes ou demandez des fonctionnalités
</Card>
<Card title="Développement frontend" icon="browser" href="/l/fr/developers/contribute/capabilities/frontend-development/folder-architecture-front">
Contribuez à l'interface utilisateur
</Card>
</CardGroup>

View File

@@ -0,0 +1,176 @@
---
title: Guide de style
icon: paintbrush
description: Conventions de code et bonnes pratiques pour contribuer à Twenty.
---
## React
### Composants fonctionnels uniquement
Utilisez toujours des composants fonctionnels TSX avec des exports nommés.
```tsx
// ❌ Bad
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good
export function MyComponent() {
return <div>Hello World</div>;
};
```
### Propriétés
Créez un type nommé `{ComponentName}Props`. Utilisez la déstructuration. N'utilisez pas `React.FC`.
```tsx
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
### Pas de propagation de props à partir d'une seule variable
```tsx
// ❌ Bad
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
// ✅ Good
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
```
## Gestion de l'état
### Atomes Jotai pour l'état global
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
```
* Préférez les atomes au prop drilling
* N'utilisez pas `useRef` pour l'état — utilisez `useState` ou des atomes
* Utilisez les familles d'atomes et les sélecteurs pour les listes
### Évitez les re-renders inutiles
* Extrayez `useEffect` et la récupération de données dans des composants sidecar frères
* Préférez les gestionnaires d'événements (`handleClick`, `handleChange`) à `useEffect`
* N'utilisez pas `React.memo()` — corrigez plutôt la cause première
* Limitez l'utilisation de `useCallback` ou `useMemo`
```tsx
// ❌ Bad — useEffect in the same component causes re-renders
export const Page = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <div>{data}</div>;
};
// ✅ Good — extract into sibling
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <></>;
};
export const Page = () => {
const [data] = useAtomState(dataState);
return <div>{data}</div>;
};
```
## TypeScript
* **`type` plutôt que `interface`** — plus flexible, plus facile à composer
* **Littéraux de chaîne plutôt que enums** — sauf pour les enums générés par GraphQL codegen et les API internes des bibliothèques
* **Pas de `any`** — TypeScript strict appliqué
* **Pas d'importations de types** — utilisez des importations classiques (imposées par Oxlint `typescript/consistent-type-imports`)
* **Utilisez [Zod](https://github.com/colinhacks/zod)** pour la validation à l'exécution des objets non typés
## JavaScript
```tsx
// Use nullish-coalescing (??) instead of ||
const value = process.env.MY_VALUE ?? 'default';
// Use optional chaining
onClick?.();
```
## Nommage
* **Variables**: camelCase, explicites (`email` et non `value`, `fieldMetadata` et non `fm`)
* **Constantes**: SCREAMING_SNAKE_CASE
* **Types/Classes**: PascalCase
* **Fichiers/répertoires**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
* **Gestionnaires d'événements**: `handleClick` (pas `onClick` pour la fonction gestionnaire)
* **Props de composant**: préfixez avec le nom du composant (`ButtonProps`)
* **Composants stylés**: préfixez avec `Styled` (`StyledTitle`)
## Stylisation
Utilisez les composants stylés [Linaria](https://github.com/callstack/linaria). Utilisez les valeurs du thème — évitez les `px`, `rem` ou couleurs en dur.
```tsx
// ❌ Bad
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
margin-left: 4px;
`;
// ✅ Good
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
```
## Imports
Utilisez des alias au lieu de chemins relatifs:
```tsx
// ❌ Bad
import { Foo } from '../../../../../testing/decorators/Foo';
// ✅ Good
import { Foo } from '~/testing/decorators/Foo';
import { Bar } from '@/modules/bar/components/Bar';
```
## Structure des dossiers
```
front
└── modules/ # Feature modules
│ └── module1/
│ ├── components/
│ ├── constants/
│ ├── contexts/
│ ├── graphql/ (fragments, queries, mutations)
│ ├── hooks/
│ ├── states/ (atoms, selectors)
│ ├── types/
│ └── utils/
└── pages/ # Route-level components
└── ui/ # Reusable UI components (display, input, feedback, ...)
```
* Les modules peuvent importer depuis d'autres modules, mais `ui/` doit rester sans dépendances
* Utilisez des sous-dossiers `internal/` pour le code privé au module
* Composants de moins de 300 lignes, services de moins de 500 lignes

View File

@@ -0,0 +1,55 @@
---
title: API
icon: plug
description: Des API REST et GraphQL générées à partir du schéma de votre espace de travail.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## API avec un schéma propre à chaque espace de travail
Il n'existe pas de référence API statique pour Twenty. Chaque espace de travail possède son propre schéma — lorsque vous ajoutez un objet personnalisé (par exemple `Invoice`), il obtient immédiatement des points de terminaison REST et GraphQL identiques à ceux des objets intégrés comme `Company` ou `Person`. L'API est générée à partir du schéma, donc les points de terminaison utilisent directement les noms de vos objets et de vos champs — pas d'identifiants opaques.
Votre documentation API spécifique à votre espace de travail est disponible sous **Paramètres → API & Webhooks** après la création d'une clé API. Elle inclut un bac à sable interactif où vous pouvez exécuter de véritables appels sur vos données.
## Deux API
**API principale** — `/rest/` et `/graphql/`
CRUD sur les enregistrements : Personnes, Entreprises, Opportunités, vos objets personnalisés. Interroger, filtrer, parcourir les relations.
**API des métadonnées** — `/rest/metadata/` et `/metadata/`
Gestion du schéma : créer/modifier/supprimer des objets, des champs et des relations. C'est ainsi que vous modifiez votre modèle de données par programmation.
Les deux sont disponibles en REST et en GraphQL. GraphQL ajoute des upserts par lot et la possibilité de parcourir les relations en une seule requête. Les mêmes données sous-jacentes dans les deux cas.
## URL de base
| Environnement | URL de base |
| ------------- | ------------------------- |
| Cloud | `https://api.twenty.com/` |
| Auto-hébergé | `https://{your-domain}/` |
## Authentification
```
Authorization: Bearer YOUR_API_KEY
```
Créez une clé API dans **Paramètres → API & Webhooks → + Créer une clé**. Copiez-la immédiatement — elle n'est affichée qu'une seule fois. Les clés peuvent être limitées à un rôle spécifique dans **Paramètres → Membres → Rôles → onglet Attribution** afin de restreindre ce à quoi elles peuvent accéder.
<VimeoEmbed videoId="928786722" title="Création d'une clé API" />
Pour un accès basé sur OAuth (applications externes agissant au nom des utilisateurs), voir [OAuth](/l/fr/developers/extend/oauth).
## Opérations par lot
REST et GraphQL prennent tous deux en charge le traitement par lot jusqu'à 60 enregistrements par requête — création, mise à jour ou suppression. GraphQL prend également en charge l'upsert par lot (création ou mise à jour en un seul appel) en utilisant des noms au pluriel comme `CreateCompanies`.
## Limites de débit
| Limite | Valeur |
| ------------- | ---------------------------- |
| Requêtes | 100 par minute |
| Taille du lot | 60 enregistrements par appel |

View File

@@ -0,0 +1,124 @@
---
title: Configuration de l'application
description: Déclarez l'identité de votre application, son rôle par défaut, ses variables et ses métadonnées de la marketplace avec defineApplication.
icon: rocket
---
Chaque application doit avoir exactement un appel à `defineApplication`. Il déclare :
* **Identité** — identifiant universel, nom d'affichage, description.
* **Autorisations** — le rôle sous lequel s'exécutent ses fonctions logiques et ses composants front-end.
* **Variables** *(facultatif)* — paires clé–valeur exposées à votre code en tant que variables d'environnement.
* **Hooks de pré-installation / post-installation / désinstallation** *(facultatif)* — voir [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
Notes :
* Les champs `universalIdentifier` sont des identifiants déterministes que vous contrôlez. Générez-les une fois et conservez-les stables entre les synchronisations.
* `applicationVariables` deviennent des variables d'environnement pour vos fonctions et vos composants front-end. Dans les fonctions logiques (côté serveur), elles sont disponibles sous `process.env.VARIABLE_NAME`. Dans les composants front-end, utilisez `getApplicationVariable('VARIABLE_NAME')` depuis `twenty-sdk/front-component`. Les variables marquées avec `isSecret: true` sont uniquement injectées dans les fonctions logiques. Les composants front-end ne reçoivent que des variables non secrètes.
* Le rôle par défaut est détecté automatiquement à partir du fichier de rôle marqué avec [`defineApplicationRole()`](/l/fr/developers/extend/apps/config/roles) — vous n’avez pas besoin d’y faire référence depuis `defineApplication()`.
* Les fonctions de pré-installation, de post-installation et de désinstallation sont détectées automatiquement lors de la construction du manifeste — vous n'avez pas besoin de les référencer dans `defineApplication()`.
* Le passage explicite de `defaultRoleUniversalIdentifier` est toujours pris en charge pour des raisons de rétrocompatibilité, mais il est obsolète au profit de `defineApplicationRole()`.
* `serverVariables` sont des configurations et des secrets au niveau de l’instance (par exemple des clés d’API). Contrairement à `applicationVariables`, ils ne déclarent aucune valeur dans le manifeste — l’opérateur de l’espace de travail les renseigne dans les paramètres de l’application, et ils sont injectés dans les fonctions logiques uniquement une fois définis.
* Les deux types de variable acceptent `isDeprecated: true`. Utilisez-la pour mettre une variable hors service plutôt que de la supprimer : garder la clé déclarée préserve la valeur stockée (la supprimer détruit la valeur saisie par l’utilisateur), et la variable est toujours injectée, de sorte que votre code peut s’y rabattre — `process.env.NEW_API_KEY ?? process.env.API_KEY`. Une variable dépréciée disparaît des paramètres de l'application lorsqu'elle n'a plus de valeur et n'est jamais prise en compte dans la vérification de la configuration de l'application, donc `isDeprecated` l'emporte sur `isRequired`.
* Pour afficher une interface utilisateur de configuration personnalisée dans l’onglet **Settings** de l’application (à la place de la section de configuration des variables par défaut), déclarez un composant frontal avec [`defineSettingsFrontComponent()`](/l/fr/developers/extend/apps/layout/front-components#custom-settings-component) dans son propre fichier. Un seul est autorisé par application. Les sections gérées par le système (mise à niveau automatique, App URL, connexions) restent toujours visibles.
## Types de variables
`applicationVariables` et `serverVariables` acceptent tous deux un `type` optionnel (et, pour `SELECT` / `MULTI_SELECT`, une liste `options`). Types pris en charge : `TEXT` (par défaut), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
Le `type` affecte uniquement **la présentation et la validation** — il sélectionne le champ de saisie correspondant dans l’interface des paramètres de l’espace de travail (un bouton bascule, un champ numérique, une liste déroulante, un sélecteur de date, un éditeur JSON, …) et permet au build de valider votre configuration (par exemple, `SELECT` / `MULTI_SELECT` doivent déclarer des `options` non vides). Il ne change **pas** la façon dont la valeur atteint votre code.
Les valeurs sont **toujours injectées sous forme de chaînes de caractères** — cela est inhérent aux variables d’environnement (`process.env.*` est uniquement composé de chaînes). Lorsque votre fonction logique s’exécute, l’exécuteur sérialise chaque valeur selon son `type` déclaré lors de la construction de `process.env`, de sorte que le format de chaîne soit cohérent, quelle que soit la manière dont la valeur a été définie (valeur par défaut du manifeste, interface des paramètres ou version précédente) :
| Type | chaîne `process.env` |
| ------------------------------------- | ---------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | la valeur brute (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | chaîne décimale (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | tableau JSON (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | objet JSON (`'{"retries":3}'`) |
Analysez la chaîne pour la convertir dans le type attendu :
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
Il en va de même pour les composants front qui lisent les valeurs via `getApplicationVariable('VARIABLE_NAME')` — la valeur renvoyée est une chaîne ; analysez-la selon vos besoins.
## Rôle de fonction par défaut
Le rôle déclaré avec [`defineApplicationRole()`](/l/fr/developers/extend/apps/config/roles) contrôle ce à quoi les fonctions logiques de l’application et les composants front peuvent accéder :
* Les jetons d'exécution injectés dans vos fonctions logiques sont dérivés de ce rôle. Un appel agissant en tant que personne est davantage limité par ce que cette personne peut faire, de sorte qu'il ne peut jamais dépasser l'un ou l'autre. Voir [Les accès qu'un appel utilise](/l/fr/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* Le client d'API typé est limité aux autorisations accordées à ce rôle.
* Appliquez le principe du moindre privilège : déclarez uniquement les autorisations dont vos fonctions ont besoin.
Lorsque vous générez une nouvelle application, la CLI crée un fichier de rôle par défaut à `src/roles/default-role.ts`. Voir [Rôles et autorisations](/l/fr/developers/extend/apps/config/roles) pour la référence complète.
## Métadonnées de la marketplace
Si vous prévoyez de [publier votre application](/l/fr/developers/extend/apps/operations/publishing), ces champs optionnels contrôlent la façon dont elle apparaît dans la marketplace :
| Champ | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Nom de l'auteur ou de l'entreprise |
| `category` | Catégorie de l'application pour le filtrage dans la marketplace |
| `logo` | Chemin vers le logo de votre application empaqueté dans `public/` (p. ex., `public/logo.png`) |
| `galleryImages` | Tableau de chemins d’images de galerie empaquetées dans `public/` (p. ex., `public/screenshot-1.png`) |
| `aboutDescription` | Description markdown plus longue pour l'onglet "À propos". S'il est omis, la marketplace utilise le `README.md` du package sur npm |
| `websiteUrl` | Lien vers votre site web |
| `termsUrl` | Lien vers les conditions d'utilisation |
| `emailSupport` | Adresse e-mail du support |
| `issueReportUrl` | Lien vers le système de suivi des problèmes |
<Note>
`logoUrl` et `screenshots` sont des alias obsolètes de `logo` et `galleryImages`. Les URL absolues externes (`http://` ou `https://`) ne sont pas prises en charge pour ces champs : elles sont ignorées avec un avertissement au moment de la compilation. Placez plutôt les images dans le dossier `public/` de votre application.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Hooks d'installation
description: Exécutez de la logique pendant le cycle de vie d'installation, de mise à niveau ou de désinstallation — préremplissez des données, sauvegardez des enregistrements, validez la mise à niveau, nettoyez les ressources externes.
icon: wrench
---
Les hooks d'installation sont des fonctions logiques spéciales qui s'exécutent pendant le cycle de vie d'installation, de mise à niveau ou de désinstallation. Ils partagent le même environnement d'exécution que les [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) classiques, mais ils sont déclarés avec leurs propres fonctions de définition et ne relèvent pas du modèle de déclencheur habituel (HTTP, cron, événements de base de données). Les hooks d'installation reçoivent un `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` est `undefined` lors d'une nouvelle installation) ; le hook de désinstallation reçoit un `UninstallPayload` (`{ version?: string }` — la version supprimée).
Chaque application peut définir **au maximum un seul** hook de chaque type (pré-installation, post-installation, désinstallation). La génération du manifeste renvoie une erreur si plus d'un hook de n'importe quel type est détecté.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## En un coup d'œil
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Exécutions | Avant la migration des métadonnées — le schéma et les données **précédents** sont toujours intacts | Après la migration et la génération du SDK — le **nouveau** schéma est en place |
| Exécution | Toujours synchrone ; bloque l'installation | Asynchrone par défaut (mis en file d'attente, 3 nouvelles tentatives) ; mode synchrone en option via `shouldRunSynchronously: true` |
| En cas d'échec | L'installation est **abandonnée** avant toute modification du schéma | Asynchrone : jusqu'à 3 réessais. Synchrone : l'appelant reçoit `POST_INSTALL_ERROR` (les modifications de schéma **ne** sont pas annulées) |
| Utilisation typique | Sauvegarder ou corriger les données qu'une migration ferait perdre ; refuser une mise à niveau risquée en levant une exception | Initialiser des données par défaut, configurer l'espace de travail, enregistrer des ressources externes |
**Règle empirique :** privilégiez post-install par défaut. Ne recourez à la pré-installation que lorsque la migration elle-même est destructive et que vous devez intercepter l'état précédent avant qu'il ne disparaisse.
| Vous souhaitez... | Utiliser |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Initialiser des données, configurer l'espace de travail, enregistrer des ressources externes | `post-install` |
| Travail de longue durée qui ne doit pas bloquer la réponse d'installation | `post-install` (mode asynchrone par défaut, avec nouvelles tentatives du worker) |
| Configuration rapide dont l'appelant dépend immédiatement après le retour de l'installation | `post-install` avec `shouldRunSynchronously: true` |
| Lire ou sauvegarder des données que la migration à venir ferait perdre | `pre-install` |
| Rejeter une mise à niveau qui corromprait des données existantes | `pre-install` (lancer une exception depuis le gestionnaire) |
| Réconciliation à chaque mise à niveau | N'importe quel hook avec `shouldRunOnVersionUpgrade: true` |
## Comportement partagé par les deux hooks
* La configuration est une configuration `defineLogicFunction` moins les paramètres de déclencheur, plus `shouldRunOnVersionUpgrade`.
* **Quand il s'exécute** : uniquement lors des nouvelles installations, par défaut. Définissez `shouldRunOnVersionUpgrade: true` pour qu'il s'exécute également lors des mises à niveau. Utilisez `previousVersion` / `newVersion` pour bifurquer selon le chemin de mise à niveau.
* **L'idempotence est importante** : le post-install asynchrone peut être relancé, et chaque hook est réexécuté lors des mises à niveau lorsque `shouldRunOnVersionUpgrade` est activé.
* L'environnement habituel des fonctions logiques (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) est injecté, ce qui vous permet d'appeler l'API Twenty avec le jeton de votre application.
* Le hook est rattaché automatiquement au manifeste de l'application au moment de la compilation (`preInstallLogicFunction` / `postInstallLogicFunction`) — rien à référencer dans [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
* La valeur par défaut de `timeoutSeconds` est 300 pour permettre des tâches de configuration plus longues comme l'initialisation des données.
* **Non exécuté en mode dev** : `yarn twenty dev` ignore le flux d'installation et synchronise directement les fichiers, donc les hooks ne s'exécutent jamais dans ce cas. Déclenchez-les manuellement à la place :
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="S'exécute après l'application de la migration des métadonnées de l'espace de travail">
S'exécute une fois que votre application a terminé son installation : métadonnées synchronisées, client SDK généré, nouveau schéma interrogeable. Exemple — initialiser un enregistrement par défaut lors des nouvelles installations :
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Le drapeau `shouldRunSynchronously` contrôle le modèle d'exécution :
* `false` *(par défaut)* — mis en file d'attente dans la file de messages (`retryLimit: 3`) et exécuté par un worker. La réponse d'installation est renvoyée dès que la tâche est mise en file d'attente. **À utiliser pour les travaux de longue durée** — initialisation de grands ensembles de données, API tierces lentes.
* `true` — exécuté en ligne pendant le flux d'installation. La requête d'installation est bloquée jusqu'à ce que le gestionnaire ait terminé ; une erreur levée apparaît comme `POST_INSTALL_ERROR` pour l'appelant (aucune nouvelle tentative). **À utiliser pour les travaux rapides qui doivent être terminés avant la réponse.** La migration a déjà été appliquée à ce stade, donc un échec n'annule pas les modifications du schéma — il ne fait que remonter l'erreur.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="S'exécute avant l'application de la migration des métadonnées de l'espace de travail">
S'exécute avant la migration des métadonnées, sur le schéma **précédent** — l'endroit idéal pour sauvegarder des données qu'une migration ferait perdre, ou pour refuser une mise à niveau risquée. Avant l'exécution, le serveur effectue une « synchronisation réduite » purement additive qui enregistre uniquement la fonction de pré-installation de la nouvelle version ; tout le reste — les objets, champs et données de la version précédente — reste intact lorsque votre gestionnaire s'exécute.
La pré-installation est toujours **synchrone** et bloque l'installation. Si le gestionnaire lève une exception, l'installation est abandonnée avant toute modification du schéma — l'espace de travail reste sur la version précédente dans un état cohérent. C'est intentionnel : la pré-installation est votre dernière chance de refuser une mise à niveau risquée.
Exemple — copier les valeurs d'un champ hérité avant que la migration ne le supprime :
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## Hook de désinstallation
`defineUninstallLogicFunction` déclare un hook qui s'exécute lorsqu'un utilisateur désinstalle votre application. Il s'exécute **avant** que les métadonnées, les données et le code de l'application ne soient supprimés — une fois que la migration de suppression est exécutée, il ne reste plus rien à exécuter — de sorte que votre gestionnaire peut encore interroger les objets et enregistrements de l'application. Utilisez-le pour nettoyer les ressources externes : déprovisionner les ressources d'API, supprimer les bots restants, révoquer les webhooks.
Notes :
* Le hook fonctionne selon le principe du "best effort" : il s'exécute de manière synchrone, mais un échec est consigné dans les journaux et **ne bloque jamais la désinstallation** — le nettoyage ne doit pas rendre une application impossible à supprimer.
* Il reçoit `UninstallPayload` (`{ version?: string }` — la version supprimée).
* Il **ne** s'exécute **pas** lorsqu'une nouvelle installation ayant échoué est annulée — l'application n'a jamais terminé son installation.
* Le hook ne peut pas s'exécuter après la suppression de l'application, donc le nettoyage externe qui dépend des données de l'application (par exemple des identifiants de bots stockés dans des enregistrements) doit être effectué ici, et non dans une tâche planifiée externe.
* Comme les hooks d'installation, il **n'est pas exécuté en mode dev** — déclenchez-le manuellement à la place :
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: Vue d'ensemble
description: Configurer l'application elle-même — son identité, ses autorisations par défaut et ce qui s'exécute au moment de l'installation.
icon: screwdriver-wrench
---
La **couche de configuration** d'une application Twenty est ce qui décrit l'application *à la plateforme* — son identité, les autorisations qu'elle détient et le code qui s'exécute lors de l'installation ou de la mise à niveau. Ces déclarations n'ajoutent pas de nouvelles structures de données ni de comportement à l'exécution ; elles indiquent à Twenty *qui est l'application* et *comment la configurer*.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Configuration de l'application" icon="rocket" href="/l/fr/developers/extend/apps/config/application">
`defineApplication` — identité, rôle par défaut, variables, métadonnées de la marketplace.
</Card>
<Card title="Rôles et autorisations" icon="shield-halved" href="/l/fr/developers/extend/apps/config/roles">
`defineRole` — déclarez ce que les fonctions logiques de votre application peuvent lire et écrire.
</Card>
<Card title="Hooks d'installation" icon="wrench" href="/l/fr/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction` et `defineUninstallLogicFunction` — sauvegarder les données, préremplir les valeurs par défaut, valider les mises à niveau, nettoyer lors de la suppression.
</Card>
</CardGroup>
## Comment les éléments s'articulent
* L'**application** est le point d'entrée. Chaque application comporte exactement un appel à `defineApplication()` qui pointe vers un **Rôle** par défaut.
* Le **Rôle** contrôle ce que les fonctions logiques et les composants frontaux de l'application peuvent lire et écrire. Suivez le principe du moindre privilège : accordez uniquement les autorisations dont votre code a réellement besoin.
* Les **hooks d'installation** s'exécutent lors de l'installation ou de la mise à niveau — la pré-installation avant la migration des métadonnées (ce qui permet de refuser une mise à niveau risquée), la post-installation après la migration (ce qui permet de préremplir les données par défaut selon le nouveau schéma). Le hook de désinstallation s’exécute juste avant que l’application ne soit supprimée, ce qui lui permet de nettoyer les ressources externes tant que les données de l’application sont encore lisibles.
<Note>
Les hooks d'installation partagent l'environnement d'exécution des [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) — même signature de handler, mêmes variables d'environnement, même client d'API typé — mais ils sont déclarés avec leurs propres fonctions `define` et vivent en dehors du modèle de déclencheurs habituel (HTTP, cron, événements de base de données).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: Ressources publiques
description: Distribuez des fichiers statiques — images, icônes, polices — avec votre application via le dossier public/.
icon: folder-open
---
Le dossier `public/` à la racine de votre application contient des fichiers statiques — images, icônes, polices ou toute autre ressource dont votre application a besoin à l'exécution. Ces fichiers sont automatiquement inclus dans les builds, synchronisés pendant le mode de développement et téléversés vers le serveur.
Les fichiers placés dans `public/` sont :
* **Accessibles publiquement** — une fois synchronisés avec le serveur, les fichiers sont servis à une URL publique. Aucune authentification n'est nécessaire pour y accéder.
* **Disponibles dans les composants frontaux** — utilisez les URL des ressources pour afficher des images, des icônes ou tout média dans vos composants React.
* **Disponibles dans les fonctions logiques** — référencez des URL de ressources dans des e-mails, des réponses d'API ou toute logique côté serveur.
* **Utilisés pour les métadonnées de marketplace** — les champs `logo` et `galleryImages` dans `defineApplication()` référencent des fichiers de ce dossier (p. ex., `public/logo.png`). Celles-ci sont affichées dans la marketplace lorsque votre application est publiée. Les URL absolues externes sont ignorées pour ces champs — intégrez plutôt les images dans `public/`. Chaque image ne doit pas dépasser 10 Mo.
* **Synchronisés automatiquement en mode dev** — lorsque vous ajoutez, mettez à jour ou supprimez un fichier dans `public/`, il est automatiquement synchronisé avec le serveur. Aucun redémarrage nécessaire.
* **Inclus dans les builds** — `yarn twenty dev:build` regroupe toutes les ressources publiques dans la sortie de distribution.
## Accéder aux ressources publiques avec `getPublicAssetUrl`
Utilisez l'utilitaire `getPublicAssetUrl` de `twenty-sdk` pour obtenir l'URL complète d'un fichier dans votre répertoire `public/`. Cela fonctionne à la fois dans les **fonctions logiques** et les **composants frontaux**.
**Dans une fonction logique :**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**Dans un composant frontal :**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
L'argument `path` est relatif au dossier `public/` de votre application. Les deux `getPublicAssetUrl('logo.png')` et `getPublicAssetUrl('public/logo.png')` aboutissent à la même URL — le préfixe `public/` est supprimé automatiquement s'il est présent.

View File

@@ -0,0 +1,201 @@
---
title: Rôles et autorisations
description: Déclarez quels objets et champs les fonctions de logique et les composants frontaux de votre application peuvent lire et écrire.
icon: shield-halved
---
Un **rôle** est un ensemble d’autorisations : quels objets une application peut lire ou écrire, quels champs elle peut voir et quelles fonctionnalités de plateforme elle peut utiliser. Les fonctions de logique et les composants frontaux de chaque application héritent des autorisations du rôle marqué avec `defineApplicationRole()` (voir [Le rôle de fonction par défaut](#the-default-function-role) ci-dessous).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## Sécurité au niveau des lignes
Les autorisations d’objet et de champ déterminent à *quels objets et champs* un rôle peut accéder. Les **prédicats d’autorisation au niveau des lignes** vont plus loin et déterminent *quels enregistrements* un rôle peut voir et sur lesquels il peut agir — par exemple, un rôle en libre-service où chaque utilisateur externe ne voit que ses propres enregistrements.
Déclarez les prédicats avec `rowLevelPermissionPredicates` sur le rôle. Comme le reste du manifeste, chaque prédicat porte son propre `universalIdentifier` et fait référence à un objet et à un champ par leur `universalIdentifier`, un `operand` et (facultativement) un champ workspaceMember dont la valeur est injectée au moment de la requête — vous pouvez ainsi exprimer : "la relation de propriétaire de l’enregistrement **est** le membre de l’espace de travail actuel" :
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
Comme les prédicats sont livrés dans le manifeste, ils sont créés, mis à jour et supprimés en même temps que le rôle à chaque installation et mise à niveau — il n’y a aucune étape distincte après l’installation à garder synchronisée.
### Combiner des prédicats avec des groupes
Par défaut, les prédicats d’un rôle sont combinés avec `AND`. Pour en combiner certains avec `OR` (ou pour imbriquer la logique), déclarez une entrée `rowLevelPermissionPredicateGroups` et faites pointer chaque prédicat vers celle-ci via `predicateGroupUniversalIdentifier`. Ce rôle permet à un partenaire de voir une opportunité qu’il **possède** ou pour laquelle il est le **point de contact** :
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
Notes :
* Donnez à chaque prédicat et groupe un `universalIdentifier` stable (n’importe quel uuid) — il sert de clé à l’entité lors des mises à niveau, et les prédicats référencent les groupes via celui-ci.
* Les prédicats peuvent référencer des objets et des champs appartenant à votre application ou aux objets standard de Twenty.
* La sécurité au niveau des lignes est appliquée pour les espaces de travail sur les offres qui l’incluent ; les prédicats sont tout de même synchronisés sur les autres offres, ils ne sont simplement pas appliqués.
## Le rôle de fonction par défaut
Lorsque vous générez une nouvelle application, la CLI crée un fichier de rôle par défaut déclaré avec `defineApplicationRole()` :
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` est un léger wrapper autour de `defineRole()` qui marque le rôle utilisé comme rôle par défaut de votre application au moment de l’installation. La validation est identique à `defineRole`, mais le pipeline de build connecte automatiquement son `universalIdentifier` au champ `defaultRoleUniversalIdentifier` du manifeste de l’application — vous n’avez donc pas besoin de le référencer vous-même depuis [`defineApplication`](/l/fr/developers/extend/apps/config/application).
Notes :
* Exactement **un** `defineApplicationRole(...)` est autorisé par application — la génération du manifeste échouera si elle en trouve plus d’un.
* Utilisez `defineRole()` (et non `defineApplicationRole()`) pour tous les rôles **supplémentaires** fournis par votre application.
* Le fait de définir explicitement `defaultRoleUniversalIdentifier` dans `defineApplication()` est toujours pris en charge pour assurer la rétrocompatibilité, mais c’est obsolète et remplacé par `defineApplicationRole()`.
## Meilleures pratiques
* Partez du rôle généré par le gabarit, puis restreignez-le progressivement — par défaut, il accorde un large accès en lecture, ce qui correspond rarement à ce que vous voulez en production.
* Remplacez `objectPermissions` et `fieldPermissions` par les objets et champs dont vos fonctions ont réellement besoin.
* `permissionFlagUniversalIdentifiers` contrôlent l’accès aux capacités au niveau de la plateforme. Maintenez-les au strict minimum.
* Voir un exemple fonctionnel : [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).

View File

@@ -0,0 +1,52 @@
---
title: Extension des objets
description: Ajoutez des champs aux objets standard de Twenty (Person, Company, …) ou aux objets d'autres applications en utilisant defineField.
icon: wand-magic-sparkles
---
Utilisez `defineField()` pour ajouter un champ à un objet que vous ne possédez pas — un objet standard de Twenty comme Person ou Company, ou un objet fourni par une autre application installée. Contrairement aux champs en ligne déclarés à l'intérieur de [`defineObject`](/l/fr/developers/extend/apps/data/objects), les champs autonomes nécessitent un `objectUniversalIdentifier` pour préciser l'objet qu'ils étendent.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
## Points clés
* `objectUniversalIdentifier` identifie l'objet cible. Pour les objets standard de Twenty, importez la constante depuis `twenty-sdk` :
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* Lorsque vous définissez des champs **en ligne dans `defineObject()`**, vous n'avez **pas** besoin de `objectUniversalIdentifier` — il est hérité de l'objet parent.
* `defineField()` est la seule manière d'ajouter des champs à des objets que vous n'avez pas créés avec `defineObject()`.
* L’emplacement du fichier est à votre discrétion. La convention est `src/fields/\<name>.field.ts`, mais le SDK détecte les champs n'importe où dans `src/`.
* La `writability` peut également être définie par champ : `MetadataWritability.APPLICATION` limite les écritures à la logique interne de votre application, `MetadataWritability.SYSTEM` est réservé aux données gérées par la plateforme. La `writability` d’un champ ne peut être que plus stricte que le niveau de son objet, et les lectures ne sont pas affectées. Voir [Objects](/l/fr/developers/extend/apps/data/objects) pour la sémantique complète.
* Pour ajouter un onglet à une mise en page standard (par exemple la page de détail de Task ou de Company), utilisez [`definePageLayoutTab`](/l/fr/developers/extend/apps/layout/page-layouts#definepagelayouttab) avec `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` depuis `twenty-sdk/define`.
## Ajouter une relation à un objet existant
Pour ajouter un champ de relation (par exemple pour lier votre objet personnalisé à un `Person` standard), utilisez `defineField()` avec `FieldType.RELATION`. Le modèle est le même que pour les relations en ligne, mais avec `objectUniversalIdentifier` défini explicitement. Voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle bidirectionnel.

View File

@@ -0,0 +1,143 @@
---
title: Objets
description: Déclarez de nouveaux types d’enregistrements — des tables personnalisées avec leurs propres champs — à l’aide de defineObject.
icon: table
---
Les **objets** personnalisés sont de nouveaux types d’enregistrements que votre application ajoute à un espace de travail — carte postale, facture, abonnement, tout ce qui est spécifique à votre domaine. Chaque objet déclare son schéma (champs, relations, valeurs par défaut) et un identifiant universel stable qui survit aux synchronisations et aux déploiements.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
## Points clés
* Le `universalIdentifier` doit être unique et stable entre les déploiements.
* Chaque champ nécessite un `name`, un `type`, un `label` et son propre `universalIdentifier` stable.
* Le tableau `fields` est facultatif — vous pouvez définir des objets sans champs personnalisés.
* `openRecordIn` définit où les enregistrements de cet objet s’ouvrent lorsqu’on clique dessus : `ObjectOpenRecordIn.USER_CHOICE` (la valeur par défaut, qui suit la préférence de chaque membre de l’espace de travail dans Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` ou `ObjectOpenRecordIn.RECORD_PAGE`. Épinglez-le sur `RECORD_PAGE` pour les enregistrements qui ont besoin d’une page complète pour être utilisables, comme les flux de travail et les tableaux de bord, ou sur `SIDE_PANEL` pour les enregistrements qui n’ont de sens qu’en tant que panneau rapide, comme les événements de calendrier.
* `writability` contrôle qui peut écrire des enregistrements de l’objet en général, avant que les autorisations de rôle ne s’appliquent : `MetadataWritability.OPEN` (la valeur par défaut — les rôles de l’espace de travail décident), `MetadataWritability.APPLICATION` (seules les fonctions logiques propres à votre application peuvent créer, mettre à jour ou supprimer des enregistrements ; utilisez ceci pour des objets de type configuration dont les enregistrements confèrent un comportement, afin que les membres de l’espace de travail ayant un large accès aux enregistrements ne puissent pas les modifier via l’API), ou `MetadataWritability.SYSTEM` (réservé aux données gérées par la plateforme). Les opérations de lecture ne sont pas affectées — ceci est imposé côté serveur, contrairement à `isUIEditable`, qui ne fait que masquer les éléments de l’interface utilisateur. Cela existe aussi au niveau de chaque champ, où cela ne peut être que plus strict que le niveau de l’objet.
* Les champs en ligne définis ici n’ont **pas** besoin d’un `objectUniversalIdentifier` — il est hérité de l’objet parent. Utilisez [`defineField()`](/l/fr/developers/extend/apps/data/extending-objects) pour ajouter des champs aux objets que vous ne possédez pas.
* Vous pouvez générer de nouveaux objets avec `yarn twenty dev:add object`, qui vous guide à travers le nommage, les champs et les relations. Voir [Architecture → Scaffolding entities](/l/fr/developers/extend/apps/getting-started/scaffolding).
<Note>
**Les champs de base sont ajoutés automatiquement.** Lorsque vous définissez un objet personnalisé, Twenty crée pour vous des champs standard comme `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` et `deletedAt`. Vous n’avez pas besoin de les déclarer dans votre tableau `fields` — uniquement vos champs personnalisés. Vous pouvez remplacer un champ par défaut en en déclarant un avec le même nom, mais c’est rarement une bonne idée.
</Note>
## Types de champ
L’ensemble complet des valeurs de `FieldType`, exportées depuis `twenty-sdk/define` :
| Catégorie | Types |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Texte | `TEXT`, `RICH_TEXT`, `ARRAY` (de chaînes), `RAW_JSON` |
| Numérique | `NUMBER` (`universalSettings.dataType` : `'float'` / `'int'` / `'bigint'`), `NUMERIC` (précision arbitraire), `RATING`, `POSITION` |
| Dates | `DATE`, `DATE_TIME` |
| Choix | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Composés | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identifiants et relations | `UUID`, `RELATION`, `MORPH_RELATION` (voir [Relations](/l/fr/developers/extend/apps/data/relations)) |
| Système | `TS_VECTOR` (vecteur de recherche en texte intégral, géré par le serveur) |
Les types composés stockent plusieurs sous-champs (par exemple `FULL_NAME` = prénom + nom de famille ; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` et `MULTI_SELECT` nécessitent un tableau `options` comme dans l’exemple ci-dessus.
## Valeurs par défaut
Les valeurs par défaut de type chaîne littérale doivent être entourées de guillemets simples **à l’intérieur** de la chaîne — `defaultValue: "'Draft'"`, et non `defaultValue: "Draft"`. C’est pourquoi le champ `status` ci-dessus utilise `` `'${PostCardStatus.DRAFT}'` ``.
Les chaînes sans guillemets sont réservées aux valeurs par défaut calculées, évaluées lors de la création d’un enregistrement :
* `'uuid'` — génère un UUID (pour les champs `UUID`)
* `'now'` — l’horodatage actuel (pour les champs `DATE_TIME`)
La même convention s’applique aux sous-champs de type chaîne des valeurs par défaut composites (par exemple `{ source: "'MANUAL'" }` sur un champ `ACTOR`) ainsi qu’aux valeurs `SELECT`/`MULTI_SELECT`. Une valeur par défaut littérale de type chaîne laissée sans guillemets génère un avertissement lors de la compilation de votre application.
## Nullabilité
`isNullable` détermine si un champ accepte `NULL`. Sa valeur par défaut est `true` — omettez-le pour les champs facultatifs. Définissez `isNullable: false` pour rendre un champ obligatoire au niveau de la base de données.
Les modifications de `isNullable` sont appliquées à chaque synchronisation, y compris celles qui mettent à jour un champ existant — vous pouvez donc modifier la nullabilité d’un champ en éditant le manifeste puis en relançant la synchronisation.
<Note>
**Rendre un champ existant non nullable nécessite une valeur par défaut.** Lorsque vous modifiez un champ avec `isNullable: false`, vous devez également fournir une `defaultValue` non nulle. La valeur par défaut remplit rétroactivement toutes les lignes `NULL` existantes avant que la contrainte `NOT NULL` ne soit appliquée ; sans cela, la synchronisation échoue avec `Default value cannot be null for non-nullable fields`. Les champs de relation et les champs `TS_VECTOR` sont toujours nullables, donc `isNullable` n’a aucun effet sur eux.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## Et après
* **Connectez cet objet aux autres** — voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle de relation bidirectionnelle.
* **Ajoutez des champs aux objets d’autres applications** — voir [Extending Objects](/l/fr/developers/extend/apps/data/extending-objects) pour `defineField()`.
* **Afficher cet objet dans l’interface utilisateur** — voir [Éléments du menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items) pour ajouter une entrée dans la barre latérale ; voir [Vues](/l/fr/developers/extend/apps/layout/views) pour ajouter des configurations de liste personnalisées.

View File

@@ -0,0 +1,101 @@
---
title: Vue d'ensemble
description: Façonnez les données que votre application ajoute à un espace de travail — objets, champs et relations.
icon: database
---
La **couche de données** d’une application Twenty correspond aux données que votre application *ajoute* à un espace de travail — les nouveaux types d’enregistrements qu’elle déclare, les colonnes qu’elle ajoute aux objets existants et la manière dont ces enregistrements sont reliés les uns aux autres.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Objets" icon="table" href="/l/fr/developers/extend/apps/data/objects">
`defineObject` — déclarez de nouveaux types d’enregistrements avec leurs propres champs.
</Card>
<Card title="Extension des objets" icon="wand-magic-sparkles" href="/l/fr/developers/extend/apps/data/extending-objects">
`defineField` — ajoutez des champs aux objets standard ou à ceux d’autres applications.
</Card>
<Card title="Relations" icon="diagram-project" href="/l/fr/developers/extend/apps/data/relations">
Connexions bidirectionnelles `MANY_TO_ONE` / `ONE_TO_MANY` entre objets.
</Card>
<Card title="Types d'activités de la chronologie" icon="clock-rotate-left" href="/l/fr/developers/extend/apps/data/timeline-activity-types">
Définissez des événements d'audit automatiques et des événements d'application explicites pour les chronologies d'enregistrements.
</Card>
</CardGroup>
## Aperçu des entités
| Entité | Objectif | Défini avec |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Objet** | Un nouveau type d’enregistrement personnalisé (par ex. PostCard, Invoice) avec ses propres champs | `defineObject()` |
| **Champ** | Une colonne d’un objet. Les champs autonomes peuvent étendre des objets que vous n’avez pas créés (par ex. ajouter `loyaltyTier` à Company) | `defineField()` |
| **Relation** | Un lien bidirectionnel entre deux objets — les deux côtés étant déclarés comme des champs | `defineField()` avec `FieldType.RELATION` |
| **Index** | Un index de base de données pour accélérer une requête récurrente sur l’un de vos objets | `defineIndex()` |
| **Type d'activité de la chronologie** | Un événement d'audit automatique ou un événement de domaine explicite affiché dans les chronologies des enregistrements. | `defineTimelineActivityType()` |
Le SDK les détecte via une analyse AST au moment de la compilation, donc l’organisation des fichiers vous appartient — la convention est `src/objects/`, `src/fields/`, `src/indexes/` et `src/timeline-activity-types/`. Des UUID `universalIdentifier` stables relient tout d’un déploiement à l’autre.
## Index (optionnels)
Les applications peuvent fournir des index avec leurs objets pour que les requêtes récurrentes restent rapides. Le cas le plus courant est une colonne de statut ou de clé étrangère que vous lisez fréquemment.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### Index uniques
`defineIndex` accepte `isUnique: true` pour l’unicité sur une ou plusieurs colonnes. C’est la primitive recommandée — `defineField({ isUnique: true })` est obsolète et sera supprimé dans une future version.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Autres contraintes
* Les clauses `WHERE` partielles restent sous le contrôle de l’administrateur — les applications ne peuvent pas les déclarer.
* Chaque objet est limité à 10 index personnalisés (les index propres au framework ne comptent pas).
Ordonnez le tableau `fields` dans l’ordre dans lequel Postgres doit l’utiliser — colonne la plus à gauche en premier, comme dans un annuaire téléphonique. Les index ne sont pas gratuits : chaque écriture dans la table les met à jour. Ajoutez-en un uniquement lorsque vous avez une requête qui en a besoin.
<Note>
Vous cherchez **Application Config** ou **Roles & Permissions** ? Ces éléments décrivent l’application elle-même plutôt que les données qu’elle ajoute — ils se trouvent sous [Config](/l/fr/developers/extend/apps/config/overview). Vous cherchez **Connections** (Linear, GitHub, Slack OAuth) ? Ceux-ci existent pour être appelés *depuis* des fonctions logiques et se trouvent sous [Logic](/l/fr/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Relations
description: Reliez des objets entre eux avec des relations MANY_TO_ONE / ONE_TO_MANY bidirectionnelles.
icon: diagram-project
---
Les relations relient deux objets entre eux. Dans Twenty, les relations sont toujours **bidirectionnelles** — chaque relation a deux côtés, et chaque côté est déclaré comme un champ qui référence l'autre.
| Type de relation | Description | Possède une clé étrangère ? |
| ---------------- | ------------------------------------------------------------------------------------ | --------------------------- |
| `MANY_TO_ONE` | De nombreux enregistrements de cet objet pointent vers un enregistrement de la cible | Oui (`joinColumnName`) |
| `ONE_TO_MANY` | Un enregistrement de cet objet possède de nombreux enregistrements de la cible | Non (le côté inverse) |
## Fonctionnement des relations
Chaque relation nécessite **deux champs** qui se référencent mutuellement :
1. Le côté **MANY_TO_ONE** — réside sur l'objet qui détient la clé étrangère.
2. Le côté **ONE_TO_MANY** — réside sur l'objet qui possède la collection.
Les deux champs utilisent `FieldType.RELATION` et se référencent mutuellement via `relationTargetFieldMetadataUniversalIdentifier`.
## Exemple : une carte postale a de nombreux destinataires
Un `PostCard` peut être envoyé à de nombreux enregistrements `PostCardRecipient`. Chaque destinataire appartient à exactement une carte postale.
**Étape 1: Définir le côté ONE_TO_MANY sur PostCard** (le côté "one") :
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Étape 2: Définir le côté MANY_TO_ONE sur PostCardRecipient** (le côté "many" — détient la clé étrangère) :
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Imports circulaires :** les deux champs de relation se référencent l'un l'autre via leur `universalIdentifier`. Pour éviter les problèmes d'import circulaire, exportez les ID de vos champs en tant que constantes nommées depuis chaque fichier, puis importez-les dans l'autre. Le système de build les résout au moment de la compilation.
</Note>
## Relations de jonction
Un objet de jonction connecte un enregistrement source à un ou plusieurs enregistrements cibles. Par exemple, `PostCardRecipient` peut contenir à la fois une relation `postCard` et une relation `recipient`. Dans le champ `ONE_TO_MANY` de l'objet source, définissez `junctionTargetFieldUniversalIdentifier` sur le champ de relation de l'objet de jonction qui pointe vers la cible :
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` doit identifier une relation `MANY_TO_ONE` ou une relation morph sur l'objet de jonction. Il indique à Twenty quel enregistrement associé est la cible lorsqu'une fonctionnalité telle que la [diffusion des activités de chronologie](/l/fr/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) parcourt la jonction.
## Relation avec des objets standard
Pour créer une relation avec un objet Twenty intégré (Person, Company, etc.), utilisez `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` :
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
## Propriétés des champs de relation
| Propriété | Obligatoire | Description |
| ---------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `type` | Oui | Doit être `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Oui | Le `universalIdentifier` de l'objet cible |
| `relationTargetFieldMetadataUniversalIdentifier` | Oui | Le `universalIdentifier` du champ correspondant sur l'objet cible |
| `universalSettings.relationType` | Oui | `RelationType.MANY_TO_ONE` ou `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE uniquement | Ce qui se passe lorsque l'enregistrement référencé est supprimé : `CASCADE`, `SET_NULL`, `RESTRICT` ou `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE uniquement | Nom de la colonne de base de données pour la clé étrangère (p. ex., `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Jonction `ONE_TO_MANY` uniquement | Champ de relation de l'objet de jonction qui pointe vers la cible de diffusion |
## Champs de relation en ligne
Vous pouvez également déclarer une relation directement à l'intérieur de [`defineObject`](/l/fr/developers/extend/apps/data/objects). Lorsqu'il est en ligne, omettez `objectUniversalIdentifier` — il est hérité de l'objet parent :
```ts
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// … other fields
],
});
```

View File

@@ -0,0 +1,210 @@
---
title: Ciblage des métadonnées système
description: Résolvez les identifiants universels déterministes des métadonnées Twenty provisionnées automatiquement sur chaque objet, afin que votre application puisse s’y référer sans les coder en dur.
icon: gears
---
Chaque objet dans Twenty est fourni avec des **métadonnées système** que vous ne déclarez jamais vous‑même, comme un ensemble de champs et une vue principale en liste avec ses colonnes. Le serveur crée l’ensemble de ces éléments lorsque l’objet est provisionné, et cet ensemble s’agrandit au fur et à mesure que Twenty évolue.
Comme vous ne les déclarez pas, il n’existe pas de constante `universalIdentifier` que vous puissiez importer. À la place, le serveur **dérive** chaque identifiant de manière déterministe, et `twenty-sdk` expose la même dérivation afin que votre manifeste puisse résoudre la valeur exacte utilisée par le serveur.
## Champs système
Les champs scalaires présents sur chaque objet, que vous ne déclarez jamais avec [`defineField()`](/l/fr/developers/extend/apps/data/extending-objects) :
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Alors, comment référencer `createdAt` en tant que colonne dans une [vue](/l/fr/developers/extend/apps/layout/views) ?
### Le problème
Depuis Twenty 2.19, l'identifiant universel d'un champ système est **dérivé de manière déterministe** par le serveur à partir de trois entrées : l'identifiant universel de l'application, l'identifiant universel de l'objet et le nom du champ. Inventer un id et le coder en dur ne fonctionnera pas : il ne correspond à rien sur le serveur, et la synchronisation rejette la référence orpheline :
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### La solution
<Note>
`getFieldUniversalIdentifier` est disponible à partir de `twenty-sdk` 2.21.
</Note>
Utilisez `getFieldUniversalIdentifier` pour résoudre exactement la même valeur que celle utilisée par le serveur. Elle prend les trois entrées et renvoie l'identifiant universel du champ :
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` est l'identifiant de votre application, celui que vous transmettez à [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
* `objectUniversalIdentifier` est l'identifiant de l'objet auquel le champ appartient.
* `name` est le nom du champ système, l'une des valeurs listées ci-dessus.
### Exemple : une colonne createdAt dans une vue
Le cas typique consiste à ajouter une colonne `createdAt` à une vue de l'un de vos objets personnalisés. Résolvez l'id du champ et référencez-le comme n'importe quel autre `fieldMetadataUniversalIdentifier` :
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Le même id résolu fonctionne partout où un `fieldMetadataUniversalIdentifier` est attendu : champs de vue, filtres, tris, regroupements et widgets de mise en page.
<Note>
Résolvez l'id, ne le codez pas en dur. Parce que le serveur dérive la valeur à partir de
l'id de l'application, de l'id de l'objet et du nom du champ, appeler
`getFieldUniversalIdentifier` garde votre référence correcte même si ces
entrées changent, et évite les divergences si la dérivation évolue un jour.
</Note>
### Champs de relation système
<Note>
`getSystemRelationFieldUniversalIdentifier` est disponible dans `twenty-sdk`
à partir de la version 2.23 et nécessite un serveur Twenty en version 2.23 ou ultérieure.
</Note>
Outre les champs système scalaires ci-dessus, le serveur met également à disposition quatre **champs de relation système** sur chaque objet : `timelineActivities`, `attachments`, `noteTargets` et `taskTargets`, chacun pointant vers l’objet de relation standard correspondant.
Ainsi, ces champs ne sont pas résolus avec `getFieldUniversalIdentifier` : leur identifiant est dérivé **indépendamment du nom**, à partir de l’objet qui héberge le champ et de l’objet vers lequel le champ pointe. De cette façon, renommer un objet ne modifie jamais les identifiants de ses champs de relation.
Utilisez `getSystemRelationFieldUniversalIdentifier` pour les résoudre :
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` est l’objet qui **héberge** le champ.
* `relationTargetObjectUniversalIdentifier` est l’objet vers lequel le champ **pointe**.
La direction est encodée par l’ordre des arguments. Pour résoudre le côté inverse (par exemple `attachment.targetRocket`, le champ morph que le serveur crée sur l’objet de relation standard), inversez les deux :
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
Comme pour les champs système scalaires, l’id résolu fonctionne partout où un `fieldMetadataUniversalIdentifier` est attendu.
## Vues système
<Note>
`getSystemViewUniversalIdentifier` et `getSystemViewFieldUniversalIdentifier`
sont disponibles dans `twenty-sdk` à partir de la version 2.26 et nécessitent
un serveur Twenty en version 2.26 ou ultérieure.
</Note>
Le serveur provisionne également une **vue système** sur chaque objet : la vue principale en liste (`All {objectLabelPlural}`, indexée sur `ViewKey.INDEX` (dérivée de `SYSTEM_VIEW_KEYS.INDEX`)), avec une colonne par champ affichable. Comme pour les champs de relation système, leurs identifiants sont dérivés **sans nom**, de sorte que renommer un objet ou un champ ne les modifie jamais.
Utilisez `getSystemViewUniversalIdentifier` pour la résoudre :
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` correspond à l’application propriétaire de l’**objet**, ce qui sert d’espace de noms pour la vue.
* `objectUniversalIdentifier` est l’objet que la vue répertorie.
* `viewKey` est le discriminateur de vue système : `SYSTEM_VIEW_KEYS.INDEX` pour la vue principale en liste, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` pour la vue du widget de champs de la page d’enregistrement. Il sert de clé pour la dérivation ; seul `INDEX` est également stocké sur la ligne de la vue.
L’identifiant résolu fonctionne partout où un `viewUniversalIdentifier` est attendu, par exemple pour une entrée de barre latérale [`NavigationMenuItemType.VIEW`](/l/fr/developers/extend/apps/layout/navigation-menu-items). Pour simplement ouvrir la liste principale d’un objet, privilégiez `NavigationMenuItemType.OBJECT` avec `targetObjectUniversalIdentifier` : cela ne nécessite aucune dérivation.
`getSystemViewFieldUniversalIdentifier` résout une seule **colonne** sur une vue système, à partir de la vue et du champ qu’elle affiche :
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Notez le premier argument : une colonne est placée dans l’espace de noms de l’application propriétaire du **champ qu’elle affiche**, et non de celle propriétaire de la vue. Un champ que votre application ajoute à un objet standard obtient sa colonne dérivée sous votre application, sur une vue appartenant à Twenty.
<Warning>
Les vues système et leurs colonnes sont **la propriété du serveur** : résolvez leurs identifiants pour les référencer, jamais pour les déclarer. `key` sur
[`defineView()`](/l/fr/developers/extend/apps/layout/views) est obsolète et ignoré, de sorte qu’une vue de manifeste ne peut jamais revendiquer la clé `INDEX`, et le serveur provisionne déjà une colonne pour chaque champ que vous ajoutez, de sorte que déclarer votre propre
`defineViewField()` pour ce même champ sur une vue système entre en conflit avec celle‑ci.
</Warning>
## Objets standard de Twenty
Pour un objet Twenty **standard** (Person, Company, Opportunity, …), vous n’avez rien à dériver : les identifiants sont des constantes pré‑calculées que vous pouvez importer directement, à la fois pour les champs et pour les vues.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
Utilisez les fonctions utilitaires ci‑dessus lorsque l’objet est l’un de ceux que **votre application** définit avec [`defineObject()`](/l/fr/developers/extend/apps/data/objects), et pour lesquels il n’existe pas une telle constante.
<Note>
`name` est un champ **par défaut**, pas un champ système. Il conserve son propre identifiant universel codé en dur et n'est pas résolu via
`getFieldUniversalIdentifier`. Sur les objets que vous définissez, référencez le champ
`name` avec l'identifiant que vous lui avez donné dans `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Types d'activités de chronologie
description: Définissez les événements d'audit automatique et les événements explicites de l'application qui s'affichent dans les calendriers d'enregistrement.
icon: clock-rotate-left
---
Un type d'activité de la timeline définit le vocabulaire stable pour un événement affiché dans la timeline d'un enregistrement. Les objets standards et les objets d'application utilisent le même contrat : un type a une étiquette et une icône, peut éventuellement déclarer lorsqu'il est émis, et peut éventuellement être rendu à travers l'un des [composants frontaux] de votre application (/developers/extend/apps/layout/front-components).
<Note>
Les types d'activité de la chronologie sont en bêta et arrivent bientôt dans Vingt 2.34. L'API
peut évoluer pendant que nous apprenons des cas d'utilisation des développeurs d'applications.
</Note>
En créer un avec l'échafaudage :
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
Ou définissez-le directement :
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## Événements automatiques et explicites
Ajoute `emit` quand Vingt devrait créer ce type automatiquement. `emit.on` supporte `created`, `updated`, `deleted`, `restored`, `linked`, et `unlinked`, tandis que `emit.objectUniversalIdentifier` identifie l'objet source. Un seul type efficace peut gérer la même clé d'émission dans un espace de travail : `on`, object et optionnel via une relation.
Sans `emit.through`, l'événement est écrit sur la timeline de l'enregistrement source. Pour l'encourager aux enregistrements liés, définissez `emit.through. elationFieldUniversalIdentifier` à une relation directe de plusieurs à un ou à une relation de jonction [un à plusieurs] (/developers/extend/apps/data/relations#junction-relations) sur l'objet source. Les relations de morphe se répandent vers chaque membre de leur groupe de morphes, donc une seule déclaration peut cibler plusieurs types d'objets.
Pour une relation directe, créer ou restaurer l'enregistrement source produit `linked`, supprimer cela produit `unlinked`, et repointant la relation produit `unlinked` sur la cible précédente plus `linked` sur la nouvelle cible. Les autres mises à jour de sources ne produisent pas d'événements de liens. Il s'agit du contrat utilisé par les pièces jointes, dont la cible est une relation de morphe directe.
Pour une relation de jonction, `universalSettings.junctionTargetFieldUniversalIdentifier` identifie la relation de l'objet de la jonction à la cible. Créer ou supprimer une ligne de jonction produit `linked` ou `unlinked`. Repointer de chaque côté d'une ligne de jonction produit également un événement de lien; les mises à jour des champs de jonction non liés ne le font pas.
Les événements `linked` et `unlinked` nécessitent `emit.through`, car leur déclenchement est un changement de la relation directe ou de la jonction configurée.
Par exemple, il s'agit du même contrat générique utilisé par les notes, les tâches, les messages et les événements du calendrier :
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
Une `mise à jour` à travers un événement est émise pour chaque mise à jour source par défaut. Définissez `emit.through.triggerFieldUniversalIdentifiers` lorsque seuls les changements aux champs source sélectionnés devraient apparaître sur les fils cibles.
Omettre `emit` pour un événement de domaine explicite que votre fonction logique se crée. Cela évite de produire à la fois une ligne d'audit automatique et une ligne explicite pour la même opération.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
Créez des événements explicites avec [`createTimelineActivity()`](/l/fr/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). Le code d'application utilise des identifiants universels stables ; vingt résolvent les identifiants de métadonnées spécifiques à l'installation.
## Rendu personnalisé
Sans composant frontal, Twenty affiche une ligne générique native à partir de l'étiquette de type, de l'icône et des métadonnées de l'objet lié. Cela fonctionne pour les objets standards et personnalisés sans moteur de rendu spécifique à l'objet.
Pour des détails personnalisés, définissez `frontComponentUniversalIdentifier` à un composant frontal appartenant à la même application:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
La ligne native reste la présentation réduite. Vingt montent le composant avant seulement après que l'utilisateur ait agrandi cette rangée, en évitant un bac à sable et un travailleur pour chaque événement visible. À l'intérieur du composant, appelez `useTimelineActivityId()` à partir de `vingt-sdk/front-component` pour lire l'ID de ligne et récupérer toutes les données dont votre présentation a besoin. Il retourne `null` lorsque le composant est rendu en dehors d'une ligne de timeline.
## Remplacer la présentation d'une autre application
Une application possède des contrats automatiques de timeline pour ses propres objets. Pour personnaliser un événement sur un objet appartenant à une autre application, déclarer explicitement le type existant avec `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Une substitution doit inclure `emit`, car elle remplace un emplacement d'émission automatique existant plutôt qu'un type explicite. Le type référencé doit appartenir à l'application de l'objet cible et décrire la même action et la même route. La suppression de l'application écrasante restaure le type de base ; la suppression du type de base désactive la substitution.
## Outrepasser et rendre muet l'espace de travail
Les administrateurs de l'espace de travail peuvent modifier la présentation d'un type ou couper en sourdine ses événements automatiques et explicites sans avoir à creuser l'application. Utilisez la mutation `updateTimelineActivityType` de l'API de métadonnées avec l'ID de type spécifique à l'installation :
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label` et `icon` sont stockés en tant que substituts d'espace de travail, donc les mises à jour ultérieures des applications n'écrasent pas les choix de l'administrateur. Définissez `isActive` à `false` pour arrêter l'émission automatique et rejeter les nouveaux événements explicites de ce type. Les lignes existantes restent visibles.
Restaurer les valeurs par défaut de l'application, y compris l'état actif, avec:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Champs de configuration
| Champ | Obligatoire | Description |
| ------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------ |
| `universalIdentifier` | Oui | UUID stable pour le type d'installations et de mises à jour |
| `name` | Oui | Nom programmatique local à l'application ; doit être unique au sein de l'application |
| `label` | Oui | Texte d'action utilisé par le moteur de rendu natif |
| `icon` | Non | Vingt icônes affichées à côté de l'événement |
| `émettre` | Non | Déclaration d'émission automatique ; omet pour les types explicites |
| `emit.on` | Avec émission | Action d'audit qui permet d'écrire ce type |
| `emit.objectUniversalIdentifier` | Avec émission | Objet dont les enregistrements émettent ce type |
| `emit.through.relationFieldUniversalIdentifier` | Non | Relation directe ou de jonction utilisée pour alimenter les fils connexes |
| `emit.through.triggerFieldUniversalIdentifiers` | Non | Champs source qui peuvent déclencher une `mise à jour` à travers l'événement |
| `frontComponentUniversalIdentifier` | Non | Composant frontal appartenant à une application monté lorsque la ligne est étendue |
| `replacesTimelineActivityTypeUniversalIdentifier` | Non | Type d'émission existant remplacé; nécessite `emit` |
Vingt instantanés instantanés d'identité sémantique et présentation durable se replient quand un événement est créé. Les lignes historiques conservent leur action originale et leur signification d'objet. Pendant que le type est installé, son étiquette traduite en cours, son icône et son composant frontal sont rendus en direct. Après désinstallation, la clé de secours de l'instantané conserve la ligne lisible. La résolution utilise l'identifiant universel, donc la présentation survit également à la réinstallation de l'application.

View File

@@ -0,0 +1,102 @@
---
title: Concepts
description: Fonctionnement des applications Twenty — modèle d’entité, sandboxing et cycle de vie de l’installation.
icon: sitemap
---
Les applications Twenty sont des packages TypeScript qui étendent votre espace de travail avec des objets personnalisés, de la logique, des composants d’interface utilisateur et des capacités d’IA. Elles s’exécutent sur la plateforme Twenty avec une isolation complète en bac à sable et des contrôles d’autorisations.
## Fonctionnement des applications
Une application est un ensemble d’**entités** déclarées à l’aide des fonctions `defineEntity()` du package `twenty-sdk`. Le SDK détecte ces déclarations via une analyse de l’AST au moment de la compilation et produit un **manifeste** — une description complète de ce que votre application ajoute à un espace de travail. Ces fonctions valident votre configuration au moment de la compilation et offrent l’autocomplétion dans l’IDE et une sécurité de typage.
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**L’organisation des fichiers vous revient.** La détection des entités est basée sur l’AST — le SDK trouve les appels à `export default defineEntity(...)` quel que soit l’emplacement du fichier. La structure de dossiers ci-dessus est une convention, pas une obligation.
</Note>
## Types d’entités
| Entité | Objectif | Documentation |
| --------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Application** | Identité de l’application, rôle par défaut, variables | [Configuration de l’application](/l/fr/developers/extend/apps/config/application) |
| **Rôle** | Ensembles d’autorisations pour les objets et les champs | [Rôles et autorisations](/l/fr/developers/extend/apps/config/roles) |
| **Objet** | Types d’enregistrements personnalisés avec des champs | [Objets](/l/fr/developers/extend/apps/data/objects) |
| **Champ** | Ajouter des champs à des objets provenant d’autres applications | [Extension des objets](/l/fr/developers/extend/apps/data/extending-objects) |
| **Relation** | Liens bidirectionnels entre les objets | [Relations](/l/fr/developers/extend/apps/data/relations) |
| **Fonction logique** | TypeScript côté serveur avec des déclencheurs | [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) |
| **Compétence** | Instructions réutilisables pour des agents d’IA | [Compétences et agents](/l/fr/developers/extend/apps/logic/skills-and-agents) |
| **Agent** | Assistants d’IA avec des prompts personnalisés | [Compétences et agents](/l/fr/developers/extend/apps/logic/skills-and-agents) |
| **Fournisseur de connexion** | Identifiants OAuth pour les API tierces | [Connexions](/l/fr/developers/extend/apps/logic/connections) |
| **Vue** | Vues de liste d’enregistrements préconfigurées | [Vues](/l/fr/developers/extend/apps/layout/views) |
| **Élément de menu de navigation** | Entrées personnalisées de la barre latérale | [Éléments du menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items) |
| **Mise en page** | Onglets et widgets sur la page de détails d’un enregistrement | [Mises en page](/l/fr/developers/extend/apps/layout/page-layouts) |
| **Composant frontal** | Interface React isolée en bac à sable dans Twenty | [Composants frontaux](/l/fr/developers/extend/apps/layout/front-components) |
| **Dépendances partagées** | Dépendances partagées par les composants frontaux de l’application, déclarées dans package.json | [Dépendances partagées](/l/fr/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) |
| **Élément du menu de commande** | Actions rapides et entrées Cmd+K | [Éléments du menu de commande](/l/fr/developers/extend/apps/layout/command-menu-items) |
## Exécution en bac à sable
* **Les fonctions logiques** s’exécutent dans des processus Node.js isolés sur le serveur. Ils accèdent uniquement aux données via le client d’API typé, qui agit soit en tant que personne ayant déclenché l’exécution, soit en tant que l’application elle-même. Consultez [Les accès utilisés par un appel](/l/fr/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* **Les composants frontaux** s’exécutent dans des Web Workers en utilisant Remote DOM — isolés en bac à sable de la page principale mais rendant des éléments DOM natifs (pas d’iframes). Ils communiquent avec Twenty via une API hôte de passage de messages.
* **Les autorisations** sont appliquées au niveau de l’API. Deux jetons d’exécution sont injectés, tous deux dérivés du rôle déclaré avec `defineApplicationRole()` ; le jeton par défaut est davantage restreint à ce que la personne ayant déclenché l’exécution peut faire lorsqu’une personne est à l’origine de l’exécution.
## Cycle de vie de l’application
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — surveille vos fichiers source et synchronise en direct les modifications avec un serveur Twenty connecté. Le client d’API typé est régénéré automatiquement lorsque le schéma change.
* **`yarn twenty dev:build`** — compile TypeScript, regroupe les fonctions logiques et les composants frontaux avec esbuild, et produit un manifeste.
* **Hooks pré/post-install et de désinstallation** — fonctions optionnelles qui s’exécutent pendant l’installation ou juste avant la désinstallation. Voir [Install Hooks](/l/fr/developers/extend/apps/config/install-hooks) pour plus de détails.
## Prochaines étapes
<CardGroup cols={2}>
<Card title="Configuration" icon="screwdriver-wrench" href="/l/fr/developers/extend/apps/config/overview">
Identité de l’application, rôle par défaut et hooks d’installation et de désinstallation.
</Card>
<Card title="Données" icon="database" href="/l/fr/developers/extend/apps/data/overview">
Objets, champs et relations bidirectionnelles.
</Card>
<Card title="Logique" icon="bolt" href="/l/fr/developers/extend/apps/logic/overview">
Fonctions logiques, compétences, agents et connexions OAuth.
</Card>
<Card title="Disposition" icon="table-columns" href="/l/fr/developers/extend/apps/layout/overview">
Vues, navigation, mises en page, composants front-end.
</Card>
<Card title="Opérations" icon="rocket" href="/l/fr/developers/extend/apps/operations/overview">
CLI, tests, dépôts distants, CI et publication de votre application.
</Card>
</CardGroup>

View File

@@ -0,0 +1,87 @@
---
title: Serveur local
description: Gérez le serveur Docker local de Twenty — démarrage, arrêt, mise à niveau, instance de test parallèle et configuration manuelle du SDK.
icon: server
---
## Gestion du serveur local
Utilisez `yarn twenty docker:*` pour contrôler le conteneur Twenty local :
| Commande | Ce que cela fait |
| -------------------------------------- | ---------------------------------------------------------- |
| `yarn twenty docker:start` | Démarre le serveur (récupère l’image si nécessaire) |
| `yarn twenty docker:start 2.2.0` | Démarrer une version spécifique du serveur |
| `yarn twenty docker:start --port 3030` | Démarre sur un port personnalisé |
| `yarn twenty docker:stop` | Arrête le serveur (préserve les données) |
| `yarn twenty docker:status` | Affiche l’URL, la version et les identifiants de connexion |
| `yarn twenty docker:logs` | Diffuse les journaux du serveur |
| `yarn twenty docker:reset` | Efface les données et repart de zéro |
| `yarn twenty docker:upgrade` | Récupère la dernière image `twenty-app-dev` |
| `yarn twenty docker:upgrade 2.2.0` | Met à niveau vers une version spécifique |
Les données sont conservées entre les redémarrages dans deux volumes Docker (`twenty-app-dev-data` pour PostgreSQL, `twenty-app-dev-storage` pour les fichiers). Utilisez `reset` pour tout effacer.
## Figer la version du serveur
Lorsqu’aucune version n’est fournie, `docker:start` résout la version à partir de la plage `engines.twenty` de votre `package.json` — la même plage que le serveur utilise pour la validation lors de l’installation de votre application. Il démarre l’image `twenty-app-dev` la plus récente publiée qui satisfait la plage, et revient à `latest` lorsque le champ est absent ou qu’aucune version publiée ne correspond :
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
Passez explicitement une version pour remplacer la plage pour une seule exécution : `yarn twenty docker:start 2.3.0`. Si un conteneur existe déjà avec une version différente, `docker:start` le met à niveau sur place (en recréant le conteneur tout en préservant vos volumes de données).
## Mise à niveau de l’image du serveur
`yarn twenty docker:upgrade` récupère la dernière image, compare les empreintes et ne recrée le conteneur que si quelque chose a réellement changé. Les volumes sont conservés — seul le conteneur est remplacé. Si une nouvelle image a été récupérée et que le conteneur était en cours d’exécution, la mise à niveau démarre automatiquement un nouveau conteneur ; exécutez ensuite `yarn twenty docker:start` pour attendre qu’il devienne opérationnel.
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
Vérifiez la version en cours d’exécution avec `yarn twenty docker:status` (elle affiche la valeur de `APP_VERSION` intégrée au conteneur).
## Exécuter une instance de test parallèle
Passez `--test` à n’importe quelle commande `docker:*` pour gérer une deuxième instance totalement isolée — utile pour les tests d’intégration ou pour expérimenter sans toucher à vos données de développement principales :
| Commande | Ce que cela fait |
| ----------------------------------- | ------------------------------------------------- |
| `yarn twenty docker:start --test` | Démarre l’instance de test (port 2021 par défaut) |
| `yarn twenty docker:stop --test` | L’arrête |
| `yarn twenty docker:status --test` | Affiche son statut |
| `yarn twenty docker:logs --test` | Diffuse ses journaux |
| `yarn twenty docker:reset --test` | Efface ses données |
| `yarn twenty docker:upgrade --test` | Met à niveau son image |
L’instance de test possède son propre conteneur (`twenty-app-dev-test`), ses volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) et sa configuration — elle s’exécute parallèlement à votre instance principale sans conflits. Combinez `--test` avec `--port` pour remplacer le port 2021.
## Configuration manuelle (sans le générateur)
Ignorez le générateur si vous ajoutez le SDK à un projet existant :
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Ajoutez le script à `package.json` :
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
Vous pouvez maintenant exécuter `yarn twenty dev`, `yarn twenty docker:start` et le reste.
<Note>
N’installez pas `twenty-sdk` globalement — figez-le par projet afin que chaque application utilise sa propre version.
</Note>

View File

@@ -0,0 +1,79 @@
---
title: Structure du projet
description: Ce qui se trouve à l’intérieur d’une application Twenty générée par un modèle — fichiers, dossiers et le rôle de chacun.
icon: folder-tree
---
Une nouvelle application générée par `npx create-twenty-app` ressemble à ceci :
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md
```
## Fichiers clés
| Fichier / Dossier | Objectif |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `src/application-config.ts` | **Requis.** Le fichier de configuration principal de votre application. |
| `src/default-role.ts` | Rôle par défaut qui contrôle ce à quoi vos fonctions de logique peuvent accéder. |
| `src/constants/universal-identifiers.ts` | UUID générés automatiquement et métadonnées de l’application (nom d’affichage, description). |
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | Une page d’accueil de démarrage : un composant frontal rendu par une mise en page autonome, accessible depuis la barre latérale. |
| `src/__tests__/` | Un test unitaire plus un test d’intégration (avec sa configuration globale) qui synchronise l’application avec un serveur réel. |
| `public/` | Ressources statiques (images, polices) servies avec votre application. |
| `AGENTS.md` / `CLAUDE.md` | Consignes pour les agents IA de programmation travaillant sur l’application. |
| `CHANGELOG.md` / `SETUP.md` | Journal des modifications notables et instructions de configuration pour le développement local. |
<Note>
**L’organisation des fichiers vous revient.** Les dossiers ci-dessus sont des conventions — le SDK détecte les entités via une analyse AST sur les appels à `export default defineEntity(...)` quel que soit l’emplacement du fichier.
</Note>
## Dépendances
Les deux packages du SDK Twenty doivent être placés dans `devDependencies`, et non dans `dependencies` :
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "2.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
Le générateur de projet fige `twenty-sdk` et `twenty-client-sdk` sur sa propre version — gardez les deux synchronisés lors de la mise à niveau.
* **`twenty-sdk`** fournit le CLI `twenty` ainsi que les outils de build et de scaffolding. Il ne s’exécute qu’au moment du développement et du build et n’est jamais importé par le runtime de l’application que vous publiez.
* **`twenty-client-sdk`** *est* importé par le code de votre application (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), mais Twenty le fournit au moment de l’exécution : les fonctions de logique l’obtiennent à partir d’une couche SDK générée, et les composants front le résolvent à partir de modules servis par le serveur. La copie que vous avez installée est uniquement utilisée pour la vérification de type et le build au moment du déploiement, elle n’a donc jamais besoin d’être incluse dans le bundle déployé.
Conserver l’un ou l’autre package dans `dependencies` l’intègre dans le bundle runtime de l’application installée, où il ne fait que l’alourdir inutilement. `twenty dev:build` émet un avertissement lorsque l’un ou l’autre est encore répertorié dans `dependencies`.
Ajoutez les dépendances runtime propres à votre application (les bibliothèques que vos fonctions logiques importent réellement à l’exécution) dans `dependencies` comme d’habitude. Gardez l’arborescence légère : les `dependencies` sont installées dans une couche d’exécution limitée à 250 Mo décompressés, et une synchronisation ou une installation échoue avec une erreur de taille des dépendances au-delà de cette limite — les bibliothèques d’interface utilisateur et les outils de développement doivent aller dans `devDependencies`.

View File

@@ -0,0 +1,166 @@
---
title: Démarrage rapide
icon: rocket
description: Créez votre première application Twenty en quelques minutes.
---
## Prérequis
* **Node.js 24.5+** — [Télécharger](https://nodejs.org/)
* **Yarn 4** — fourni avec Node.js via Corepack. Activez-le : `corepack enable`
* **Docker** — [Télécharger](https://www.docker.com/products/docker-desktop/). Nécessaire pour exécuter un serveur Twenty local. Ignorez si vous avez déjà Twenty en cours d’exécution ailleurs.
La création d’une application Twenty comporte trois phases. Le générateur les regroupe en une seule commande pour le parcours idéal, mais chaque phase est un concept distinct — en cas d’échec, savoir dans quelle phase vous vous trouvez indique ce qu’il faut corriger.
| Phase | Ce que vous faites | Outil | Résultat |
| -------------------------- | ------------------------------------------------------ | ----------------------------------- | ----------------------------------------------------------- |
| **1. Génération** | Générer le code source de l’application | `npx create-twenty-app` | Un projet TypeScript sur le disque |
| **2. Exécuter un serveur** | Démarrer un serveur Twenty vers lequel se synchroniser | Docker + `yarn twenty docker:start` | Une instance Twenty en cours d’exécution |
| **3. Synchroniser** | Synchroniser en direct votre code avec le serveur | `yarn twenty dev` | Vos modifications apparaissent dans l’interface utilisateur |
---
## Phase 1 — Générez le squelette de votre projet
Créez une nouvelle application à partir du modèle :
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Le générateur est non interactif : le nom du répertoire devient le nom de l’application. Passez `--display-name` et `--description` pour personnaliser les métadonnées générées (vous pouvez également les modifier plus tard dans `src/constants/universal-identifiers.ts`). Cela génère un projet TypeScript dans `my-twenty-app/` avec un fichier de démarrage `application-config.ts`, un rôle par défaut, des workflows CI/CD et un test d’intégration.
**Après cette phase :** vous disposez du code source d’une application sur votre machine. Elle ne s’exécute pas encore — c’est la phase 2.
---
## Phase 2 — Exécuter un serveur Twenty local
Votre application a besoin d’un serveur Twenty vers lequel se synchroniser. Le serveur est une instance Twenty complète — interface utilisateur, API GraphQL, PostgreSQL — exécutée localement dans Docker. Votre code local envoie ses définitions à ce serveur, qui les fait apparaître dans l’interface utilisateur.
Le générateur de projet en crée un pour vous : avec Docker en cours d’exécution, il récupère l’image `twentycrm/twenty-app-dev`, la démarre sur le port `2020`, et authentifie le CLI auprès de l’espace de travail de démonstration prérempli (`tim@apple.dev`) — aucune connexion requise.
Pour vous connecter à un serveur Twenty existant à la place, passez `--url \<your-server-url>`. Les serveurs distants s’authentifient avec OAuth : un navigateur s’ouvre pour que vous puissiez vous connecter et cliquer sur **Authorize**, ce qui donne au CLI l’accès à votre espace de travail. (Vous pouvez aussi choisir OAuth en local avec `--authentication-method oauth` — connectez-vous avec `tim@apple.dev` / `tim@apple.dev`.)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Écran de connexion Twenty" />
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Écran d’autorisation de la CLI Twenty" />
</div>
Votre terminal confirmera que tout est configuré.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Application initialisée avec succès" />
</div>
**Après cette phase :** vous disposez d’un serveur Twenty en cours d’exécution sur [http://localhost:2020](http://localhost:2020) avec votre CLI autorisée à s’y synchroniser.
<Note>
Si Docker n’est pas installé ou en cours d’exécution, le générateur vous indiquera la bonne commande de démarrage pour votre système d’exploitation. Une fois Docker lancé, vous pouvez reprendre avec `yarn twenty docker:start` — pas besoin de régénérer le squelette.
</Note>
---
## Phase 3 — Synchroniser vos modifications
C’est la boucle interne dans laquelle vous passerez la plupart de votre temps.
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Cela surveille `src/`, reconstruit à chaque modification et synchronise le résultat avec le serveur. Modifiez un fichier, enregistrez, et en quelques secondes le serveur reflète la modification. Vous verrez un panneau d’état en direct dans votre terminal.
Pour une sortie plus détaillée (journaux de build, requêtes de synchronisation, traces d’erreurs), ajoutez `--verbose`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Sortie du terminal en mode dev" />
</div>
Ouvrez [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Vous devriez voir votre application sous **Your Apps**.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="La liste Your Apps affichant My twenty app" />
</div>
Cliquez sur **My twenty app** pour voir son **application registration** — un enregistrement au niveau du serveur décrivant votre application (nom, identifiant, informations d’identification OAuth, source). Un même enregistrement peut être installé dans plusieurs espaces de travail sur le même serveur.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Détails de l’enregistrement d’application" />
</div>
Cliquez sur **View installed app** pour voir l’installation dans l’espace de travail. L’onglet **About** affiche la version et les options de gestion.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Application installée" />
</div>
**Après cette phase :** vous disposez d’une boucle de développement en direct. Modifiez n’importe quel fichier dans `src/` et il apparaît dans l’interface utilisateur.
### Synchronisation ponctuelle pour la CI et les scripts
Utilisez `plan` et `apply` pour exécuter une fois le même pipeline, sans surveillance :
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| Commande | Comportement | Quand l'utiliser : |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `yarn twenty dev` | Surveille et resynchronise à chaque modification. Reste en cours d’exécution jusqu’à ce que vous l’arrêtiez. | Développement local interactif. |
| `yarn twenty apply` | Une seule opération de build + synchronisation, se termine avec le code `0` en cas de réussite et `1` en cas d’échec. Demande une confirmation pour les modifications destructrices (passez `--force` pour l’ignorer). | CI, hooks de pré-commit, agents IA et flux de travail scriptés. |
| `yarn twenty plan` | Construit et affiche les modifications de métadonnées **sans les appliquer**. | Inspection de ce qu’une synchronisation changerait avant de s’y engager. |
Tous les modes nécessitent un serveur distant authentifié. Voir [synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) pour plus d’informations sur `plan`.
<Note>
`yarn twenty dev --once` et `yarn twenty dev --once --dry-run` sont des alias obsolètes pour `yarn twenty apply` et `yarn twenty plan`.
</Note>
### Options du mode de développement
| Option | Description |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `--force` | Applique les modifications destructrices (suppressions) sans confirmation. |
| `--debounceMs \<ms>` | Définir le délai de temporisation des modifications de fichiers en millisecondes (valeur par défaut : `1000`). |
| `--verbose` / `--debug` | Afficher des journaux de build détaillés, les requêtes de synchronisation et les traces d’erreur. |
## Ce que vous pouvez créer
Les applications sont composées d’**entités** — chacune définie comme un fichier TypeScript avec un seul `export default` :
| Entité | Ce que cela fait |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Objets et champs** | Modèles de données personnalisés (carte postale, facture, etc.) avec des champs typés |
| **Fonctions logiques** | Fonctions TypeScript côté serveur déclenchées par des routes HTTP, des tâches cron ou des événements de base de données |
| **Composants d’interface utilisateur (UI)** | Composants React qui s’affichent dans l’interface de Twenty (panneau latéral, widgets, menu de commandes) |
| **Compétences et agents** | Fonctionnalités d’IA — instructions réutilisables et assistants autonomes |
| **Vues et navigation** | Vues de liste préconfigurées et éléments de menu de la barre latérale |
| **Mises en page** | Pages de détail d’enregistrement personnalisées avec des onglets et des widgets |
Référence complète : [Concepts](/l/fr/developers/extend/apps/getting-started/concepts).
## Prochaines étapes
<CardGroup cols={2}>
<Card title="Configuration" icon="screwdriver-wrench" href="/l/fr/developers/extend/apps/config/overview">
Identité de l’application, rôle par défaut, hooks d’installation et de désinstallation, ressources publiques.
</Card>
<Card title="Données" icon="database" href="/l/fr/developers/extend/apps/data/overview">
Objets, champs et relations bidirectionnelles.
</Card>
<Card title="Logique" icon="bolt" href="/l/fr/developers/extend/apps/logic/overview">
Fonctions logiques, compétences, agents et connexions OAuth.
</Card>
<Card title="Disposition" icon="table-columns" href="/l/fr/developers/extend/apps/layout/overview">
Vues, navigation, mises en page, composants front-end.
</Card>
<Card title="Opérations" icon="rocket" href="/l/fr/developers/extend/apps/operations/overview">
CLI, tests, dépôts distants, CI et publication de votre application.
</Card>
</CardGroup>

View File

@@ -0,0 +1,63 @@
---
title: Génération
description: Générez des fichiers d'entité de manière interactive avec `yarn twenty dev:add` — objets, champs, vues, fonctions logiques et plus encore.
icon: wand-magic-sparkles
---
Au lieu de créer manuellement les fichiers d'entité, utilisez l'outil de génération interactif :
```bash filename="Terminal"
yarn twenty dev:add
```
Il vous invite à choisir un type d'entité, vous guide à travers les champs requis, puis écrit un fichier prêt à l'emploi avec un `universalIdentifier` stable et l'appel `defineEntity()` correct.
Vous pouvez également passer directement le type d'entité pour ignorer la première invite :
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## Types d’entités disponibles
| Type d’entité | Commande | Fichier généré |
| --------------------------------- | ------------------------------------------ | --------------------------------------------------------- |
| Objet | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| Champ | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| Fonction logique | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| Composant frontal | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| Rôle | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| Compétence | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| Vue | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| Élément de menu de navigation | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Mise en page | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
| Onglet Mise en page | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| Élément du menu de commande | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| Champ de vue | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| Fournisseur de connexion | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| Type d'activité de la chronologie | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<name>.ts` |
## Ce que génère l'outil de génération
Chaque type d'entité a son propre modèle. Par exemple, `yarn twenty dev:add object` demande :
1. **Nom (singulier)** — p. ex., `invoice`
2. **Nom (pluriel)** — p. ex., `invoices`
3. **Libellé (singulier)** — rempli automatiquement depuis le nom (p. ex., `Invoice`)
4. **Libellé (pluriel)** — rempli automatiquement (p. ex., `Invoices`)
5. **Créer une vue et un élément de navigation ?** — si vous répondez oui, l'outil génère également une vue correspondante et un lien de barre latérale pour le nouvel objet.
Les autres types d'entités ont des invites plus simples — la plupart ne demandent qu'un nom.
Le type d'entité `field` est plus détaillé : il demande le nom du champ, son libellé, son type (à partir d'une liste de tous les types de champs disponibles comme `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), et le `universalIdentifier` de l'objet cible.
## Chemin de sortie personnalisé
Utilisez l'option `--path` pour placer le fichier généré à un emplacement personnalisé :
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```

View File

@@ -0,0 +1,14 @@
---
title: Résolution des problèmes
description: Problèmes courants lors de la première exécution — Docker, version de Node, Yarn, dépendances.
icon: wrench
---
* **Erreurs Docker** — Assurez-vous que Docker Desktop (ou le démon) est en cours d’exécution avant `yarn twenty docker:start`. Le message d’erreur indiquera la bonne commande de démarrage pour votre système d’exploitation.
* **Mauvaise version de Node** — Version 24.5+ requise (`engines.node: ^24.5.0`). Vérifiez avec `node -v`.
* **Yarn 4 manquant** — Exécutez `corepack enable`.
* **Dépendances cassées** — `rm -rf node_modules && yarn install`.
* **Erreurs de `twenty-sdk` après la mise à niveau vers la v2.8.0** — il est passé de `dependencies` à `devDependencies` dans la v2.8.0. Voir [Structure du projet → Dépendances](/l/fr/developers/extend/apps/getting-started/project-structure#dependencies).
* **`twenty dev:build` avertit au sujet de `twenty-client-sdk` dans `dependencies`** — il est fourni au moment de l'exécution par Twenty, donc il devrait être déplacé vers `devDependencies` avec `twenty-sdk`. Voir [Structure du projet → Dépendances](/l/fr/developers/extend/apps/getting-started/project-structure#dependencies).
Bloqué ? Demandez de l’aide sur le [Discord de Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).

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.

View File

@@ -0,0 +1,197 @@
---
title: Tâches en arrière-plan
description: Confiez les travaux longs ou soumis à des limitations de débit aux workers de Twenty en mettant en file d’attente une autre exécution de fonction logique au lieu de tout faire en ligne.
icon: layer-group
---
Une exécution de fonction logique est limitée par son `timeoutSeconds` (900 secondes maximum). Tout ce qui ne peut pas se terminer dans cette fenêtre — une resynchronisation complète, une diffusion par enregistrement, une API tierce qui vous applique des limitations de débit — doit être découpé en exécutions plus petites.
`enqueueJobs` fait exactement cela : il demande aux workers de Twenty d’exécuter plus tard l’une des fonctions logiques de votre application, une fois par charge utile, chaque exécution dans son propre processus avec son propre budget de délai d’expiration. La fonction appelante retourne immédiatement.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## Mettre des exécutions en file d’attente
Importez `enqueueJobs` depuis `twenty-sdk/logic-function`, pointez-le vers le `universalIdentifier` de la fonction logique à exécuter et transmettez une charge utile par exécution pour la mise en file d’attente.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
Chaque exécution reçoit sa charge utile comme argument du gestionnaire, exactement comme pour tout autre déclencheur. La cible doit appartenir à la **même application** que l’appelant — la mise en file d’attente de la fonction d’une autre application est rejetée avec `Logic function not found` et rien n’est mis en file d’attente. Un seul appel accepte jusqu’à `200` charges utiles.
<Note>
`enqueueJobs` renvoie dès que les jobs sont acceptés, et non pas lorsqu’ils ont été exécutés. Il ne renvoie pas les résultats des cibles — faites en sorte que chaque cible écrive ce qu’elle produit dans le [key-value store](/l/fr/developers/extend/apps/logic/key-value-store) ou dans un enregistrement d’espace de travail si vous devez les lire à nouveau.
</Note>
<Note>
L’ancien assistant `enqueueJob`, qui met en file d’attente un seul job par appel, est obsolète. Utilisez plutôt `enqueueJobs` avec une liste `payloads` à un élément.
</Note>
## Options du job
Les options s’appliquent à chaque exécution du lot.
| Option | Par défaut | Plage | Ce que cela fait |
| ------------ | ---------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | Nombre total de tentatives supplémentaires dans la file d'attente. Les nouvelles tentatives demandées par l'application sont limitées à `3`. N’augmentez cette valeur que pour les gestionnaires qui peuvent être exécutés deux fois en toute sécurité. |
| `delayMs` | `0` | `0`–`604800000` (7 jours) | Attendez ce délai avant que les exécutions deviennent éligibles. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**La priorité n’est pas encore configurable.** Les jobs mis en file d’attente s’exécutent toujours avec la priorité la plus basse, de sorte que le travail de la plateforme n’est jamais retardé derrière les jobs des applications. Le contrôle de la priorité arrive bientôt.
</Note>
L’exécution mise en file d’attente hérite de l’utilisateur exécutant la fonction qui l’a mise en file d’attente, elle agit donc avec les mêmes autorisations.
## Réessayer après un échec transitoire.
Twenty ne réessaie pas toutes les exceptions du code d'application. Une erreur ordinaire levée est traitée comme un échec permanent. En cas d'échec transitoire, une fonction logique mise en file d'attente peut demander jusqu'à trois nouvelles tentatives en levant `RetryableLogicFunctionError`.
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
Levez directement `RetryableLogicFunctionError` lorsque cela est possible. Si vous l'étendez, ne remplacez pas son `name` : Twenty reconnaît le nom sérialisé `RetryableLogicFunctionError` dans les environnements d'exécution.
`retryCount` est égal à `0` lors de l'exécution initiale et n'augmente que lorsque le code d'application demande une nouvelle tentative. `maxRetries` est au maximum de `3` et peut être inférieur lorsque la tâche mise en file d'attente a une limite globale de nouvelles tentatives plus faible. Les défaillances de la plateforme n'augmentent pas `retryCount`, bien qu'elles consomment toujours le budget de sécurité global de la file d'attente.
La file d'attente retarde les tentatives avec un délai exponentiel et une gigue. Le délai exact n'est intentionnellement pas garanti, le code d'application ne doit donc pas dépendre d'une nouvelle tentative à un moment précis. Une fois `maxRetries` atteint, un autre `RetryableLogicFunctionError` est enregistré comme échec final de l'application sans nouvelle exécution.
<Warning>
Les nouvelles tentatives réexécutent l'intégralité du gestionnaire et peuvent survenir après la réussite de certains effets secondaires. Rendez le gestionnaire idempotent avant de demander des nouvelles tentatives.
</Warning>
## Utilisation : paginer une longue synchronisation
La forme classique est une fonction qui met *elle-même* en file d’attente la prochaine exécution avec le curseur suivant. Chaque exécution traite une page de travail bien à l’intérieur de son propre délai d’expiration, et la chaîne s’arrête lorsqu’il ne reste plus rien.
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## Répartition par enregistrement
Quand le travail est naturellement par élément, mettez en file d’attente un job par élément dans un seul appel et laissez les workers les traiter en parallèle au lieu de boucler en ligne.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## Bonnes pratiques pour les tâches de longue durée
Deux règles couvrent presque toutes les longues tâches : **utilisez la récursion au lieu de boucles** et **traitez un bloc borné par exécution**.
Une exécution qui essaie de tout faire est un mode d’échec — elle atteint le délai d’expiration, et avec une nouvelle tentative elle recommence tout depuis zéro. Au lieu de cela, dimensionnez un bloc de manière à ce qu’il se termine confortablement dans `timeoutSeconds`, conservez votre position et mettez en file d’attente l’exécution suivante.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
Ce qui rend cette approche robuste :
* **Dimensionnez le bloc à partir de l’élément le plus lent, pas de la moyenne.** `CHUNK_SIZE × worst-case item time` doit tenir dans `timeoutSeconds` avec une marge de sécurité, sinon la fin d’un bloc est perdue lorsque l’exécution est interrompue.
* **Rendez la condition de terminaison explicite.** Utilisez la récursion uniquement lorsqu’un bloc complet est revenu. Une chaîne qui s’arrête uniquement sur "no results" continuera indéfiniment si la source renvoie un jour une page courte en cours de route.
* **Conservez la progression avant de mettre en file d’attente l’exécution suivante,** afin qu’un maillon ayant échoué redémarre au dernier bloc terminé plutôt qu’au début.
* **Gardez chaque bloc idempotent.** Le retraitement d’un bloc après une nouvelle tentative ne doit pas provoquer une double écriture — indexez les écritures sur l’enregistrement ou l’identifiant externe que vous traitez.
* **Préférez une chaîne par blocs à un énorme déploiement parallèle** lorsque le travail touche un tiers soumis à des limitations de débit : une chaîne avec `delayMs` se régule elle-même, alors que des milliers de jobs mis en file d’attente d’un coup deviennent tous éligibles immédiatement.

View File

@@ -0,0 +1,269 @@
---
title: Connexions
description: Autorisez votre application à agir au nom d'un utilisateur auprès de services tiers via OAuth.
icon: plug
---
Les connexions sont des identifiants qu'un utilisateur détient pour un service externe (Linear, GitHub, Slack, ...). Votre application déclare **comment** ces identifiants sont obtenus — un **fournisseur de connexion** — et les consomme à l'exécution pour effectuer des appels authentifiés vers l'API tierce.
À ce jour, seul OAuth 2.0 est pris en charge. De futurs types d'identifiants (jetons d'accès personnels, clés d'API, authentification de base) s'intégreront à la même interface — les applications utilisant déjà `defineConnectionProvider({ type: 'oauth', ... `})\` n'auront pas besoin de migrer.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Déclarez comment les connexions de votre application sont obtenues">
Un fournisseur de connexion décrit le processus d'échange OAuth requis par votre application. L'utilisateur clique sur "Ajouter une connexion" dans les paramètres de votre application, complète l'écran de consentement du fournisseur, et une ligne `ConnectedAccount` est créée dans son espace de travail.
Une configuration fonctionnelle nécessite **deux fichiers** — le fournisseur de connexion, et une déclaration `serverVariables` correspondante sur `defineApplication` qui contient les identifiants client OAuth.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Points clés :
* `name` est l'identifiant unique utilisé dans `listConnections({ providerName })` (kebab-case, doit correspondre à `^[a-z][a-z0-9-]*$`).
* `displayName` s'affiche dans l'onglet des paramètres par application et dans la liste des outils d'IA.
* `clientIdVariable` / `clientSecretVariable` sont des **noms**, pas des valeurs — ils doivent correspondre aux clés déclarées dans `defineApplication.serverVariables`. Les `client_id` et `client_secret` réels sont saisis par l'administrateur du serveur via l'interface d'enregistrement de l'application, et ne sont jamais validés dans votre dépôt.
* Utilisez `serverVariables` (pas `applicationVariables`) — les identifiants OAuth sont au niveau du serveur et il n'y a qu'une application OAuth par serveur Twenty.
* Tant que les deux `serverVariables` ne sont pas renseignées, l'onglet des paramètres par application affiche une indication "nécessite un administrateur du serveur" et le bouton "Ajouter une connexion" est désactivé.
* `type: 'oauth'` est la seule valeur prise en charge à ce jour. Le discriminateur est compatible avec les évolutions : des types futurs (`'pat'`, `'api-key'`, ...) ajouteront de nouveaux blocs de sous-configuration à côté de `oauth`.
L'URL de rappel OAuth que votre fournisseur doit autoriser est :
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="Exécuter une fonction logique lors de la connexion" description="Réagir au moment où une connexion est établie">
Certains fournisseurs vous transmettent, au moment de la connexion, des données que vous devez conserver avant que la connexion ne soit exploitable — l’exemple classique est Slack, où la réponse OAuth identifie le `team_id` de l’espace de travail sur lequel les événements entrants seront indexés. Définissez `onConnectLogicFunction` pour référencer une fonction logique dans la même application (par son `universalIdentifier`), et elle s’exécute juste après la création du `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
Le hook s’exécute **de manière asynchrone dans l’espace de travail en cours de connexion** (il est mis en file d’attente, non attendu), de sorte qu’un hook lent ou défaillant ne bloque ni n’interrompt jamais le callback OAuth — rendez-le idempotent et faites en sorte qu’il gère ses propres réessais. Le gestionnaire reçoit :
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
À partir de là, utilisez `getConnection(connectedAccountId)` pour lire le jeton d’accès actualisé et appeler l’API du fournisseur (par exemple Slack `auth.test`) ou persister un mappage dans le [stockage clé-valeur](/l/fr/developers/extend/apps/logic/key-value-store).
</Accordion>
<Accordion title="Exécuter une fonction logique lors de la déconnexion" description="Nettoyer lorsqu’une connexion est supprimée">
Tout ce qu’une application revendique au moment de la connexion doit être libéré lorsque la connexion est supprimée. Une intégration Slack qui revendique un `team_id` lors de la connexion, par exemple, doit libérer cette revendication afin qu’un autre espace de travail puisse connecter la même équipe Slack. Définissez `onDisconnectLogicFunction` pour référencer une fonction logique dans la même application, et elle s’exécute juste après la suppression du `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
Comme le hook on-connect, il s’exécute **de manière asynchrone dans l’espace de travail en cours de déconnexion** et ne bloque jamais la déconnexion. Le gestionnaire reçoit la même structure de payload :
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
Le `ConnectedAccount` n’existe déjà plus au moment où le hook s’exécute, donc `getConnection(connectedAccountId)` ne se résout plus. Tout ce dont le nettoyage a besoin (un `team_id`, un identifiant d’abonnement externe) doit avoir été écrit dans le [key-value store](/l/fr/developers/extend/apps/logic/key-value-store) au moment de la connexion, indexé par `connectedAccountId`.
Le hook est déclenché lorsqu’une connexion est supprimée directement. La désinstallation de l’application supprime ses connexions via une cascade de base de données à la place, donc le hook ne s’exécute pas dans ce cas. Déclarez un `uninstallLogicFunction` sur `defineApplication` pour ce cas : il s’exécute avant que les métadonnées de l’application ne soient supprimées, ce qui lui permet encore d’appeler `listConnections` et de nettoyer tout ce qui reste.
</Accordion>
<Accordion title="listConnections / getConnection" description="Utilisez les connexions depuis une fonction logique">
Dans un gestionnaire de fonction logique, `listConnections({ providerName })` renvoie les lignes `ConnectedAccount` de cette application pour le fournisseur donné, avec des jetons d'accès actualisés.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Chaque connexion comporte :
| Champ | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID de ligne unique ; à passer à `getConnection(id)` pour en récupérer une seule |
| `visibility` | `'user'` (réservée à un membre de l'espace de travail) ou `'workspace'` (partagée avec tous les membres) |
| `scopes` | Autorisations OAuth accordées par le fournisseur en amont (distinctes de `visibility` — sans rapport) |
| `userWorkspaceId` | L'ID userWorkspace du propriétaire — utile pour sélectionner "la connexion de l'utilisateur de la requête" dans les déclencheurs de routes HTTP |
| `accessToken` | Jeton d'accès OAuth à jour (actualisé automatiquement s'il a expiré) |
| `nom` | Le nom d'affichage de la connexion (déduit automatiquement lors du callback OAuth, renommable par l'utilisateur) |
| `handle` | L'e-mail du compte amont connecté, actualisé à chaque reconnexion; résolu à partir du `id_token` OIDC a retourné lors de l'échange de jetons, tombant à l'email de 20 utilisateurs de connexion quand le fournisseur ne retourne pas un |
| `authFailedAt` | Défini lorsque le rafraîchissement le plus récent a échoué ; l'utilisateur doit se reconnecter |
Points clés :
* Passez `{ providerName }` pour filtrer par fournisseur ; omettez-le pour obtenir toutes les connexions que cette application possède sur l'ensemble des fournisseurs.
* Le serveur actualise de manière transparente le jeton d'accès avant de répondre. Votre gestionnaire voit toujours un jeton utilisable (ou `authFailedAt` défini).
* `getConnection(id)` est l'équivalent pour une seule ligne.
</Accordion>
<Accordion title="Visibilité par utilisateur vs partagée à l'échelle de l'espace de travail" description="Comment les utilisateurs choisissent entre des identifiants privés et partagés">
Lorsqu'un utilisateur clique sur "Ajouter une connexion", il lui est demandé de choisir une visibilité :
* **Uniquement pour moi** — les identifiants sont privés pour l'utilisateur qui se connecte. Toute fonction logique appelée en son nom (déclencheur de route HTTP avec `isAuthRequired: true`) y a accès ; les déclencheurs cron et les événements de base de données n'y ont pas accès.
* **Partagé au niveau de l'espace de travail** — tout membre de l'espace de travail peut utiliser les identifiants. Les déclencheurs cron / base de données y ont également accès, puisqu'ils n'ont pas d'utilisateur de requête.
Utilisez le bon pour chaque gestionnaire :
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Plusieurs connexions par (utilisateur, fournisseur) sont autorisées, ainsi le même utilisateur peut avoir "Linear personnel" et "Linear professionnel" côte à côte.
</Accordion>
<Accordion title="Configuration unique du fournisseur" description="Enregistrez votre application OAuth auprès du service tiers">
Pour chaque fournisseur de connexion, l'administrateur du serveur doit d'abord enregistrer une application OAuth auprès du service tiers.
1. Accédez aux paramètres développeur du fournisseur (par ex. https://linear.app/settings/api/applications/new).
2. Définissez l'**URI de redirection** sur `\<SERVER_URL>/auth/apps/callback`.
3. Copiez l'**ID client** et le **Secret client** générés.
4. Ouvrez l'application installée dans Twenty en tant qu'administrateur du serveur → définissez les valeurs sur les `serverVariables` correspondantes.
5. Les membres de l'espace de travail peuvent ensuite ajouter des connexions depuis la section **Connexions** par application.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,110 @@
---
title: Stockage clé-valeur
description: Conservez des résultats intermédiaires, mettez les données en cache et partagez l’état entre les exécutions de fonctions logiques avec le magasin clé-valeur intégré à l’application.
icon: database
---
Les fonctions logiques s'exécutent dans des processus Node.js isolés et de courte durée — une fois une exécution terminée, rien de ce qui était conservé en mémoire ne survit. Lorsque vous avez besoin de **vous souvenir de quelque chose entre les exécutions** (mettre en cache une réponse d’API coûteuse, stocker un curseur pour des synchronisations incrémentales, appliquer un anti-rebond, ou transférer l’état d’une fonction à une autre), conservez-le dans le magasin clé-valeur intégré.
Chaque application dispose de son propre espace de noms isolé : les entrées sont associées à l’application authentifiée, de sorte que vos clés ne peuvent jamais entrer en collision avec celles d’une autre application, ni être lues par celle-ci.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## Obtenir, définir, supprimer
Importez `kv` depuis `twenty-sdk/logic-function`. Les valeurs peuvent être n’importe quelle charge utile sérialisable en JSON.
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## Périmètres
Chaque entrée possède une portée, passée comme option à chaque appel. La valeur par défaut est `WORKSPACE`.
* **`WORKSPACE`** (par défaut) — l’entrée est réservée à l’installation actuelle de votre application dans l’espace de travail. Chaque espace de travail qui installe l’application obtient son propre ensemble indépendant de clés. C’est ce qu’il vous faut pour les caches, les curseurs et l’état par espace de travail.
* **`SERVER`** — l’entrée est partagée entre **toutes les installations** de votre application sur le serveur. Les entrées serveur se comportent comme des **revendications** : la valeur stockée est toujours le workspaceId qui a revendiqué la clé (omettre `value` lors de `set` pour revendiquer la clé pour l’espace de travail actuel), et seul cet espace de travail peut la remplacer ou la supprimer. N’importe quelle installation peut lire l’entrée.
Les revendications serveur existent pour le routage inter-espaces de travail. Un [résolveur de route serveur](/l/fr/developers/extend/apps/logic/logic-functions#server-route-trigger) s’exécute dans l’espace de travail propriétaire de l’enregistrement de l’application, mais un webhook entrant ne transporte généralement qu’un identifiant de compte externe — pas un workspaceId Twenty. Faites en sorte que chaque espace de travail revendique son identifiant externe au moment de la connexion, puis résolvez-le dans la route :
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
Comme une clé serveur ne peut être revendiquée que pour l’espace de travail de l’appelant et jamais écrasée par un autre, un espace de travail ne peut pas détourner un mappage appartenant à quelqu’un d’autre. `kv.set` lève une exception lorsque la clé est déjà revendiquée par un autre espace de travail.
## Utilisez-le : mettez en cache un appel coûteux
Un cas d'utilisation typique consiste à mettre en cache une réponse tierce lente ou soumise à des limites de débit afin que les exécutions répétées la réutilisent au lieu d'en payer le coût à chaque fois.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## Modèles et conseils
* **Espaces de noms.** Préfixez les clés pour séparer les différentes préoccupations — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Expiration (TTL).** Le store n'a pas d'expiration intégrée. Stockez un horodatage dans la valeur (comme dans l’exemple de cache) et vérifiez-le à la lecture, ou effacez les clés obsolètes à partir d’une [fonction déclenchée par cron](/l/fr/developers/extend/apps/logic/logic-functions).
* **Que stocker.** Toute valeur sérialisable en JSON — nombres, chaînes de caractères, tableaux, objets. Gardez les entrées petites ; ceci sert à la coordination et à la mise en cache, pas aux blobs volumineux ni aux fichiers. Pour les fichiers, utilisez un champ `FILES` et [`uploadFile`](/l/fr/developers/extend/apps/logic/logic-functions#uploading-files).
* **Visibilité.** Les entrées résident dans la base de données de l’instance, et non comme enregistrements d’espace de travail — elles n’apparaissent jamais dans l’interface utilisateur de l’espace de travail, ne font pas partie du modèle de données de votre application et ne nécessitent aucun droit ni aucune permission d’objet.
## Alternative : un objet de stockage interrogeable
Le magasin intégré est délibérément opaque : les entrées ne sont pas des enregistrements, vous ne pouvez donc pas les parcourir dans l’interface utilisateur, les relier à d’autres objets, ni les filtrer avec des requêtes sur les enregistrements. Lorsque vous avez besoin de tout cela — par exemple un journal de synchronisation visible, ou un état par enregistrement — définissez plutôt un petit **objet technique** avec un champ `key` unique et un champ `RAW_JSON` `value`, puis interrogez-le via le [client d’API typé](/l/fr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Consultez [Objets](/l/fr/developers/extend/apps/data/objects) pour la référence `defineObject` et [Données → Index uniques](/l/fr/developers/extend/apps/data/overview#unique-indexes) pour faire appliquer l’unicité des clés.
* **Délimiter la portée à un enregistrement.** Ajoutez une [relation](/l/fr/developers/extend/apps/data/relations) de l’objet de stockage vers l’objet cible plutôt que de coder l’identifiant dans la clé.
* **Visibilité et autorisations.** Les lignes résident dans la base de données de l'espace de travail comme n'importe quel autre enregistrement, elles sont donc interrogeables via l'API et respectent le [rôle](/l/fr/developers/extend/apps/config/roles) de votre application. Pour garder le store hors de l'interface principale, ne l'ajoutez pas à votre [menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items).
<Note>
Contrairement au magasin intégré, un objet personnalisé est toujours limité à un seul espace de travail — il ne peut pas partager des entrées entre installations comme le font les clés `SERVER`.
</Note>

View File

@@ -0,0 +1,909 @@
---
title: Fonctions logiques
description: Définissez des fonctions TypeScript côté serveur avec des déclencheurs HTTP, cron et d’événements de base de données.
icon: bolt
---
Les fonctions logiques sont des fonctions TypeScript côté serveur qui s'exécutent sur la plateforme Twenty. Elles peuvent être déclenchées par des requêtes HTTP, des programmations cron ou des événements de base de données — et peuvent également être exposées comme des outils pour des agents d'IA.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="Définir des fonctions logiques et leurs déclencheurs">
Chaque fichier de fonction utilise `defineLogicFunction()` pour exporter une configuration avec un gestionnaire et des déclencheurs facultatifs.
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'POST',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
Types de déclencheurs disponibles :
* **httpRoute** : Expose votre fonction sur un chemin et une méthode HTTP. Dans le code de l'application, préfixez le chemin de la route avec `/s/` lorsque vous utilisez `RestApiClient` ; l'URL déployée utilise la base injectée `TWENTY_FUNCTIONS_URL` (ou `\<server-url>/s` lorsqu'elle n'est pas définie).
<Note>
Pour appeler une fonction logique déclenchée par une route depuis un composant frontal (sans interface), consultez [Appeler une fonction logique](/l/fr/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **cron** : Exécute votre fonction selon une planification à l’aide d’une expression CRON.
* **databaseEvent**: S'exécute lors des événements du cycle de vie des objets de l'espace de travail. Lorsque l'opération de l'événement est `updated`, des champs spécifiques à surveiller peuvent être spécifiés dans le tableau `updatedFields`. S'il est laissé indéfini ou vide, toute mise à jour déclenchera la fonction.
> p. ex. `person.updated`, `*.created`, `company.*`
* **serverRoute** : expose une seule route HTTP à portée d’enregistrement. Une fonction de **résolution** (déclarée avec `serverRouteTriggerSettings`) s’exécute dans l’espace de travail propriétaire et renvoie soit une `Response` synchrone, soit l’espace de travail cible ET la fonction logique cible à mettre en file d’attente ; dans le cas de la mise en file d’attente, la plateforme accuse réception avec `202` et exécute cette **cible** dans la file d’attente du worker. Voir [déclencheur de route serveur](#server-route-trigger).
<Note>
Vous pouvez également exécuter manuellement une fonction à l'aide de la CLI :
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Vous pouvez consulter les journaux avec :
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### Charge utile du déclencheur de route
Lorsqu'un déclencheur de route invoque votre fonction logique, elle reçoit un objet `RoutePayload` qui suit le
[format AWS HTTP API v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
Importez le type `RoutePayload` depuis `twenty-sdk/logic-function` :
```ts
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
Le type `RoutePayload` a la structure suivante :
| Nom de la propriété | Type | Description | Exemple |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | En-têtes HTTP (uniquement ceux répertoriés dans `forwardedRequestHeaders`) | voir la section ci-dessous |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Paramètres de la chaîne de requête (plusieurs valeurs séparées par des virgules) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Paramètres de chemin extraits du modèle de route | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corps de la requête analysé (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Corps de la requête UTF-8 d'origine, avant l'analyse JSON. Utile pour vérifier les signatures de webhook de type HMAC (par exemple `X-Hub-Signature-256` de GitHub, Stripe). `undefined` lorsque l'environnement d'exécution ne l'a pas conservé. | |
| `isBase64Encoded` | `boolean` | Indique si le corps est encodé en base64 | |
| `requestContext.http.method` | `string` | Méthode HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Chemin de la requête brut | |
#### forwardedRequestHeaders
Par défaut, les en-têtes HTTP des requêtes entrantes ne sont pas transmis à votre fonction logique pour des raisons de sécurité.
Pour accéder à des en-têtes spécifiques, listez-les dans le tableau `forwardedRequestHeaders` :
```ts
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
```
Dans votre gestionnaire, accédez aux en-têtes transférés comme ceci :
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
Les noms d'en-têtes sont normalisés en minuscules. Accédez-y en utilisant des clés en minuscules (p. ex., `event.headers['content-type']`).
</Note>
#### Réponse HTTP personnalisée
Par défaut, le retour d’une valeur simple depuis votre gestionnaire l’envoie en réponse `200` (JSON pour les objets, `text/plain` pour les chaînes). Pour contrôler le code d’état et les en-têtes de la réponse, retournez un objet `Response` depuis `twenty-sdk/logic-function` :
```ts
import { Response } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
return new Response('<h1>Hello</h1>', {
status: 201,
headers: { 'content-type': 'text/html' },
});
};
```
Pour des raisons de sécurité, les en-têtes de réponse sont restreints à une liste d’autorisation. Tout en-tête qui ne figure pas dans la liste (par exemple `Set-Cookie`, les en-têtes CORS tels que `Access-Control-Allow-Origin`, ou les en-têtes personnalisés `X-*`) est silencieusement supprimé avant l’envoi de la réponse. Les en-têtes de réponse autorisés sont :
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
Le code d’état doit être un code d’état HTTP valide (compris entre 100 et 599). Les noms des en-têtes de réponse sont comparés sans tenir compte de la casse.
</Note>
#### Réponses d’erreur de la plateforme
Au-delà des réponses propres à votre gestionnaire, la plateforme répond directement aux appels de route dans certaines situations : `404` lorsque la route ou la fonction n’existe pas, `403` lorsque l’application est arrêtée, `429` lorsque la limite de débit d’exécution est atteinte, et `422` lorsque les `dependencies` de production de l’application sont trop volumineuses pour être installées — voir [limites de taille des dépendances](/l/fr/developers/extend/apps/getting-started/project-structure#dependencies).
#### Déclencheur de route serveur
`httpRouteTriggerSettings` expose une fonction sous `/s/` et résout l’espace de travail à partir de l’hôte de la requête — ce qui fonctionne lorsque chaque espace de travail a son propre domaine. Les fournisseurs tiers, en revanche, envoient les événements de chaque locataire vers **une** URL. Dans ce cas, utilisez `serverRouteTriggerSettings`.
Le déclencheur comporte deux parties :
1. Une fonction de logique de **résolution** — déclarée avec `serverRouteTriggerSettings` — s’exécute dans votre **espace de travail propriétaire** (l’espace de travail qui possède l’enregistrement de l’application). Elle inspecte la requête entrante et renvoie soit :
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — la plateforme met cette cible en file d’attente dans l’espace de travail résolu et accuse réception avec `202 { queued: true }`, ou
* une `Response` de `twenty-sdk/logic-function` — la plateforme renvoie cette réponse HTTP **de manière synchrone** et ne met **pas** de cible en file d’attente (utilisez ceci pour les échanges de vérification, comme la `url_verification` de Slack).
Le résolveur est le point d’autorisation unique — l’URL transporte uniquement l’identifiant du résolveur. **C’est l’endroit privilégié pour vérifier les signatures des requêtes** : le résolveur s’exécute avant tout effet de bord, a accès au `rawBody` original et aux en-têtes transmis, et peut rejeter la requête sans jamais toucher la cible.
2. Une fonction de logique **cible** — une fonction de logique classique par espace de travail — s’exécute ensuite dans l’espace de travail résolu avec la charge utile renvoyée par le résolveur (ou la charge utile originale de la requête si le résolveur ne l’a pas transformée). Sa valeur de retour **n’est pas** observée par l’appelant HTTP lorsque le résolveur a choisi le chemin de mise en file d’attente.
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
Le point de terminaison est accessible à l’adresse :
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
L’identifiant est le `universalIdentifier` du résolveur issu de votre manifeste. Enregistrez cette URL auprès du fournisseur.
**Répondre à une vérification GET.** Certains fournisseurs vérifient un point de terminaison avant d'y effectuer une livraison, en envoyant une requête `GET` contenant une vérification à la même URL vers laquelle ils effectueront ensuite des événements `POST` — l'API WhatsApp Cloud de Meta en est un exemple. Une route de serveur répond uniquement à `POST`, sauf indication contraire. Déclarez donc les deux méthodes :
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
La vérification arrive dans `event.queryStringParameters`, et le renvoi d'une `Response` la renvoie au fournisseur dans la même requête. Un corps de chaîne est envoyé en tant que `text/plain`, ce que ces fournisseurs attendent :
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` remplace la valeur par défaut au lieu de s'y ajouter, donc `['GET']` seul fait que la route rejette `POST`. Seuls `GET` et `POST` sont pris en charge. Ne le définissez pas, sauf si le fournisseur a besoin du deuxième verbe : une route qui déclare `GET` verra son résolveur exécuté par tout appelant non authentifié, y compris les robots d'exploration et les outils de déploiement des aperçus de liens qui envoient des requêtes `GET` sans y être invités. Toute requête pour laquelle la plateforme ne dispose pas de méthode reçoit une réponse `405` sans que le résolveur ne soit jamais exécuté.
<Note>
**L’application doit être revendiquée et installée sur son espace de travail propriétaire.** Comme le résolveur s’exécute dans l’**espace de travail propriétaire** (l’espace de travail qui détient l’enregistrement de l’application), un déclencheur de route serveur ne fonctionne que lorsque l’application a été *revendiquée* — c’est‑à‑dire qu’elle possède un espace de travail propriétaire — **et** que cette application est **installée sur l’espace de travail propriétaire**. Tant que ces deux conditions ne sont pas remplies, le résolveur n’a nulle part où s’exécuter, donc la route ne peut pas être envoyée. Une application qui expose une fonction logique `serverRouteTriggerSettings` ne peut donc pas être répertoriée sur la place de marché tant qu’elle n’a pas été revendiquée et installée sur son espace de travail propriétaire.
</Note>
**Contrat du résolveur.** Le type `LogicFunctionConfig` du SDK impose cela à la compilation : dès que vous définissez `serverRouteTriggerSettings`, votre gestionnaire est contraint de retourner soit un `Response`, soit `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (ou une `Promise` de l’un ou l’autre). Sur le chemin d’acheminement, le `workspaceId` doit être celui d’un espace de travail où la fonction cible est installée, sinon la requête est rejetée avec un `404`. Un résultat qui ne correspond à aucune de ces formes — y compris un résultat dont les identifiants ne sont pas des UUID — est rejeté avec un `502`.
| Champ | Type | Notes |
| ---------------------------------------- | --------------------- | -------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | UUID de l’espace de travail dans lequel la cible sera exécutée. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` de la fonction de logique à invoquer dans cet espace de travail. |
| `payload` | `object` (facultatif) | S’il est défini, il remplace le corps de la requête envoyé à la cible. |
<Warning>
**La vérification de la signature est de votre responsabilité — effectuez-la dans le résolveur.** La plateforme ne vérifie pas les signatures des requêtes. Le résolveur est l’endroit recommandé pour le faire : il s’exécute en premier, avec accès à `event.rawBody` et aux en-têtes que vous avez listés dans `forwardedRequestHeaders`, et une erreur levée (ou tout `workspaceId` ne correspondant pas) interrompt la distribution avant que la cible ne soit invoquée. Si, à la place, vous repoussez la vérification vers la cible, celle-ci doit faire attention à ne pas perdre `rawBody` et les en-têtes — c’est-à-dire que le résolveur ne doit pas retourner de `payload`. Vérifiez toujours **avant** tout effet de bord et utilisez une comparaison en temps constant.
</Warning>
Pour les signatures de requêtes, la plupart des fournisseurs signent avec HMAC-SHA256 ; les éléments qui diffèrent sont le nom de l’en-tête, l’encodage de l’empreinte et la chaîne de la charge utile signée. Quelques exemples :
| Fournisseur | En-têtes à transférer | Chaîne signée | Empreinte |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (le secret est en base64 après suppression de `whsec_`) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hexadécimal |
| GitHub | `x-hub-signature-256` | `{rawBody}` | hexadécimal (préfixé par `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hexadécimal (préfixé par `v0=`) |
L’exemple de résolveur ci-dessus montre déjà le flux GitHub HMAC-SHA256 — adaptez le nom de l’en-tête, l’encodage de l’empreinte et la chaîne de la charge utile signée en fonction du fournisseur avec lequel vous vous intégrez.
<Note>
Lorsque le résolveur retourne un objet d’acheminement, la route répond `202 { queued: true }` et la cible s’exécute dans la file d’attente du worker — l’appelant n’observe jamais la latence, le résultat ou les échecs de la cible (ceux-ci sont enregistrés dans les journaux d’exécution). Cela évite que les nouvelles tentatives d’envoi de l’émetteur n’amplifient les ralentissements de traitement, ce qui est souhaitable pour l’ingestion de webhook.
Lorsque l’appelant doit lire le corps de la réponse sur la même requête (handshakes de challenge, accusés de réception interactifs), retournez plutôt un `Response` depuis le **résolveur**. La plateforme le renvoie de manière synchrone et ignore la file d’attente ; ses en-têtes passent par la même liste d’autorisation que les réponses des routes HTTP. Gardez le résolveur rapide — certains fournisseurs (par ex. Slack) ont un délai d’attente de seulement quelques secondes. Comme le résolveur est accessible en tant que point de terminaison public, protégez-le avec une limitation de débit à votre périphérie.
</Note>
#### Charge utile du déclencheur d'événement de base de données
Lorsqu’un déclencheur d’événement de base de données appelle votre fonction logique, celle-ci reçoit un `DatabaseEventPayload` par enregistrement modifié. La charge utile combine les métadonnées concernant l'espace de travail et l'objet source avec l'événement au niveau de l'enregistrement.
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
La charge utile inclut :
| Propriété | Description |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `name` | Nom de l’événement, comme `person.updated`. |
| `workspaceId` | Espace de travail où l’événement s’est produit. |
| `objectMetadata` | Métadonnées pour l’objet qui a été modifié. |
| `recordId` | ID de l’enregistrement modifié. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Champs de l’acteur lorsque l’événement a été provoqué par un utilisateur de l’espace de travail. |
| `properties` | Données d’enregistrement pour l’événement, avec `before`, `after`, `diff` et `updatedFields` selon l’opération. |
| Événement | Données d’enregistrement |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
Pour les suppressions logiques (soft deletes), `.deleted` suit la structure de type mise à jour, car le champ `deletedAt` de l’enregistrement change.
Pour les suppressions permanentes, utilisez `.destroyed`.
<Note>
`databaseEventTriggerSettings.updatedFields` filtre les événements de mise à jour qui déclenchent la fonction.
`event.properties.updatedFields` indique quels champs ont réellement changé pour l’événement actuel.
</Note>
Exemple d’événement de création :
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
Exemple d’événement de mise à jour :
```ts
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
return {
personId: event.recordId,
updatedFields,
previousEmail: before.emails?.primaryEmail,
currentEmail: after.emails?.primaryEmail,
emailDiff: diff.emails,
};
};
```
Déclencher uniquement lors des mises à jour de l’adresse e-mail :
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
Exemple d’événement de destruction :
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### Contexte d'exécution
Chaque gestionnaire reçoit un deuxième argument décrivant l'exécution elle-même, quel que soit ce qui l'a déclenchée. Alors que la forme du premier argument varie selon le déclencheur, celle-ci ne varie pas :
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| Champ | Type | Description |
| ------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | L'espace de travail auquel appartient l'exécution. |
| `userWorkspaceId` | `string \| null` | La personne qui a déclenché l'exécution, en tant que membre de cet espace de travail. |
| `workspaceMemberId` | `string \| null` | La même personne sous la forme d'un enregistrement de membre de l'espace de travail, auquel vous liez les enregistrements. |
| `retryCount` | `number` | `0` lors de la première tentative. Voir [Tâches en arrière-plan](/l/fr/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | Limite supérieure des nouvelles tentatives demandées par l'application. |
`userWorkspaceId` et `workspaceMemberId` sont `null` lorsque personne n'a déclenché l'exécution : les planifications cron, les hooks d'installation et les webhooks non authentifiés n'ont aucune personne derrière eux. Ils sont également `null` lorsque la personne ne possède aucun enregistrement de membre de l'espace de travail, ou que le sien a été supprimé.
<Note>
**Le contexte vous indique qui a déclenché l'exécution.** Ce que l'exécution peut *faire* est une déclaration distincte, ci-dessous.
</Note>
#### Quels accès un appel utilise
Chaque client — `CoreApiClient`, `MetadataApiClient` et `RestApiClient` — agit en tant que personne ayant déclenché l'exécution : son rôle est combiné à celui de votre application, de sorte que l'appel ne peut jamais faire plus que l'un ou l'autre. C'est le comportement par défaut, et cela signifie qu'une personne ne peut jamais utiliser votre application pour dépasser ses propres autorisations.
Lorsque personne n'a déclenché l'exécution, il n'y a aucune personne dont il faut adopter l'identité, le même client utilise donc les propres accès de votre application : les planifications cron, les hooks d'installation et les webhooks non authentifiés ne nécessitent aucun traitement particulier.
Certains appels nécessitent légitimement les propres accès de l'application même lorsqu'une personne *est* à l'origine de l'exécution — pour lire les enregistrements de configuration de votre application, ou effectuer un travail que cette personne ne pourrait pas faire elle-même. Créez un deuxième client pour ceux-ci :
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
Créez les deux une seule fois, à la portée du module, et chaque site d'appel indique alors les accès qu'il utilise par le client qu'il appelle. `RestApiClient` accepte la même option :
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**Une exécution que personne n'a déclenchée agit en tant que votre application.** Les planifications cron, les hooks d'installation et les webhooks non authentifiés n'ont aucune personne derrière eux, le client par défaut utilise donc les propres accès de votre application et continue de fonctionner. `runAs: 'application'` n'est nécessaire que lorsque vous souhaitez utiliser ces accès dans une exécution qu'une personne *a* déclenchée.
Vérifiez `context.workspaceMemberId` lorsqu'une fonction se comporte différemment selon qu'une personne est derrière elle, par exemple pour attribuer un enregistrement.
</Note>
Les assistants du SDK qui accèdent aux propres ressources de votre application utilisent toujours ses accès et ignorent `runAs` : le [magasin clé-valeur](/l/fr/developers/extend/apps/logic/key-value-store), les [connexions](/l/fr/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` et la facturation de crédits.
#### Exposer une fonction en tant qu'outil d'IA ou en tant qu'action de workflow
Les fonctions logiques peuvent être exposées sur deux surfaces, chacune avec son propre déclencheur :
* **`toolTriggerSettings`** — rend la fonction découvrable par les fonctionnalités d'IA de Twenty (chat, MCP, appel de fonctions). Utilise le schéma JSON standard, le format que les LLM comprennent nativement.
* **`workflowActionTriggerSettings`** — fait apparaître la fonction comme une étape dans le concepteur visuel de workflows. Utilise le `InputSchema` riche de Twenty afin que le concepteur puisse afficher des éditeurs de champs appropriés, des sélecteurs de variables et des libellés.
Une fonction peut opter pour l'un, l'autre ou les deux. Elles côtoient `cronTriggerSettings`, `databaseEventTriggerSettings` et `httpRouteTriggerSettings` — même modèle, même structure.
<Note>
**Lien avec l’action Code du workflow.** L’action **Code** intégrée dans le générateur de workflows est elle-même une fonction logique — Twenty en crée une pour chaque étape Code et affiche son éditeur en ligne. `workflowActionTriggerSettings` est la manière de transformer ce code ponctuel en ligne en une action **réutilisable** : définissez la fonction une fois dans votre application et elle devient sélectionnable dans n’importe quel workflow, au lieu d’être copiée-collée dans chaque étape Code. Voir l’[action Code](/l/fr/user-guide/workflows/capabilities/workflow-actions#code) dans le guide utilisateur pour la vue côté utilisateur final.
</Note>
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
Points clés :
* Une fonction peut mélanger les surfaces — déclarez à la fois `toolTriggerSettings` et `workflowActionTriggerSettings` pour l'exposer à la fois dans le chat ET dans le concepteur de workflows.
* `toolTriggerSettings.inputSchema` et `workflowActionTriggerSettings.inputSchema` sont tous deux facultatifs. Lorsqu'ils sont omis, le générateur de manifeste les déduit à partir du code source du gestionnaire (schéma JSON pour l'outil d'IA, `InputSchema` de Twenty pour l'action de workflow). Fournissez-en un explicitement lorsque vous souhaitez un typage plus riche — par exemple, avec des champs compatibles avec `FieldMetadataType` comme `CURRENCY` ou `RELATION` pour le concepteur de workflows, ou avec des champs `description` que l'agent d'IA peut lire :
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
```
Pour déclarer vos paramètres **une seule fois** et les utiliser sur les deux surfaces, définissez un seul schéma JSON (`InputJsonSchema`) et convertissez-le pour l’action de flux de travail avec `jsonSchemaToInputSchema` depuis `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` prend directement le schéma JSON, tandis que `workflowActionTriggerSettings.inputSchema` attend le `InputSchema` de Twenty :
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### Un exemple complet d’action de workflow
`workflowActionTriggerSettings` accepte quatre champs :
| Champ | Objectif |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Nom affiché pour l’action dans le sélecteur d’étapes du générateur de workflows. Utilise par défaut le `name` de la fonction. |
| `icon` | Icône affichée à côté de l’action (un nom `tabler-icons`, par exemple `IconBuilding`). |
| `inputSchema` | Le riche `InputSchema` de Twenty — ce que le générateur affiche sous forme de champs configurables (avec des sélecteurs de variables). Optionnel ; déduit du gestionnaire lorsqu’il est omis. |
| `outputSchema` | Déclare la structure renvoyée par le gestionnaire, afin que **les étapes suivantes puissent établir une correspondance avec ses champs de sortie**. Optionnel ; sans cela, la sortie est exposée comme une valeur opaque unique. |
Assembler le tout — une fonction exposée comme action de workflow, avec une sortie déclarée pour que les étapes ultérieures puissent référencer `taskId` :
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
Une fois l’application installée, **Enrich Company** apparaît dans le sélecteur d’actions du générateur de workflows. Le générateur affiche `companyName` et `domain` sous forme de champs de saisie (chacun pouvant récupérer des valeurs à partir des étapes précédentes), et les étapes en aval peuvent référencer les sorties `taskId` et `enriched` de l’étape.
<Note>
**Rédigez une bonne `description`.** Les agents IA s'appuient sur le champ `description` de la fonction pour décider quand utiliser l'outil. Soyez précis sur ce que fait l'outil et quand il doit être appelé.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**Aides à l’exécution.** `twenty-sdk/utils` réexporte de petites aides à l’exécution afin que les gestionnaires n’importent jamais directement depuis `twenty-shared`. Par exemple, `isDefined(value)` renvoie `false` à la fois pour `null` et `undefined` — utilisez-le pour restreindre en toute sécurité les entrées de gestionnaire optionnelles, qui peuvent arriver sous forme de `null` à l’exécution même lorsqu’elles sont typées `T | undefined` :
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**Hooks d'installation** — les gestionnaires de pré-installation, de post-installation et de désinstallation — partagent ce runtime mais sont déclarés avec leurs propres fonctions define et ne prennent pas de paramètres de déclenchement. Voir [hooks d'installation](/l/fr/developers/extend/apps/config/install-hooks) pour `definePreInstallLogicFunction`, `definePostInstallLogicFunction` et `defineUninstallLogicFunction`.
</Note>
## Créer une activité de chronologie.
Utilisez `createTimelineActivity()` pour publier un événement de domaine explicite depuis une fonction logique. Définissez d'abord l'événement comme un [type d'activité de chronologie](/l/fr/developers/extend/apps/data/timeline-activity-types), puis référencez le type et les objets à l'aide de leurs identifiants universels stables :
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
Twenty résout les identifiants universels en identifiants de métadonnées spécifiques à l'installation, valide que le type d'activité de chronologie appartient à l'application appelante et capture un instantané de ses métadonnées de présentation dans la nouvelle activité.
Les entrées requises sont `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` et `targetRecordId`. Vous pouvez également fournir `happensAt`, `properties` et `workspaceMemberId`. `happensAt` contrôle la position et l'heure affichée de l'événement dans la chronologie ; par défaut, il correspond à l'heure de création.
Pour associer un autre enregistrement à l'événement, fournissez ensemble `linkedRecordId` et `linkedObjectMetadataUniversalIdentifier`. Vous pouvez également fournir `linkedRecordCachedName` comme solution de repli d'affichage historique :
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
Le rôle de la fonction logique doit disposer de l'autorisation d'écriture sur l'objet standard `timelineActivity`. Gardez les types d'événements explicites non liés à une `action` ; un type lié à une action reçoit déjà des événements d'audit automatiques et produirait sinon des lignes en double.
## Clients d'API typés (twenty-client-sdk)
Le package `twenty-client-sdk` fournit deux clients GraphQL typés pour interagir avec l'API Twenty depuis vos fonctions logiques et vos composants frontaux.
| Client | Importer | Point de terminaison | Généré ? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------------------ | --------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — données de l'espace de travail (enregistrements, objets) | Oui, au moment du dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configuration de l'espace de travail, téléversements de fichiers | Non, livré prêt à l'emploi |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Interroger et modifier les données de l'espace de travail (enregistrements, objets)">
`CoreApiClient` est le client principal pour interroger et modifier les données de l'espace de travail. Il est **généré à partir du schéma de votre espace de travail** lors de l'exécution de `yarn twenty dev` ou `yarn twenty dev:build`, il est donc entièrement typé pour correspondre à vos objets et champs.
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Le client utilise une syntaxe d'ensemble de sélection : passez `true` pour inclure un champ, utilisez `__args` pour les arguments et imbriquez des objets pour les relations. Vous bénéficiez d'une autocomplétion complète et d'une vérification de types basée sur le schéma de votre espace de travail.
<Note>
**CoreApiClient est généré au moment du dev/build.** Si vous l'utilisez sans exécuter d'abord `yarn twenty dev` ou `yarn twenty dev:build`, une erreur est levée. La génération se fait automatiquement — la CLI inspecte le schéma GraphQL de votre espace de travail et génère un client typé à l'aide de `@genql/cli`.
</Note>
#### Utiliser CoreSchema pour les annotations de type
`CoreSchema` fournit des types TypeScript correspondant à vos objets d'espace de travail — utile pour typer l'état des composants ou les paramètres de fonction :
```ts
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
</Accordion>
<Accordion title="MetadataApiClient" description="Configuration de l'espace de travail, applications et téléversements de fichiers">
`MetadataApiClient` est livré prêt à l'emploi avec le SDK (aucune génération requise). Il interroge le point de terminaison `/metadata` pour la configuration de l'espace de travail, les applications et les téléversements de fichiers. Il accepte la même option `runAs` que `CoreApiClient` — voir [Quels accès un appel utilise](#whose-access-a-call-uses).
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### Téléverser des fichiers
Le `MetadataApiClient` inclut une méthode `uploadFile` pour joindre des fichiers aux champs de type fichier :
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| Paramètre | Type | Description |
| ---------------------------------- | -------- | ---------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Le contenu brut du fichier |
| `filename` | `string` | Le nom du fichier (utilisé pour le stockage et l'affichage) |
| `contentType` | `string` | Type MIME (par défaut `application/octet-stream` s'il est omis) |
| `fieldMetadataUniversalIdentifier` | `string` | Le `universalIdentifier` du champ de type fichier de votre objet |
Points clés :
* Utilise le `universalIdentifier` du champ (et non son ID propre à l'espace de travail), de sorte que votre code de téléversement fonctionne dans tout espace de travail où votre application est installée.
* L'`url` renvoyée est une URL signée que vous pouvez utiliser pour accéder au fichier téléversé.
</Accordion>
</AccordionGroup>
<Note>
Lorsque votre code s'exécute sur Twenty (fonctions logiques ou composants frontaux), la plateforme injecte des identifiants sous forme de variables d'environnement :
* `TWENTY_API_URL` — URL de base de l'API Twenty
* `TWENTY_APP_ACCESS_TOKEN` — Clé de courte durée pour l'accès par défaut : le rôle d'une personne combiné à celui de votre application lorsqu'une personne est à l'origine de l'exécution, le propre rôle de votre application lorsque personne ne l'est. La personne est celle qui a déclenché l'exécution dans une fonction logique, ou celle qui consulte la page dans un composant frontal.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Clé de courte durée limitée au seul rôle de votre application. Uniquement pour les fonctions logiques, toujours injectée dans celles-ci, et utilisée par `runAs: 'application'`.
Vous n'avez **pas** besoin de les transmettre aux clients — ils lisent automatiquement depuis `process.env`, et [Quels accès un appel utilise](#whose-access-a-call-uses) explique comment choisir entre eux. Les propres autorisations de votre application sont déterminées par le rôle déclaré avec `defineApplicationRole()` (ou référencé via `defaultRoleUniversalIdentifier` dans `application-config.ts`) ; une exécution agissant en tant que personne ne peut jamais dépasser ce rôle, ni le sien.
</Note>

View File

@@ -0,0 +1,61 @@
---
title: Vue d'ensemble
description: TypeScript côté serveur qui s’exécute dans Twenty — déclenché par des routes HTTP, des planifications CRON, des événements de base de données, des outils d’IA ou des actions de flux de travail.
icon: bolt
---
La **couche logique** d’une application Twenty est le code qui *s’exécute* — des gestionnaires TypeScript côté serveur qui réagissent aux requêtes HTTP, aux planifications CRON et aux modifications d’enregistrements ; des compétences et des agents d’IA qui résident dans l’espace de travail ; et des connexions OAuth qui permettent à vos fonctions d’agir au nom d’un utilisateur dans des services tiers.
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
▼
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Fonctions logiques" icon="bolt" href="/l/fr/developers/extend/apps/logic/logic-functions">
Le bloc de construction principal — types de déclencheurs, charges utiles et client API typé.
</Card>
<Card title="Compétences et agents" icon="robot" href="/l/fr/developers/extend/apps/logic/skills-and-agents">
Instructions d’agent IA réutilisables et assistants avec invites système personnalisées.
</Card>
<Card title="Connexions" icon="plug" href="/l/fr/developers/extend/apps/logic/connections">
Identifiants OAuth détenus par votre application pour des services tiers — Linear, GitHub, Slack, et plus encore.
</Card>
<Card title="Stockage clé-valeur" icon="database" href="/l/fr/developers/extend/apps/logic/key-value-store">
Conservez l’état entre les exécutions de fonctions logiques — caches, curseurs et revendications inter-espaces de travail.
</Card>
<Card title="Tâches en arrière-plan" icon="layer-group" href="/l/fr/developers/extend/apps/logic/background-jobs">
Mettez en file d’attente l’exécution d’une fonction logique sur les workers afin de contourner la limite de temps par exécution.
</Card>
</CardGroup>
## Aperçu des types de déclencheurs
Une fonction logique choisit un ou plusieurs déclencheurs — chaque entrée ci-dessous est un champ distinct sur `defineLogicFunction()`:
| Déclencheur | Moment d’exécution | Paramètre |
| -------------------------------- | ---------------------------------------------------------------------------- | ------------------------------- |
| **Route HTTP** | Une requête atteint votre point de terminaison `/s/\<path>` | `httpRouteTriggerSettings` |
| **Cron** | Une expression CRON correspond | `cronTriggerSettings` |
| **Événement de base de données** | Un enregistrement de l’espace de travail est créé, mis à jour ou supprimé | `databaseEventTriggerSettings` |
| **Outil IA** | Une fonctionnalité IA de Twenty décide d’appeler votre fonction | `toolTriggerSettings` |
| **Action de flux de travail** | Une étape de flux de travail invoque votre fonction | `workflowActionTriggerSettings` |
Les fonctions s’exécutent dans un environnement isolé dans des processus Node.js sandboxés et accèdent à l’espace de travail via un client API typé, limité au rôle déclaré sur [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
<Note>
Les **hooks au moment de l’installation** — du code qui s’exécute avant ou après l’installation — partagent ce runtime mais utilisent leurs propres fonctions define et se trouvent sous [Config → Install Hooks](/l/fr/developers/extend/apps/config/install-hooks).
</Note>

View File

@@ -0,0 +1,190 @@
---
title: Compétences et agents
description: Définir les compétences et les agents d'IA pour votre application.
icon: robot
---
<Warning>
Les compétences et les agents sont actuellement en phase alpha. Cette fonctionnalité fonctionne mais est encore en évolution.
</Warning>
Les applications peuvent définir des fonctionnalités d'IA au sein de l'espace de travail — des instructions de compétences réutilisables et des agents avec des invites système personnalisées.
<AccordionGroup>
<Accordion title="defineSkill" description="Définir des compétences pour les agents IA">
Les compétences définissent des instructions et des capacités réutilisables que les agents IA peuvent utiliser dans votre espace de travail. Utilisez `defineSkill()` pour définir des compétences avec validation intégrée :
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Points clés :
* `name` est une chaîne d'identification unique pour la compétence (kebab-case recommandé).
* `label` est le nom d'affichage lisible par l'utilisateur dans l'UI.
* `content` contient les instructions de la compétence — c'est le texte que l'agent IA utilise.
* `icon` (optionnel) définit l'icône affichée dans l'UI.
* `description` (optionnel) fournit un contexte supplémentaire sur l'objectif de la compétence.
</Accordion>
<Accordion title="defineAgent" description="Définir des agents d'IA avec des prompts personnalisés">
Les agents sont des assistants IA qui vivent dans votre espace de travail. Utilisez `defineAgent()` pour créer des agents avec un prompt système personnalisé :
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
Points clés :
* `name` est un identifiant unique pour l'agent (kebab-case recommandé).
* `label` est le nom d'affichage montré dans l'UI.
* `prompt` est le prompt système qui définit le comportement de l'agent.
* `description` (optionnel) fournit un contexte sur ce que fait l'agent.
* `icon` (optionnel) définit l'icône affichée dans l'UI.
* `modelId` (optionnel) remplace le modèle d'IA par défaut utilisé par l'agent.
* `responseFormat` (facultatif) contrôle la forme de la sortie de l'agent. Par défaut, la valeur est `{ type: 'text' }` pour le texte libre. Utilisez `{ type: 'json', schema }` pour forcer une sortie JSON structurée.
Par défaut, un agent renvoie du texte libre. Pour obtenir une sortie structurée, définissez `responseFormat` sur `{ type: 'json' }` et fournissez un `schema` :
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
Remarques sur le schéma :
* Le schéma est un objet plat : le `type` de chaque propriété doit être un type primitif (`string`, `number` ou `boolean`). Les objets imbriqués et les tableaux ne sont pas pris en charge.
* `description` (facultatif) sur chaque propriété guide le modèle sur ce qu'il doit y mettre.
* `required` (facultatif) répertorie les propriétés que le modèle doit toujours renvoyer.
* `additionalProperties: false` (facultatif) interdit toute propriété non déclarée dans `properties`.
</Accordion>
<Accordion title="runAgent" description="Exécuter un agent depuis une fonction logique">
`runAgent()` permet à une fonction logique d'exécuter l'un des agents de votre application (avec ses compétences et ses outils). Identifiez l'agent à l'aide du `universalIdentifier` que vous avez passé
à `defineAgent()`. Passez soit une chaîne de caractères `prompt`, soit un historique de conversation `messages`,
mais pas les deux :
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
Pour les bots multi-tours (Slack, Discord, Teams, …), passez l'historique du fil de discussion en
`messages` au lieu d'un seul `prompt` :
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
Points clés :
* Fournissez **exactement l'un des deux** : `prompt` (string) ou `messages` (1 à 100 entrées
de `{ role: 'user' | 'assistant', content: string }`).
* L'agent s'exécute **de manière synchrone** et peut lire/mettre à jour lui-même des enregistrements via ses propres outils — `runAgent()` se résout une fois l'exécution terminée.
* Une application ne peut exécuter que ses propres agents.
* Le [rôle par défaut](/l/fr/developers/extend/apps/config/roles) de l'application doit accorder l'indicateur d'autorisation `AI` — ajoutez `SystemPermissionFlag.AI` à ses `permissionFlagUniversalIdentifiers` (ou définissez `canAccessAllTools: true`).
Sans cela, `runAgent()` échoue avec une erreur d'autorisation.
* Définissez une valeur généreuse pour `timeoutSeconds` sur la fonction logique — les exécutions d'agent peuvent prendre plusieurs secondes.
* `success` est `true` et `result` est non nul lorsque l'exécution se termine ; en cas d'échec, `success` est `false`, `result` est `null`, et `error` contient la raison (par exemple, lorsque l'espace de travail épuise ses crédits AI en cours d'exécution).
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**Évitez les boucles :** si vous appelez `runAgent()` à partir d'un déclencheur d'événement de base de données `*.updated` et que l'agent met à jour le même enregistrement, limitez le déclencheur avec `updatedFields` à un champ que l'agent n'écrit jamais (par exemple l'URL source), ou vérifiez si un champ cible est encore vide avant d'appeler `runAgent()`.
</Warning>
### Exécution au nom d’un membre de l’espace de travail
Passez `runAsWorkspaceMemberId` lorsque l’exécution est déclenchée par une personne — par exemple un chatbot répondant à un message — afin que l’agent agisse comme ce membre plutôt qu’en tant qu’application :
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
L’exécution agit alors avec **le propre rôle du membre** : elle peut faire tout ce que ce membre peut faire et rien de plus, les enregistrements qu’elle crée lui sont attribués, et ses permissions au niveau des lignes s’appliquent. Le rôle de l’agent ne participe pas — il s’agit de la valeur par défaut de l’application pour les exécutions sans personne derrière. Omettez ce champ pour les exécutions autonomes (tâches planifiées, déclencheurs d’événements de base de données) : celles-ci conservent le rôle propre de l’agent. L’application qui agit reste rattachée au contexte de l’exécution à des fins de traçabilité, sans restreindre les permissions du membre.
Votre application est responsable de faire correspondre la personne qui a déclenché l’exécution à un membre de l’espace de travail. Nommer un membre nécessite un jeton d’accès de l’application, et ce qu’un jeton peut
nommer dépend du fait qu’il soit ou non associé à un utilisateur :
* Un jeton **sans utilisateur associé** peut nommer n’importe quel membre. Une fonction logique s’exécute
avec un tel jeton, tout comme les jetons créés via `client_credentials` ou à partir d’une clé
d’API.
* Un jeton émis **au nom d’un utilisateur**, comme en reçoit un composant frontal, ne peut
nommer que le membre de cet utilisateur lui-même.
Tout autre type — une simple session utilisateur, une clé d’API sans jeton d’application — ne peut pas
nommer de membre du tout.
<Warning>
`runAgent()` lève une exception lorsque le membre de l’espace de travail ne peut pas être résolu — un
membre inconnu ou supprimé, ou un membre sans rôle. Elle ne revient jamais au rôle
propre de l’agent, car cela accorderait plus d’accès que ce que l’appelant a demandé.
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: Les commandes `yarn twenty` pour exécuter des fonctions, diffuser en continu les journaux, gérer les installations d'applications et basculer entre des remotes.
icon: terminal
---
Le CLI `yarn twenty` est votre interface pour tout ce qui concerne les applications. Liste complète des commandes :
| Commande | Ce que cela fait | Documenté dans |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `dev` | Surveille les fichiers sources et synchronise en direct les modifications | [Prise en main rapide](/l/fr/developers/extend/apps/getting-started/quick-start) |
| `plan` | Prévisualiser les modifications de métadonnées sans les appliquer | [Synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `appliquer` | Appliquer les modifications de métadonnées après avoir affiché le plan | [Synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | Compiler l’application et générer le client d’API (`--tarball` pour empaqueter un `.tgz`) | [Publication](/l/fr/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | Exécuter la vérification des types TypeScript | [Tests](/l/fr/developers/extend/apps/operations/testing) |
| `dev:add` | Générer la structure d’une nouvelle entité | [Génération de structure](/l/fr/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | Régénérer le client d’API typé | cette page |
| `dev:function:exec` / `dev:function:logs` | Exécuter des fonctions et afficher leurs journaux en continu | cette page |
| `dev:translations-extract` | Extraire les chaînes traduisibles dans les catalogues `locales/` | [Traductions](/l/fr/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | Déclencher la synchronisation du catalogue de la place de marché | [Publication](/l/fr/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | Cycle de vie de publication | [Publication](/l/fr/developers/extend/apps/operations/publishing) et cette page |
| `docker:*` | Gérer le conteneur du serveur Twenty local | [Serveur local](/l/fr/developers/extend/apps/getting-started/local-server) |
| `remote:*` | Gérer les connexions serveur | cette page |
Chaque commande accepte `-r, --remote \<name>` pour cibler un serveur distant spécifique au lieu de celui par défaut.
## Exécuter des fonctions (`yarn twenty dev:function:exec`)
Exécutez une fonction logique manuellement sans la déclencher via HTTP, cron ou un événement de base de données :
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## Afficher les journaux des fonctions (`yarn twenty dev:function:logs`)
Diffuser en continu les journaux d'exécution des fonctions logiques de votre application :
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Ceci est différent de `yarn twenty docker:logs`, qui affiche les journaux du conteneur Docker. `yarn twenty dev:function:logs` affiche les journaux d'exécution des fonctions de votre application depuis le serveur Twenty.
</Note>
## Génération du client typé (`yarn twenty dev:generate-client`)
Régénérez le client API typé (`twenty-client-sdk`) à partir du schéma du remote actif, sans compiler ni synchroniser une application. Utilisez-le pour obtenir un client typé dans n’importe quel projet — comme un service back-end hébergé dans un dépôt distinct — qui communique avec votre instance Twenty :
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
Puis importez le client dans votre code :
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
Relancez la commande chaque fois que votre modèle de données change afin d’actualiser les types générés.
<Note>
Le client est généré dans `node_modules`, il n’est donc pas versionné avec votre code. Exécutez `yarn twenty dev:generate-client` après chaque installation (par exemple dans un script `postinstall` ou dans un pipeline CI).
</Note>
## Désinstaller une application (`yarn twenty app:uninstall`)
Supprimez votre application de l'espace de travail actif :
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## Gestion des remotes
Un remote est un serveur Twenty auquel votre application se connecte. Lors de la configuration, le générateur en crée un automatiquement pour vous. Vous pouvez ajouter d’autres remotes ou basculer entre eux à tout moment.
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
Vos identifiants sont stockés dans `~/.twenty/config.json`.

View File

@@ -0,0 +1,32 @@
---
title: Vue d'ensemble
description: Créez, testez et livrez votre application — commandes CLI, tests d’intégration, CI et publication sur un serveur ou sur npm.
icon: rocket
---
La **couche d’opérations** regroupe tout ce que vous faites *à* votre application plutôt qu’*avec* elle : lancer des commandes CLI, exécuter des tests d’intégration sur un véritable serveur Twenty, configurer la CI et livrer des versions — soit sous la forme d’une archive tar déployée sur un seul serveur, soit sous la forme d’un package npm répertorié sur la place de marché.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## Dans cette section
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/fr/developers/extend/apps/operations/cli">
Référence `yarn twenty` — exec, logs, uninstall, remotes.
</Card>
<Card title="Synchronisation et récupération" icon="compass" href="/l/fr/developers/extend/apps/operations/sync-and-recovery">
Quelle commande utiliser et quand, comment lire le diff de synchronisation, et une procédure de récupération.
</Card>
<Card title="Tests" icon="flask" href="/l/fr/developers/extend/apps/operations/testing">
Configuration de Vitest, tests d’intégration, vérification des types, flux de travail CI.
</Card>
<Card title="Publication" icon="upload" href="/l/fr/developers/extend/apps/operations/publishing">
Générer, déployer une archive tar, publier sur npm, installer.
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: Publication
icon: upload
description: Distribuez votre application Twenty sur la marketplace ou déployez-la en interne.
---
## Vue d'ensemble
Une fois que votre application est [créée et testée en local](/l/fr/developers/extend/apps/getting-started/concepts), vous avez deux options pour sa distribution :
* **Déployer une archive tar** — téléversez votre application directement sur un serveur Twenty spécifique pour un usage interne ou privé.
* **Publier sur npm** — répertoriez votre application dans la marketplace Twenty pour que tout espace de travail puisse la découvrir et l'installer.
Les deux parcours commencent à la même étape de **build**.
## Compilation de votre application
Exécutez la commande `build` pour compiler votre application et générer un `manifest.json` prêt pour la distribution :
```bash filename="Terminal"
yarn twenty dev:build
```
Cela compile les sources TypeScript, transpile les fonctions de logique et les composants front-end, et écrit le tout dans `.twenty/output/`. Ajoutez `--tarball` pour produire également un paquet `.tgz` pour la distribution manuelle ou la commande `publish`.
## Déploiement sur un serveur (archive tar)
Pour les applications que vous ne souhaitez pas rendre publiques — outils propriétaires, intégrations réservées aux entreprises ou versions expérimentales — vous pouvez déployer une archive tar directement sur un serveur Twenty.
### Prérequis
Avant le déploiement, vous avez besoin d'un remote configuré pointant vers le serveur cible. Les remotes stockent localement l'URL du serveur et les informations d'authentification dans `~/.twenty/config.json`.
Ajouter un remote :
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### Déploiement
Compilez et téléversez votre application sur le serveur en une seule étape :
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### Partage d'une application déployée
Les applications au format tarball ne sont pas répertoriées dans la place de marché publique, donc les autres espaces de travail sur le même serveur ne les découvriront pas en parcourant. Pour partager une application déployée :
1. Allez dans **Paramètres > Applications > Inscriptions** et ouvrez votre application
2. Dans l'onglet **Distribution**, cliquez sur **Copier le lien de partage**
3. Partagez ce lien avec des utilisateurs sur d'autres espaces de travail — il les amène directement à la page d'installation de l'application
Le lien de partage utilise l'URL de base du serveur (sans sous-domaine d'espace de travail) afin qu'il fonctionne pour tout espace de travail sur le serveur.
### Gestion des versions
Lors de la mise à jour d'une application au format tarball déjà déployée, le serveur exige que la `version` dans `package.json` soit **strictement supérieure** (selon l'ordre [SemVer](https://semver.org)) à la version actuellement déployée. Le redéploiement de la même version, ou la tentative d’en pousser une inférieure, est refusé avant que l’archive tar ne soit stockée — vous verrez une erreur `VERSION_ALREADY_EXISTS` émise par la CLI.
Pour publier une mise à jour :
1. Incrémentez le champ `version` dans votre `package.json` (par ex. `1.2.3` → `1.2.4`, `1.3.0` ou `2.0.0`)
2. Exécutez `yarn twenty app:publish --private` (ou `yarn twenty app:publish --private --remote production`)
3. Les espaces de travail qui ont installé l’application et y ont activé la mise à niveau automatique (dans l’onglet Paramètres de l’application) sont mis à niveau automatiquement en arrière-plan ; les autres verront la mise à niveau disponible dans leurs paramètres
<Note>
Les tags de préversion fonctionnent comme prévu : incrémenter `1.0.0-rc.1` → `1.0.0-rc.2` est autorisé, et une version finale telle que `1.0.0` est correctement reconnue comme supérieure à `1.0.0-rc.5`. La version dans `package.json` doit elle-même être une chaîne SemVer valide.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### Compatibilité des versions du serveur
Si votre application utilise une fonctionnalité introduite dans une version spécifique du serveur Twenty (par exemple, des fournisseurs OAuth ajoutés en v2.3.0), vous devez déclarer la version minimale du serveur requise par votre application en utilisant le champ `engines.twenty` dans `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
La valeur est une [plage semver](https://github.com/npm/node-semver#ranges) standard. Modèles courants:
| Plage | Signification |
| ---------------------------------- | ------------------------------------------------------------------- |
| `>=2.3.0` | Tout serveur à partir de 2.3.0 |
| `>=2.3.0 \<3.0.0` | 2.3.0 ou ultérieure, mais inférieure à la prochaine version majeure |
| `^2.3.0` | Identique à `>=2.3.0 \<3.0.0` |
**Ce qui se passe lors du déploiement et de l'installation:**
* Si `engines.twenty` est défini et que la version du serveur cible ne satisfait pas la plage, le déploiement (chargement du tarball) ou l'installation est refusé avec une erreur `SERVER_VERSION_INCOMPATIBLE` et un message indiquant à la fois la plage requise et la version réelle du serveur.
* Si `engines.twenty` est **non défini**, l'application est acceptée sur n'importe quelle version du serveur (rétrocompatible avec les applications existantes).
* Si le serveur n'a pas de `APP_VERSION` configuré, la vérification est ignorée.
<Note>
Le serveur est l'autorité de référence pour la vérification — il valide `engines.twenty` tant lors du chargement du tarball que lors de l'installation dans l'espace de travail. Si vous déployez un tarball hors bande ou installez depuis la place de marché, le serveur applique toujours la compatibilité.
</Note>
## CI/CD automatisés (workflows préconfigurés)
Les applications générées avec `create-twenty-app` sont livrées avec trois workflows GitHub Actions prêts à l’emploi, sous `.github/workflows/`. L’intégration continue (CI) s’exécute sans configuration, le déploiement continu (CD) ne nécessite qu’un seul secret, et la publication sur npm requiert une configuration unique de npm trusted-publisher.
### CI — `ci.yml`
Il exécute automatiquement vos tests d’intégration à chaque push sur `main` et sur les pull requests.
**Ce que cela fait:**
1. Récupère le code source de votre application.
2. Lance une instance de test Twenty isolée à l’aide de l’action composite `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (l’équivalent CI de `yarn twenty docker:start --test`).
3. Active Corepack, configure Node.js à partir de votre `.nvmrc` et installe les dépendances avec `yarn install --immutable`.
4. Exécute `yarn test`, en passant `TWENTY_API_URL` et `TWENTY_API_KEY` depuis l’instance lancée pour que vos tests puissent communiquer avec un serveur réel.
**Paramètres de configuration:**
* `TWENTY_VERSION` (variable d’environnement, par défaut `latest`) — figez la version du serveur Twenty utilisée en CI en la modifiant dans `ci.yml`.
* La concurrence est regroupée par `github.ref` et annule les exécutions en cours lors de nouveaux push.
Aucun secret n’est requis — l’instance de test est éphémère et n’existe que pendant la durée du job.
### CD — `cd.yml`
Déploie votre application vers un serveur Twenty configuré à chaque push sur `main`, et éventuellement depuis une pull request lorsque le label `deploy` est appliqué.
**Ce que cela fait:**
1. Récupère la tête de la PR (pour les PR étiquetées) ou le commit poussé.
2. Exécute `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — l’équivalent CI de `yarn twenty app:publish --private`.
3. Exécute `twentyhq/twenty/.github/actions/install-twenty-app@main` afin que la version nouvellement déployée soit installée dans l’espace de travail cible.
**Configuration requise:**
| Paramètre | Où | Objectif |
| ----------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_DEPLOY_URL` | `env` dans `cd.yml` (par défaut `http://localhost:3000`) | Le serveur Twenty vers lequel déployer. Remplacez ceci par l’URL réelle de votre serveur avant la première utilisation. |
| `TWENTY_DEPLOY_API_KEY` | Dépôt GitHub **Settings → Secrets and variables → Actions** | Clé API avec la permission de déployer sur le serveur cible. |
<Note>
La valeur par défaut de `TWENTY_DEPLOY_URL`, `http://localhost:3000`, est un espace réservé — elle n’atteindra rien depuis un runner hébergé par GitHub. Remplacez-la par l’URL publique de votre serveur (ou utilisez un runner auto-hébergé avec accès réseau) avant d’activer la CD.
</Note>
**Déclencher un déploiement de prévisualisation depuis une PR:**
Ajoutez le label `deploy` à une pull request. La condition `if:` dans `cd.yml` exécutera le job pour cette PR en utilisant le commit de tête de la PR, vous permettant de valider une modification sur le serveur cible avant la fusion.
### Publication — `publish.yml`
Publie votre application sur npm avec provenance lorsque vous poussez une étiquette de version (par exemple `v1.0.0`), ou lorsque vous exécutez manuellement le workflow depuis l’onglet Actions.
**Ce que cela fait:**
1. Récupère votre application, configure Node.js et met à jour npm (la publication de confiance nécessite npm 11.5.1 ou une version ultérieure).
2. Exécute `yarn twenty app:publish`, qui génère l’application et publie `.twenty/output` sur npm. En CI, il ajoute automatiquement `--provenance` et `--access public`, de sorte qu’aucun indicateur n’est nécessaire dans le workflow.
**Configuration unique :**
Sur npmjs.com, ouvrez votre package > **Settings → Trusted Publisher** et enregistrez ce dépôt avec le workflow `publish.yml` (voir la [documentation npm sur la publication de confiance](https://docs.npmjs.com/trusted-publishers)). La publication avec provenance certifie quel dépôt GitHub a construit le package, ce qui est aussi la façon de revendiquer la propriété de votre application dans une marketplace Twenty.
<Note>
npm n’accepte la provenance que depuis des dépôts sources **publics**. Si vous publiez depuis un dépôt privé, npm rejette le bundle de provenance OIDC avec une erreur `E422 ... Erreur « Unsupported GitHub Actions source repository visibility: "private"` ». Pour publier depuis un dépôt privé, désactivez la provenance en définissant `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` dans la variable `env` de l’étape de publication (un indice commenté est inclus dans le fichier `publish.yml` généré automatiquement) :
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### Épingler les actions réutilisables
Les workflows `ci.yml` et `cd.yml` référencent des actions réutilisables sur `@main`, de sorte que les mises à jour des actions dans le dépôt `twentyhq/twenty` sont récupérées automatiquement. Si vous souhaitez des builds déterministes, remplacez `@main` par un SHA de commit ou un tag de version sur chaque ligne `uses:`.
## Publication sur npm
Publier sur npm rend votre application découvrable dans la marketplace Twenty. N'importe quel espace de travail Twenty peut parcourir, installer et mettre à niveau des applications de la marketplace directement depuis l'interface utilisateur.
### Prérequis
* Un compte [npm](https://www.npmjs.com)
* Le mot-clé `twenty-app` dans le tableau `keywords` de votre `package.json` (à ajouter manuellement — il n'est pas inclus par défaut dans le modèle `create-twenty-app`)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### Métadonnées de la marketplace
La configuration `defineApplication()` prend en charge des champs optionnels qui contrôlent la façon dont votre application apparaît sur la place de marché. Utilisez `logo` et `galleryImages` pour référencer des images depuis le dossier `public/` :
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
Voir l'[accordéon defineApplication](/l/fr/developers/extend/apps/config/application#marketplace-metadata) dans la page Building Apps pour la liste complète des champs de la place de marché (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensions recommandées pour les images de la galerie
La place de marché affiche `galleryImages` dans un conteneur fixe `8:5` (par exemple, `1600×1000 px`).
<Note>
Les images de la galerie, quel que soit leur ratio d'aspect, sont affichées en entier et ne sont jamais recadrées, mais tout ce qui est nettement plus haut ou plus étroit que `8:5` affichera des bandes vides sur les côtés.
</Note>
#### Limite de taille d’image
Le `logo` et chaque fichier `galleryImages` ne doivent pas dépasser **10 Mo**. Les fichiers plus volumineux sont ignorés lorsque la place de marché réhéberge vos ressources publiées, ils ne seront donc pas affichés.
### Publier
```bash filename="Terminal"
yarn twenty app:publish
```
Pour publier sous un dist-tag spécifique (par exemple, `beta` ou `next`) :
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### Comment fonctionne la découverte de la place de marché
Le serveur Twenty synchronise son catalogue de la place de marché à partir du registre npm **toutes les heures**.
Vous pouvez déclencher la synchronisation immédiatement au lieu d'attendre :
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
Les métadonnées affichées dans la marketplace proviennent de votre configuration `defineApplication()` — voir [Métadonnées de la marketplace](#marketplace-metadata) ci-dessus.
<Note>
Si votre application ne définit pas de `aboutDescription` dans `defineApplication()`, la place de marché utilisera automatiquement le `README.md` de votre package depuis npm comme contenu de la page À propos. Cela signifie que vous pouvez maintenir un seul README à la fois pour npm et pour la place de marché Twenty. Si vous souhaitez une description différente dans la place de marché, définissez explicitement `aboutDescription`.
</Note>
### Publication via CI
Le workflow `publish.yml` préconfiguré décrit ci-dessus publie automatiquement sur npm lors des tags de version, avec provenance. Comme `yarn twenty app:publish` ajoute `--provenance` et `--access public` pour vous lorsqu’il s’exécute en CI, le workflow n’a besoin d’aucune option npm — seulement de la configuration unique de Trusted Publisher.
Pour d’autres systèmes de CI (GitLab CI, CircleCI, etc.), exécutez `yarn install` puis `yarn twenty app:publish`. La provenance est émise lorsque l’environnement peut générer un jeton OIDC et est ignorée automatiquement dans le cas contraire.
<Note>
La **provenance npm** ajoute un badge de confiance à votre fiche npm, permettant aux utilisateurs de vérifier que le package a été construit à partir d’un commit spécifique dans un pipeline CI public. C’est également ce qui vous permet de revendiquer la propriété de votre application dans une marketplace Twenty. Voir la [documentation npm sur la provenance](https://docs.npmjs.com/generating-provenance-statements) pour plus de détails.
</Note>
## Installation des applications
Une fois qu'une application est publiée (npm) ou déployée (tarball), les espaces de travail peuvent l'installer via l'interface utilisateur.
Accédez à la page **Paramètres > Applications** dans Twenty, où les applications de la place de marché et celles déployées en tarball peuvent être parcourues et installées.
{/* TODO: add screenshot of the UI when the app is registered */}
Vous pouvez également installer des applications depuis la ligne de commande :
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
Le serveur applique le versionnement SemVer à l’installation, en reprenant les mêmes règles que lors du déploiement :
* L’installation de la même version déjà installée dans votre espace de travail est refusée avec une erreur `APP_ALREADY_INSTALLED`.
* L’installation d’une version inférieure à celle actuellement installée est refusée avec une erreur `CANNOT_DOWNGRADE_APPLICATION`.
Pour installer une version plus récente, déployez-la ou publiez-la d’abord, puis relancez `yarn twenty app:install`.
</Note>

View File

@@ -0,0 +1,126 @@
---
title: Synchronisation et récupération
description: Quelle commande utiliser et quand, comment lire la sortie de synchronisation, et une échelle de récupération pour les cas où les métadonnées locales dérivent — avant d'en arriver à une réinitialisation complète.
icon: compass
---
Le développement local d'applications tourne autour de la **synchronisation** : le CLI reconstruit votre manifeste et le serveur applique uniquement la différence entre celui-ci et les métadonnées déjà présentes dans votre espace de travail. Cette page explique vers quelle commande se tourner, comment lire ce qu'une synchronisation a modifié, et quoi faire — dans l'ordre — lorsque l'état local semble incohérent.
## Quelle commande, quand
<Note>
Pour l'itération locale au quotidien, vous voudrez presque toujours `yarn twenty dev`. Le déploiement et la publication servent à livrer des versions, **pas** pour la boucle locale.
</Note>
| Vous souhaitez… | Commande | Notes |
| ------------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Itérer localement avec la synchronisation en direct | `yarn twenty dev` | Surveille vos fichiers et synchronise à chaque modification. |
| Synchroniser une fois puis quitter (CI, scripts, hooks) | `yarn twenty apply` | Effectue une compilation + synchronisation, puis quitte. Ajoutez `--force` pour ignorer la confirmation des changements destructifs. |
| Prévisualiser les changements **sans les appliquer** | `yarn twenty plan` | Calcule et affiche le diff ; n'écrit rien. |
| Retirer l'application de l'espace de travail | `yarn twenty app:uninstall` | Ajoutez `--yes` pour ignorer la confirmation. |
| Envoyer une archive tarball vers un serveur | `yarn twenty app:publish --private` | Nécessite une version de `package.json` **strictement supérieure** — voir [Publication](/l/fr/developers/extend/apps/operations/publishing). |
| Publier sur la place de marché (npm) | `yarn twenty app:publish` | — |
| Installer / mettre à niveau une version déployée | `yarn twenty app:install` | Installe la version actuellement déployée. |
| Effacer le serveur local et repartir de zéro | `yarn twenty docker:reset` | Supprime **toutes** les données locales — en dernier recours. |
<Note>
`yarn twenty dev --once` et `yarn twenty dev --once --dry-run` fonctionnent toujours comme alias obsolètes de `yarn twenty apply` et `yarn twenty plan`.
</Note>
### La synchronisation locale n'a pas besoin d'un incrément de version
La règle de `version` strictement croissante (`VERSION_ALREADY_EXISTS` lors du déploiement, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` lors de l'installation) s'applique à **`app:publish` / `app:install`** — le chemin de mise en production. `yarn twenty dev` synchronise votre manifeste sur place et ne nécessite jamais de changement de version, vous n'avez donc pas besoin de toucher à `package.json` pour itérer. Si vous vous surprenez à incrémenter la version pour tester un changement local, c'est que vous utilisez le chemin de mise en production alors que vous voulez la boucle de développement.
## Lire la sortie de synchronisation
Chaque synchronisation affiche les changements de métadonnées qu'elle a appliqués (ou qu'elle appliquerait, avec `plan`), à la manière de Terraform — un bloc par entité avec ses attributs, puis une ligne récapitulative :
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
C'est votre premier diagnostic : il vous indique exactement quels objets, champs et mises en page ont changé, afin que vous puissiez confirmer qu'une synchronisation a fait ce que vous attendiez avant de vérifier l'interface utilisateur.
Les changements destructifs (`to destroy`) sont listés avec ce qu'ils suppriment (par ex. `objectMetadata "auditNote" — drops the table and all its rows`) et nécessitent une confirmation interactive, ou `--force` dans les scripts.
Lorsqu'une synchronisation échoue sur une seule entité, l'erreur nomme l'entité en cause et son `universalIdentifier`, par exemple :
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
Utilisez cet identifiant pour trouver l'entité dans votre manifeste (et, si nécessaire, dans l'espace de travail) plutôt que de deviner laquelle est en conflit.
## Prévisualiser les changements (plan)
`yarn twenty plan` construit votre manifeste, demande au serveur le plan de migration et l'affiche — **sans rien appliquer**. C'est le moyen sûr de répondre « que changerait cette synchronisation ? » avant de s'y engager.
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
Un plan :
* **N'écrit rien** — aucune migration de métadonnées, aucune mise à jour de l'enregistrement d'application, aucun changement de rôle/onglet par défaut, et aucune génération de client d'API.
* Renvoie le **même diff** qu'une synchronisation réelle appliquerait, afin que vous puissiez examiner à l'avance les entités créées/mises à jour/supprimées.
* Est utile avant un changement risqué, lors de la révision d'un changement généré par une IA, ou dans un script qui doit échouer si un changement inattendu est sur le point d'être appliqué.
<Note>
Un plan ne prévisualise que les changements de **métadonnées**, et il nécessite que l'application ait été synchronisée au moins une fois (pour que l'espace de travail la connaisse). Si vous l'exécutez sur une application qui n'a jamais été synchronisée, le serveur indique que l'application n'est pas installée — exécutez d'abord une fois `yarn twenty dev`.
</Note>
## Échelle de récupération
Lorsque les métadonnées locales semblent incorrectes, augmentez le niveau de manière progressive dans cet ordre et arrêtez-vous dès que vous êtes débloqué. Chaque étape est plus perturbatrice que la précédente.
1. **Resynchroniser.** Exécutez à nouveau `yarn twenty apply`. Les synchronisations sont idempotentes — réexécuter un manifeste propre est sûr et résout souvent un incident passager.
2. **Prévisualiser le plan.** Exécutez `yarn twenty plan` pour voir exactement ce que la prochaine synchronisation compte changer, sans l'appliquer.
3. **Lire l'erreur nommée.** Si une synchronisation échoue, relevez le type de métadonnées et l'`universalIdentifier` dans le message (voir ci-dessus) et localisez cette entité dans votre manifeste. Un conflit pointe généralement vers un identifiant dupliqué ou réutilisé.
4. **Désinstaller et réinstaller.** `yarn twenty app:uninstall`, puis synchronisez à nouveau (`yarn twenty dev`). Cette opération reconstruit les métadonnées de l'application à partir d'une base saine tout en gardant le reste de votre espace de travail intact.
5. **Réinitialisation complète (en dernier recours).** `yarn twenty docker:reset`, puis réinjectez des données et resynchronisez.
<Warning>
`yarn twenty docker:reset` supprime **toutes** les données de votre instance locale — chaque espace de travail, enregistrement et application. Ne l'utilisez que lorsque les étapes précédentes ont échoué.
</Warning>
<Note>
Vous avez rencontré une erreur de métadonnées ? Veuillez [ouvrir un ticket](https://github.com/twentyhq/twenty/issues/new/choose) et inclure le message d'échec de migration (avec son type de métadonnées et son `universalIdentifier`), la sortie `Metadata changes` de la synchronisation, ainsi que les commandes que vous avez exécutées.
</Note>
## Éviter les synchronisations concurrentes sur un même espace de travail
La synchronisation applique des migrations de métadonnées. Exécuter plusieurs opérations de synchronisation, de déploiement ou d'installation contre le **même espace de travail au même moment** — par exemple, plusieurs terminaux ou agents IA qui itèrent en parallèle — peut entrelacer ces migrations et laisser les métadonnées dans un état partiellement appliqué.
Le serveur sérialise les synchronisations par espace de travail pour éviter cela, mais vous devriez tout de même faire transiter les opérations sensibles sur les métadonnées par un **processus unique** plutôt que de les lancer en concurrence. Si vous orchestrez le développement avec plusieurs agents, faites passer leurs appels de synchronisation/déploiement/installation par une file unique afin qu'un seul s'exécute à la fois.
## Distinguer les types d'échecs
Lorsque quelque chose se passe mal, le diff de métadonnées et les erreurs nommées vous permettent de localiser l'échec :
* **Erreur de construction du manifeste** — le CLI échoue avant la synchronisation (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`) ; corrigez le code source de votre application.
* **Erreur de synchronisation / migration** — la construction réussit mais l'application du diff échoue, en nommant l'entité et son `universalIdentifier` ; corrigez les métadonnées en conflit.
* **Erreur de taille des dépendances** — la synchronisation ou l’installation échoue parce que les `dependencies` de production de l’application sont trop volumineuses pour être installées en tant que couche d’exécution (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`) ; déplacez vers `devDependencies` les packages que vos fonctions logiques n’importent pas au moment de l’exécution (bibliothèques d’interface utilisateur, outils de développement).
* **Erreur d’exécution du code de l’application** — la synchronisation réussit, mais vos fonctions logiques ou composants se comportent mal à l’exécution ; consultez les [journaux de fonctions](/l/fr/developers/extend/apps/operations/cli).
* **État local de l’instance** — aucun des cas ci-dessus et l’espace de travail semble toujours incorrect ; descendez l’échelle de récupération.

View File

@@ -0,0 +1,264 @@
---
title: Tests
description: Configuration de Vitest, tests d’intégration sur un serveur Twenty réel, vérification des types et CI avec GitHub Actions.
icon: flask
---
Le SDK fournit des API programmatiques qui vous permettent de construire, déployer, installer et désinstaller votre application depuis le code de test. Combiné avec [Vitest](https://vitest.dev/) et les clients d'API typés, vous pouvez écrire des tests d'intégration qui vérifient que votre application fonctionne de bout en bout sur un serveur Twenty réel.
## Utilisation de packages npm
Vous pouvez installer et utiliser n'importe quel package npm dans votre application. Les fonctions logiques et les composants frontaux sont tous deux empaquetés avec [esbuild](https://esbuild.github.io/), qui intègre leurs dépendances dans la sortie — aucun `node_modules` n'est nécessaire à l'exécution.
### Installation d'un package
```bash filename="Terminal"
yarn add axios
```
Puis importez-le dans votre code :
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
Il en va de même pour les composants frontaux :
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### Comment fonctionne le bundling
L'étape de build utilise esbuild pour produire un seul fichier autonome par fonction logique et par composant frontal. Les paquets importés sont intégrés dans le bundle, à l’exception de ceux listés ci-dessous.
**Les fonctions logiques** s'exécutent dans un environnement Node.js. Les modules intégrés de Node (`fs`, `path`, `crypto`, `http`, etc.) sont disponibles et n'ont pas besoin d'être installés.
**Les composants frontaux** s'exécutent dans un Web Worker. Les modules intégrés de Node ne sont **pas** disponibles — seuls les packages npm qui fonctionnent dans un environnement navigateur sont pris en charge. Notez que le bac à sable implémente un DOM *partiel*, de sorte qu’un package peut être compilé sans erreur tout en échouant à l’exécution ; voir [Limitations actuelles](/l/fr/developers/extend/apps/layout/front-components#current-limitations).
Les deux environnements disposent de `twenty-client-sdk/core` et `twenty-client-sdk/metadata` en tant que modules pré-fournis — ils ne sont pas intégrés au bundle mais résolus à l'exécution par le serveur.
Les composants frontaux peuvent également partager des dépendances via un [bundle de dépendances partagées](/l/fr/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components). Les paquets listés dans le champ `frontComponentSharedDependencies` du fichier package.json de l'application sont construits une fois dans un fichier séparé et chargés à partir de celui-ci à l'exécution au lieu d'être intégrés à chaque composant, de sorte qu'un composant qui n'importe que des paquets partagés n'embarque quasiment aucun code de dépendance propre.
## Installation
L'application générée inclut déjà Vitest. Si vous le configurez manuellement, installez les dépendances :
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Créez un `vitest.config.ts` à la racine de votre application :
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
Créez un fichier de configuration global qui vérifie que le serveur est joignable, écrit une configuration de test pour le SDK (`~/.twenty/config.test.json`) et synchronise l’application avant l’exécution des tests :
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## APIs programmatiques du SDK
Le sous-chemin `twenty-sdk/cli` exporte des fonctions que vous pouvez appeler directement depuis le code de test :
| Fonction | Description |
| -------------- | ----------------------------------------------------------------------------------- |
| `appBuild` | Construire l'application et éventuellement créer une archive tarball |
| `appDeploy` | Téléverser une archive tarball vers le serveur |
| `appDevOnce` | Construire et synchroniser l’application une fois (identique à `yarn twenty apply`) |
| `appInstall` | Installer l'application sur l'espace de travail actif |
| `appUninstall` | Désinstaller l'application de l'espace de travail actif |
Chaque fonction retourne un objet résultat avec `success: boolean` et soit `data` soit `error`.
## Écrire un test d'intégration
Voici un exemple complet qui construit, déploie et installe l'application, puis vérifie qu'elle apparaît dans l'espace de travail :
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## Exécuter les tests
Assurez-vous que votre serveur Twenty local est en cours d'exécution, puis :
```bash filename="Terminal"
yarn test
```
Ou en mode surveillance (watch) pendant le développement :
```bash filename="Terminal"
yarn test:watch
```
## Vérification des types
Vous pouvez également exécuter une vérification des types sur votre application sans exécuter les tests :
```bash filename="Terminal"
yarn twenty dev:typecheck
```
Cela exécute `tsc --noEmit` sur le `tsconfig.json` de votre application et signale toute erreur de type. Les applications générées contiennent également un script `yarn typecheck` qui couvre aussi les fichiers de test (`tsconfig.spec.json`).
## CI avec GitHub Actions
Le générateur crée un workflow prêt à l’emploi dans `.github/workflows/ci.yml`. À chaque push sur `main` et à chaque pull request, il lance un serveur Twenty éphémère dans le runner (via l’action `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`), puis exécute `yarn lint`, `yarn typecheck`, `yarn test:unit` et `yarn test` avec `TWENTY_API_URL` / `TWENTY_API_KEY` pointant vers ce serveur. Aucun secret n’est requis, et vous pouvez fixer la version du serveur via la variable d’environnement `TWENTY_VERSION` en haut du workflow.
Voir [Publication → CI/CD automatisé](/l/fr/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) pour un guide complet des trois workflows générés (`ci.yml`, le pipeline de déploiement `cd.yml` et `publish.yml` pour la publication sur npm).

View File

@@ -0,0 +1,143 @@
---
title: Traductions
description: Publiez votre application en plusieurs langues — traduisez les libellés du manifeste et les chaînes d’interface utilisateur via un seul catalogue de locales.
icon: language
---
Les applications Twenty sont rédigées en **anglais** : les chaînes de vos sources et de votre manifeste
sont le texte source en anglais, `en` est la locale source à partir de laquelle vous traduisez, et
toute locale laissée non traduite revient à celle-ci par défaut.
Votre application comporte deux types de textes traduisibles, et tous deux transitent par le même
catalogue `locales/` :
* **Libellés du manifeste** — noms d’objets et de champs, titres de vues, éléments de menu et
autres chaînes déclarées dans les métadonnées de votre application.
* **Chaînes de front-component** — le texte d’interface utilisateur que vos composants React front affichent.
Vous marquez les chaînes traduisibles, les extrayez dans des catalogues par locale,
traduisez ces catalogues, et le build sert la bonne langue pour
l’utilisateur actuel — sans configuration supplémentaire.
## Marquer les chaînes de front-component
Importez les helpers de traduction depuis `twenty-sdk/front-component` :
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### Quand utiliser quoi
* **`Trans`** — texte statique dans JSX. Utilisez les props `message` et `values` pour
l’interpolation, comme dans `Trans message="Hi {name}" values={{ name }}`;
l’interpolation directement dans les enfants n’est pas extractible statiquement.
* **`useTranslate().t`** — chaînes dynamiques à l’intérieur d’un composant. Se réaffiche lorsque
l’utilisateur change de langue. À privilégier à l’intérieur du rendu.
* **`t(...)`** (importé directement) — traduction immédiate utilisable **partout**,
y compris dans les gestionnaires d’événements, helpers et dans la portée du module — pas seulement dans le rendu.
* **`msg(...)`** — un descripteur paresseux pour les chaînes déclarées comme données (constantes,
configuration). Résolvez-le plus tard avec `t(descriptor)`.
### Contexte
Passez `context` pour lever l’ambiguïté entre des chaînes source identiques qui se traduisent
différemment :
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
Les chaînes contextuelles apparaissent dans les catalogues de paramètres régionaux comme un groupe indexé par leur
contexte : `"\<context>": { "\<message>": "\<translation>" }`. Les libellés de manifeste sont
contextuels par construction — chacun porte le rôle de métadonnée qu’il joue
(`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …), de sorte que
le même mot peut être traduit différemment en tant que nom d’objet et en tant que nom de vue.
## Extraction et traduction
Exécutez la commande d’extraction depuis le répertoire de votre application :
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
L’extraction collecte à la fois les libellés de votre manifeste et les chaînes `t()`/`msg()`/`Trans`
provenant de votre source front-component dans `locales/\<locale>.json`, indexées par
chaîne source. Renseignez les traductions :
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
Les chaînes sans contexte sont des entrées plates ; les chaînes contextuelles — y compris chaque libellé de manifeste — sont imbriquées sous leur clé de contexte.
Les espaces réservés comme `{name}` sont substitués à l’exécution — conservez-les dans la
traduction. Toute chaîne laissée vide revient au texte source.
## Comment ça marche
`twenty dev:build` compile les catalogues et sert la bonne langue pour
l’utilisateur actuel : les libellés du manifeste sont résolus côté serveur, et les catalogues
front-component sont intégrés à chaque bundle de composant. À l’exécution, un composant lit la
locale depuis son contexte d’exécution (la langue actuelle de l’hôte) et résout
chaque chaîne à partir de son catalogue, en revenant à la source lorsqu’une traduction
manque. Changer la langue dans l’hôte réaffiche en direct les chaînes `Trans` et
`useTranslate().t`.
Les libellés du manifeste sont compilés à chaque synchronisation — `twenty dev:build`, `twenty apply`
et la surveillance continue de `twenty dev` —, donc modifier un fichier de paramètres régionaux et
le synchroniser suffit pour voir le nouveau texte. Les catalogues de composants front-end sont intégrés à
chaque bundle de composants lors de la compilation, donc modifier l'un d'eux implique toujours
de relancer `twenty dev:build` (et de redéployer).
Les enfants textuels de `Trans` peuvent s’étendre sur plusieurs lignes — les espaces sont réduits
de la même façon que JSX les réduit, donc ces deux exemples sont extraits avec la clé `Welcome back` :
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -0,0 +1,81 @@
---
title: 5. An AI agent
icon: robot
description: Laissez un agent générer des documents à partir d'un chat, à l'aide de votre outil.
---
Parce que `generate-document` est exposé comme un **outil**, un agent AI peut l'appeler.
Ajoutons un agent et une compétence pour que les utilisateurs puissent simplement dire *"générer une proposition pour
Jeffery Griffin"*.
## La compétence
Un [skill](/l/fr/developers/extend/apps/logic/skills-and-agents) est des instructions réutilisables
— des connaissances que vous attachez aux agents. Le nôtre enseigne au modèle comment utiliser
l'outil.
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## L'agent
Un [agent](/l/fr/developers/extend/apps/logic/skills-and-agents) paie une invite avec un modèle
. Définissez explicitement `responseFormat` pour éviter un avertissement de construction.
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
L'agent ne peut appeler l'outil que si son rôle le permet. Nous avons déjà défini
`canAccessAllTools: true` et `canBeAssignedToAgents: true` sur le rôle de l'application dans
[Chapitre 2](/l/fr/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## Essayez-le
Ouvrez une discussion avec **Document Assistant** et demandez-lui de rédiger un document pour une
personne dans votre CRM. Il trouve l'enregistrement, appelle `generate-document`, et rapporte
le document qu'il a créé — qui apparaît maintenant dans votre vue **Documents**
exactement comme les chemins du menu de commande et du workflow.
C'est le payoff de l'exposition de la logique en tant qu'outil : **une fonction, de nombreuses portes frontales** — Menu de commande
, HTTP, étape de workflow et maintenant langage naturel.
**Après cette étape:** l'application est complète et vraiment utile. Il est temps de
l’expédier.
<Card title="Suivant : publication →" icon="rocket" href="/fr/developers/extend/apps/tutorials/document-generator/publishing">
Ajouter des métadonnées de marketplace et publier.
</Card>

View File

@@ -0,0 +1,303 @@
---
title: 4. Construire l'interface utilisateur
icon: table-columns
description: Vues, navigation dans la barre latérale, une commande et des composants frontaux.
---
Pour le moment, les objets ne sont accessibles que dans les paramètres. Donnons à l'application une présence
réelle dans l'interface utilisateur : vues de la liste, entrées de la barre latérale,
**Générer un document** en un clic, un composant frontal de la page d'enregistrement pour **prévisualiser** un document
et un onglet d'onglet de texte natif **éditeur** pour les modèles.
## Vues et navigation
Une [vue](/l/fr/developers/extend/apps/layout/views) est une liste enregistrée d’un objet donné.
Un [élément du menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items)
place cette vue dans la barre latérale. Le serveur provisionne déjà automatiquement, pour chaque objet, la vue par défaut « Tous ... » ; les vues définies par une application sont donc des vues supplémentaires, sélectionnées.
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
Ajouter la même paire pour les modèles. Les deux affichent maintenant dans la barre latérale :
<Frame caption="Documents et gabarits dans la barre latérale, avec le document généré listé.">
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Vue des documents avec un document généré" />
</Frame>
## Un composant frontal
Un [composant frontal](/l/fr/developers/extend/apps/layout/front-components) est un composant React
bac à sable à l'intérieur de Twenty. Notre lecture de l'enregistrement sélectionné, charge les gabarits
par l'intermédiaire de `CoreApiClient`, et POSTs vers la route à partir du dernier chapitre
.
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
Style avec des variables CSS en ligne (`var(--t-color-blue)`), pas de valeurs importées de
`21ui`. Les mocks SDK que ce paquet pendant la compilation, donc les importations au niveau des modules de constantes de thème
seraient `indéfinies`. Voir le
[composant complet](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## Une commande pour l'ouvrir
Un [lien de menu de commande](/l/fr/developers/extend/apps/layout/command-menu-items) avec
`availabilityType: 'RECORD_SELECTION'` s'affiche quand une personne est sélectionnée, et
ouvre le composant dans le panneau latéral.
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## Essayer tout le flux
Ouvrez **People**, cochez une personne et appuyez sur <kbd>ΩK</kbd> / <kbd>Ctrl K</kbd>.
"Générer un document" apparaît, marqué avec votre application :
<Frame caption="La commande s'affiche lorsqu'une personne est sélectionnée.">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Menu de commande avec Générer un document" />
</Frame>
Exécutez-le — votre composant s'ouvre dans le panneau latéral. Choisissez un modèle, cliquez sur
**Générer**, et un nouveau record se trouve dans **Documents**.
<Frame caption="Le composant avant, le chargement des modèles et la génération au clic.">
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Générer le panneau latéral du document" />
</Frame>
Chaque document généré enregistre votre application comme auteur:
<Frame caption="Créé par le générateur de documents, statut généré.">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Un enregistrement de document généré" />
</Frame>
## Aperçu d'un document sur sa page de dossier
Un composant frontal n’est pas seulement destiné aux menus de commande — vous pouvez en monter un comme **onglet sur une page d’enregistrement**. Ajoutons un onglet *Aperçu* à l'enregistrement du document qui rend le corps
Markdown comme une page lisse et imprimable.
Le composant lit l'id de l'enregistrement courant depuis son contexte d'exécution, charge le document
et le rendu. Les composants frontaux s'exécutent dans un **sandbox** qui n'autorise qu'une liste
de balises HTML — l'injection HTML brute (`dangerouslySetInnerHTML`) et
`\<style>` sont bloqués — donc nous rendons le Markdown comme des éléments React avec des styles
en ligne via un petit [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
.
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
query: { id: recordId },
});
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
Montez-le avec une [mise en page de la page] (/developers/extend/apps/layout/page-layouts). Une disposition `RECORD_PAGE` ajoute des onglets à la vue d’enregistrement d’un objet ; un onglet avec un seul widget `FRONT_COMPONENT` héberge le composant en pleine largeur :
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
Ouvrez n'importe quel document — un onglet **Aperçu** le rend magnifiquement, avec des liens vers la page
et le PDF :
<Frame caption="L'onglet Aperçu affiche le document avec des styles en ligne, plus des liens rapides.">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Composant frontal de la visionneuse de documents dans un onglet de page d'enregistrement" />
</Frame>
## Modifier un modèle avec l'éditeur de texte riche
Les gabarits n'ont pas du tout besoin d'un composant personnalisé. Parce que le `body` est un champ
`RICH_TEXT`, Vingt fournissent déjà un éditeur de texte complet — le
est le même que les objets standard Note et Tâche. Nous nous contentons de le mettre en surface sur la page d'enregistrement du gabarit
.
Ajouter un onglet avec un widget `FIELD` en mode d'affichage `EDITOR`, pointant vers le champ `body`
via `fieldMetadataId`:
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
Un champ `RICH_TEXT` stocke à la fois le bloc JSON de l'éditeur et une projection Markdown
. Le pipeline de génération lit que Markdown projection, donc
placeholders, le PDF, et la page web partageable fonctionnent tous de façon inchangée —
voir
[`template-record. age-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
Les widgets `FIELD` prennent également en charge d'autres modes d'affichage et options, notamment l’énumération des enregistrements d’une relation imbriquée à deux sauts — voir la [référence des widgets de champ](/l/fr/developers/extend/apps/layout/page-layouts#field-widgets).
Maintenant, les éditeurs écrivent des modèles dans un éditeur de texte riche approprié:
<Frame caption="Onglet Modèle : l'éditeur natif de texte riche de Vingt lié au champ corps.">
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Enregistrement du modèle avec l'onglet de l'éditeur natif de texte riche" />
</Frame>
**Après cette étape:** les documents d'aperçu et les modèles sont modifiables
dans l'application. Ensuite, laissez un agent IA les générer depuis un chat.
<Card title="Suivant : un agent IA →" icon="robot" href="/fr/developers/extend/apps/tutorials/document-generator/ai-agent">
Ajoutez un agent et une compétence qui appellent votre outil.
</Card>

View File

@@ -0,0 +1,135 @@
---
title: 1. Modèle de données
icon: database
description: Modèles de documents et de modèles avec des objets, des champs et une relation.
---
Notre application a besoin de deux objets personnalisés : **modèles de documents** (quoi écrire) et
**documents** (le résultat généré). Définissons-les.
Échappez chaque fichier d'entité avec le CLI — il génère un UUID valide et le dossier
correct pour vous :
```bash filename="Terminal"
yarn twenty dev:add object
```
Ci-dessous nous montrons les fichiers finis.
<Note>
Chaque constante `*_UNIVERSAL_IDENTIFIER` vit dans
`src/constants/universal-identifiers.ts` et est importée quand elle est utilisée. Les snippets
ci-dessous omettent ces importations par souci de brièveté — conservez-les dans vos propres fichiers.
</Note>
## L'objet modèle
Un modèle a un `nom`, un `body` avec `{{placeholders}}`, et un `target` que
dit s'il est écrit pour une personne ou une entreprise. Le `body` est un champ
`RICH_TEXT`. Vingt lui donne donc un éditeur de texte complet.
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
L'option `SELECT` **values** doit être `UPPER_CASE` (`PERSON`, pas `person`), et le
`defaultValue` est enveloppé par des guillemets supplémentaires : `` `'PERSON'` ``. Le `label` est ce que les utilisateurs de
voient.
</Warning>
## L'objet du document
Le document généré stocke le contenu `content` et un `status`. Définissez
de la même manière, avec un `status` de `DRAFT` / `GENERATED`. Fichier complet :
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## Les lier avec une relation
Chaque document devrait revenir au modèle dont il provient. Les relations sont
**bidirectionnelles** — vous définissez les deux côtés, chacun dans son propre fichier de champs.
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
L'autre côté (`template-documents-relation.field.ts`) est un champ
`RelationType.ONE_TO_MANY` nommé `documents` qui pointe dans la direction opposée.
Voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle complet.
## Voyez-la en 20
Avec `yarn twenty dev` en cours d'exécution, ouvrez **Paramètres → Modèle de données**. Les deux objets
apparaissent, taggés avec votre application.
<Frame caption="Les deux objets personnalisés, détenus par l'application Générateur de documents.">
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Réglages du modèle de données montrant les modèles de documents et de documents" />
</Frame>
Créez un modèle avec lequel tester — nommez-le *proposition de vente*, définissez **Cible** à
*Personne*, et collez un corps avec quelques marqueurs :
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="Un enregistrement de gabarit. Le corps garde ses espaces réservés jusqu'à ce qu'un document soit généré.">
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Un enregistrement de modèle de proposition de vente avec le corps du placeholder" />
</Frame>
**Après cette étape :** vous avez des objets `documentTemplate` et `document`, liés par
par une relation, et un modèle à générer. Ensuite, la logique qui le remplit.
<Card title="Suivant : génération de documents →" icon="bolt" href="/fr/developers/extend/apps/tutorials/document-generator/generating-documents">
Écrit la fonction logique qui remplit le modèle.
</Card>

View File

@@ -0,0 +1,239 @@
---
title: 2. Génération des documents
icon: bolt
description: Une fonction logique, exposée comme un outil AI et une action de workflow.
---
Maintenant le cœur : une [fonction logique](/l/fr/developers/extend/apps/logic/logic-functions)
qui charge un modèle et un enregistrement, remplit les marqueurs et enregistre un nouveau document
.
Nous allons écrire la logique commerciale une fois en tant que **gestionnaire**, puis l'exposer à travers
plusieurs déclencheurs. Ce chapitre connecte deux d’entre eux — un **outil d’IA** et une
**action de flux de travail**.
## L'assistant de rendu
Gardez une logique pure dans son propre fichier donc il est facile de le tester. Cela aplanit un record
en jetons `{{dot.path}}` et les substitue.
```ts filename="src/logic-functions/utils/render-template.ts"
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
export const renderTemplate = (body: string, values: Record<string, string>) => {
const missingTokens = new Set<string>();
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
const value = values[token];
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
return value;
});
return { content, missingTokens: [...missingTokens] };
};
```
<Tip>
Comme ce fichier n'a pas d'effets secondaires, vous pouvez le couvrir avec des tests unitaires rapides
(`yarn test:unit`). Voir [Testing](/l/fr/developers/extend/apps/operations/testing).
</Tip>
## Le gestionnaire
Le gestionnaire utilise le [`CoreApiClient`](/l/fr/developers/extend/apps/logic/logic-functions)
généré pour lire et écrire les données CRM. Il charge le modèle, charge l'enregistrement cible, remplit
le corps, et crée un `document`.
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';
export const generateDocumentHandler = async (
input: { templateId: string; recordId: string },
) => {
const client = new CoreApiClient();
// Use a filtered list query, not the singular lookup: the singular query
// throws when nothing matches, which would become a 500 instead of a 404.
const { documentTemplates } = await client.query({
documentTemplates: {
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
edges: { node: { id: true, name: true, body: true, target: true } },
},
});
const documentTemplate = documentTemplates?.edges?.[0]?.node;
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
const { createDocument } = await client.mutation({
createDocument: {
__args: { data: {
name: `${documentTemplate.name} — ${record.displayName}`,
content, status: 'GENERATED', templateId: documentTemplate.id,
} },
id: true, name: true,
},
});
return { success: true, documentId: createDocument.id, content, missingTokens };
};
```
`loadRecordValues` exécute une requête différente pour une personne par rapport à une entreprise et aplatit
le résultat — voir
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).
## Exposer comme un outil et une action de workflow
Une seule `defineLogicFunction` peut contenir plusieurs déclencheurs. Ici, `toolTriggerSettings`
le rend appelable par les agents AI, et `workflowActionTriggerSettings` le transforme en une étape
dans le constructeur de workflow visuel. Les deux décrivent leur entrée avec un schéma JSON.
```ts filename="src/logic-functions/generate-document.ts"
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
name: 'generate-document',
description: 'Generate a document from a template and a CRM record.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: generateDocumentInputSchema,
},
workflowActionTriggerSettings: {
label: 'Generate Document',
icon: 'IconFileText',
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
outputSchema: [{ type: 'object', properties: {
success: { type: 'boolean' }, documentId: { type: 'string' },
} }],
},
handler: generateDocumentHandler,
});
```
Le schéma d'entrée est un schéma JSON simple décrivant `templateId` et `recordId` —
voir [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).
## Accorder l'accès
Les fonctions logiques s'exécutent en tant que rôle de l'application. Il a besoin de lire les modèles et les enregistrements
et de créer des documents, donc permettez cela dans `src/roles/default-role.ts`:
```ts
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Document Generator default role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canAccessAllTools: true,
canBeAssignedToAgents: true,
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```
`UPLOAD_FILE` permet à la fonction d'envoyer le PDF généré dans la section suivante.
Voir [Roles](/l/fr/developers/extend/apps/config/roles) pour des autorisations plus fines.
## Joindre un vrai fichier PDF
Un champ de texte rendu est utile, mais les utilisateurs veulent un vrai document. Nous allons générer un
**PDF** et le stocker dans l'enregistrement en tant que fichier téléchargeable.
Premièrement, donnez à l'objet `document` un champ `FILES` pour contenir le PDF. Les applications téléchargent
dans leurs champs **propres** de fichiers, donc ce champ permet de router le téléchargement:
```ts filename="src/objects/document.object.ts"
{
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.FILES,
name: 'file',
label: 'File',
icon: 'IconFileTypePdf',
universalSettings: { maxNumberOfValues: 1 },
}
```
Maintenant, renvoie ce PDF. Une application est un vrai projet Node, vous pouvez donc ajouter n'importe quel package npm
dont vous avez besoin et l'importer n'importe où ailleurs. Nous utilisons **[pdf-lib](https://pdf-lib.js.org/)**
pour dessiner le PDF et **[marked](https://marked.js.org/)** pour analyser le corps du Markdown
— le CLI les installe dans le runtime de la fonction pour vous :
```bash filename="Terminal"
yarn add pdf-lib marked
```
L'aide complète est
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
Il analyse le Markdown en jetons avec `marked. exer`, puis les pose avec
pdf-lib: de vraies rubriques, **gras**/*italique* exécute, puces et listes numérotées,
blockquotes et règles — un rendu A4 polyvalent et polyvalent du modèle
lui-même, plutôt qu'un mur de texte.
<Frame caption="Le PDF généré : typographie réelle et mise en forme Markdown, rendant le corps du modèle.">
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Un PDF brossé et commercialisable généré" />
</Frame>
<Note>
Les polices intégrées de pdf-lib's utilisent l'encodage WinAnsi, de sorte que les accents WesternEuropean rendent
hors de la boite; l'aide mappe les guillemets intelligents et les tirets et drops les caractères qu'elle
ne peut pas encoder. Le rendu de scripts non latins (chinois, arabe, cyrillique) impliquerait
d’intégrer une police Unicode.
</Note>
Ensuite téléchargez-le et stockez la référence sur l'enregistrement. `uploadFile` achemine les octets
vers votre champ de fichiers appartenant à l'application; le `id` retourné est ce que vous sauvegardez:
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';
const uploaded = await new MetadataApiClient().uploadFile(
Buffer.from(bytes),
fileName,
'application/pdf',
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);
await client.mutation({
updateDocument: {
__args: {
id: documentId,
data: { file: [{ fileId: uploaded.id, label: fileName }] },
},
id: true,
},
});
```
Le document généré possède maintenant un PDF téléchargeable:
<Frame caption="Le PDF généré, stocké dans le champ Fichier du document.">
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Un enregistrement de document avec un fichier PDF généré" />
</Frame>
<Note>
`uploadFile` ne cible que les champs de fichiers **app-owned** (donc les téléchargements nécessitent toujours une application
qui possède le champ, plus le paramètre `UPLOAD_FILE`). C'est pourquoi le PDF
se trouve sur le propre champ `file` de l'enregistrement — le même motif que l'application
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
utilise pour les enregistrements.
</Note>
**Après cette étape:** chaque document généré a un PDF réel, téléchargeable. Mais
rien ne peut *appeler* le générateur de l'interface utilisateur — pour cela nous avons besoin d'une route HTTP.
<Card title="Suivant : Routes HTTP →" icon="globe" href="/fr/developers/extend/apps/tutorials/document-generator/http-routes">
Servez la fonction via HTTP et rendez les documents en tant que pages Web.
</Card>

View File

@@ -0,0 +1,150 @@
---
title: 3. Routes HTTP
icon: globe
description: Déclencher la fonction sur HTTP et afficher les documents en tant que pages Web.
---
Le même gestionnaire peut également répondre aux requêtes HTTP. Nous allons ajouter deux itinéraires :
* un point de terminaison **POST** que l'interface utilisateur appelle pour générer un document, et
* un point de terminaison public **GET** qui rend un document en tant que page web imprimable.
Les deux utilisent `httpRouteTriggerSettings`. Les routes de l'application sont accessibles via le chemin `/s/` ; sur les instances locales
et auto-hébergées, ce chemin est géré par votre serveur Twenty
(par exemple, `http://localhost:2020/s/documents/generate`). Sur Twenty Cloud, `RestApiClient`
résout le chemin via le domaine des fonctions de l'espace de travail.
## Itinéraire POST — générer à la demande
Ceci réutilise `generateDocumentHandler`, donc il n'y a pas de logique à répéter — juste un adaptateur
mince qui lit le corps de la requête.
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
Le gestionnaire partagé renvoie un `status` suggéré en cas d'échec, donc la route peut
répondre avec un code approprié `4xx`/`5xx`. `isAuthRequired: true` signifie que l'appelant
doit présenter un jeton valide — le composant frontal dans le chapitre suivant passe automatiquement le jeton d'accès de l'utilisateur
.
## Obtenir la route - afficher en tant que page web
Pour retourner du HTML au lieu de JSON, enveloppez le corps dans un en-tête `Response` avec un
`Content-Type`. Cette route est publique (`isAuthRequired: false`) donc un document généré par
peut être partagé comme un lien.
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
`documentHtmlPage` rend le corps de Markdown en HTML (avec [marked](https://marked.js.org/),
assaini et le dépose dans une netteté page imprimable qui ne montre que le contenu du modèle
— la même apparence que le PDF et l'aperçu dans l'application.
[Voir l'aide](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## Essayez-le
Avec un modèle et une personne dans votre espace de travail, appelez la route (récupérez un jeton à partir de
**Paramètres → APIs & Webhooks**):
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
Ouvrez le document retourné dans votre navigateur :
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="L'itinéraire public GET rend le document en tant que page imprimable.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Une page web de document rendu" />
</Frame>
<Tip>
Vous pouvez également diffuser les logs d'une fonction en testant avec
`yarn twenty dev:function:logs`, ou l'appeler directement avec
`yarn twenty dev:function:exec`.
</Tip>
**Après cette étape :** l'application peut générer des documents via HTTP et les servir comme
pages web. Maintenant, rendons-le utilisable sans `curl`.
<Card title="Prochaine étape : construire l'interface utilisateur →" icon="table-columns" href="/fr/developers/extend/apps/tutorials/document-generator/building-the-ui">
Vues, navigation, commande et composant frontal.
</Card>

View File

@@ -0,0 +1,70 @@
---
title: "Tutoriel : Document Generator"
icon: wand-magic-sparkles
description: Créez une véritable application Twenty qui génère des documents personnalisés à partir des données de votre CRM.
---
Dans ce tutoriel, vous allez créer **Document Generator** — une application qui transforme des modèles réutilisables
en documents personnalisés en utilisant les données déjà présentes dans votre CRM.
Rédigez un modèle une seule fois avec `{{placeholders}}`, puis générez un document rempli
pour n’importe quelle personne ou entreprise en un clic — depuis le menu de commande, depuis un
agent IA ou depuis un workflow.
<Frame caption="Un modèle, généré pour une personne spécifique, ouvert comme une page imprimable.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Un document de proposition commerciale généré" />
</Frame>
## Ce que vous apprendrez
Chaque chapitre ajoute une capacité. À la fin, vous aurez utilisé la plupart du SDK.
| Chapitre | Capacité | Référence |
| -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [1. Modèle de données](/l/fr/developers/extend/apps/tutorials/document-generator/data-model) | Objets, champs et une relation | [Données](/l/fr/developers/extend/apps/data/overview) |
| [2. Générer des documents](/l/fr/developers/extend/apps/tutorials/document-generator/generating-documents) | Une fonction logique (outil IA + action de workflow) qui remplit un modèle Markdown et y joint un PDF soigné | [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) |
| [3. Routes HTTP](/l/fr/developers/extend/apps/tutorials/document-generator/http-routes) | Servir du JSON et une page HTML partageable depuis des routes | [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) |
| [4. Créer l’interface utilisateur](/l/fr/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vues, navigation, menu de commandes et composants frontaux qui prévisualisent un document et modifient un modèle | [Mise en page](/l/fr/developers/extend/apps/layout/overview) |
| [5. Un agent IA](/l/fr/developers/extend/apps/tutorials/document-generator/ai-agent) | Agent + compétence | [Compétences et agents](/l/fr/developers/extend/apps/logic/skills-and-agents) |
| [6. Publication](/l/fr/developers/extend/apps/tutorials/document-generator/publishing) | Publiez-la sur la place de marché | [Publication](/l/fr/developers/extend/apps/operations/publishing) |
## Prérequis
Vous devez avoir terminé le [démarrage rapide](/l/fr/developers/extend/apps/getting-started/quick-start) :
un serveur Twenty local en cours d’exécution sur le port `2020` et le CLI authentifié dessus.
Sinon, générez-en un et lancez-le maintenant :
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
Vous préférez lire le code finalisé ? L’application complète se trouve dans
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
Chaque extrait ci-dessous en est tiré.
</Note>
## Comment l’application s’assemble
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Un modèle avec des espaces réservés est transformé en un document soigné avec un PDF, déclenché depuis le menu de commandes, un agent IA, un workflow ou un lien partageable" />
</Frame>
Vous rédigez un **modèle** une seule fois dans un éditeur de texte enrichi, avec `{{placeholders}}`. Choisir un
modèle et un enregistrement CRM remplit les espaces réservés et stocke un
**document** soigné (avec un fichier PDF). Tout le reste — le menu de commandes, l’agent IA,
l’étape de workflow, le lien partageable — n’est qu’une façon différente de déclencher ce
même générateur.
## Gardez cette boucle en marche
Laissez `yarn twenty dev` s’exécuter dans un terminal pendant tout le tutoriel. Chaque fois que
vous ajoutez ou modifiez un fichier sous `src/`, il est resynchronisé avec votre serveur en quelques
secondes, afin que vous puissiez voir chaque capacité apparaître dans l’interface utilisateur au fur et à mesure que vous la construisez.
<Card title="Commencez à construire →" icon="database" href="/l/fr/developers/extend/apps/tutorials/document-generator/data-model">
Chapitre 1 : modéliser des documents et des modèles.
</Card>

View File

@@ -0,0 +1,137 @@
---
title: 6. Publication
icon: rocket
description: Ajoutez des métadonnées de marketplace et publiez votre application.
---
Votre application fonctionne. La dernière étape consiste à la décrire pour le marché et à la publier.
## Ajouter des métadonnées de marketplace
La [configuration de l'application](/l/fr/developers/extend/apps/config/application) porte l'identité
qui apparaît sur le marché : auteur, catégorie, logo et liens de support
. Placez un logo dans `public/` et référencez-le avec `logo`.
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logo: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/fr/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
Le rôle par défaut est déclaré avec `defineApplicationRole()` dans son propre fichier — vous
ne passez plus ici `defaultRoleUniversalIdentifier`.
</Tip>
Ajoute également le mot-clé `21app` à `package.json` pour que l'application soit découverte:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## Ajouter des captures d'écran de la galerie
Une annonce boursière se vend avec des captures d'écran. Déposez quelques PNG dans
`public/gallery/` et référencez-les avec `galleryImages` — ils sont affichés comme une
galerie sur la page de l’annonce, dans l’ordre du tableau.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
Menez avec le gain : faire la première capture d'écran le résultat fini (un document
généré), puis montrez comment il est déclenché et créé. Utilisez des captures
à haute résolution - c'est la première chose qu'un utilisateur voit.
</Tip>
Donnez le même traitement à `README.md` - c'est la première page sur npm et GitHub.
Ouvrez avec la proposition de valeur et une capture d'écran, listez les fonctionnalités du titre,
puis gardez les détails de construction sous le pli.
## Vérifiez avant d'expédier
Exécuter les mêmes portes CI :
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty plan # preview the metadata diff
```
Le plan affiche exactement ce qui changerait sur le serveur sans les appliquer —
un bon dernier contrôle de cohérence. Voir
[Testing](/l/fr/developers/extend/apps/operations/testing) et
[Synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery).
## Publier
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
`app:publish` construit et publie sur npm par défaut; `--private` télécharge une archive
vers un registre privé du serveur Vingt à la place. Pour faire surface à une application publiée
dans une instance, déclenchez une synchronisation de catalogue :
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
Tous les détails et la liste de vérification de la version :
[Publishing](/l/fr/developers/extend/apps/operations/publishing).
## Vous avez construit une application 🎉
Dans six chapitres, vous avez utilisé la plupart des surfaces du SDK :
* **Objets, champs et relations** pour modéliser les données
* Une **fonction logique** exposée comme un **outil IA**, une **action de workflow**, et des **routes HTTP**
* **Voires, navigation, une commande et un composant frontal** pour l'interface utilisateur
* Un **agent + compétence** pour la génération de langage naturel
* **Métadonnées du Marketplace** et le flux de publication
L'application terminée est à
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## Où aller ensuite
<CardGroup cols={2}>
<Card title="Référence des données" icon="database" href="/l/fr/developers/extend/apps/data/overview">
Chaque type de champ, chaque relation et chaque option d'index.
</Card>
<Card title="Référence logique" icon="bolt" href="/l/fr/developers/extend/apps/logic/overview">
Cron et les déclencheurs d'événements de base de données, le stockage de valeurs clés, les connexions OAuth.
</Card>
<Card title="Référence de mise en page" icon="table-columns" href="/l/fr/developers/extend/apps/layout/overview">
Mise en page des pages, widgets du tableau de bord et plus de surfaces de l'interface utilisateur.
</Card>
<Card title="Opérations" icon="rocket" href="/l/fr/developers/extend/apps/operations/overview">
CLI, tests, télécommandes et CI.
</Card>
</CardGroup>

View File

@@ -0,0 +1,147 @@
---
title: API
description: Interrogez et modifiez vos données CRM par programmation à l'aide de REST ou de GraphQL.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty a été conçu pour être convivial pour les développeurs, offrant des API puissantes qui s'adaptent à votre modèle de données personnalisé. Nous fournissons quatre types d'API distincts pour répondre à différents besoins d'intégration.
## Approche axée sur les développeurs
Twenty génère des API spécifiquement pour votre modèle de données :
* **Pas besoin d'ID longs** : Utilisez directement les noms de vos objets et champs dans les points de terminaison
* **Objets standard et personnalisés traités à égalité** : Vos objets personnalisés bénéficient du même traitement API que ceux intégrés
* **Points de terminaison dédiés** : Chaque objet et champ a son propre point de terminaison API
* **Documentation personnalisée** : Générée spécifiquement pour le modèle de données de votre espace de travail
<Note>
Votre documentation API personnalisée est disponible sous **Paramètres → API & Webhooks** après la création d'une clé API. Comme Twenty génère des API qui correspondent à votre modèle de données personnalisé, la documentation est propre à votre espace de travail.
</Note>
## Les deux types d'API
### API principale
Accessible sur `/rest/` ou `/graphql/`
Travaillez avec vos **enregistrements** réels (les données) :
* Créer, lire, mettre à jour, supprimer People, Companies, Opportunities, etc.
* Interroger et filtrer les données
* Gérer les relations entre les enregistrements
### API de métadonnées
Accessible sur `/rest/metadata/` ou `/metadata/`
Gérez votre **espace de travail et votre modèle de données** :
* Créer, modifier ou supprimer des objets et des champs
* Configurer les paramètres de l'espace de travail
* Définir les relations entre les objets
## REST vs GraphQL
Les API Core et Metadata sont disponibles aux formats REST et GraphQL :
| Format | Opérations disponibles |
| ----------- | ----------------------------------------------------------------------- |
| **REST** | CRUD, opérations par lot, upserts |
| **GraphQL** | Identique + **upserts par lot**, requêtes de relations en un seul appel |
Choisissez selon vos besoins — les deux formats accèdent aux mêmes données.
## Points d'accès API
| Environnement | URL de base |
| ---------------- | ------------------------- |
| **Cloud** | `https://api.twenty.com/` |
| **Auto-hébergé** | `https://{your-domain}/` |
## Authentification
Chaque requête API nécessite une clé API dans l'en-tête :
```
Authorization: Bearer YOUR_API_KEY
```
### Créer une clé API
1. Allez dans **Paramètres → API & Webhooks**
2. Cliquez sur **+ Créer une clé**
3. Configurer:
* **Nom** : Nom descriptif pour la clé
* **Date d'expiration** : Date à laquelle la clé expire
4. Cliquez sur **Enregistrer**
5. **Copiez immédiatement** — la clé n'est affichée qu'une seule fois
<VimeoEmbed videoId="928786722" title="Création d'une clé API" />
<Warning>
Votre clé API donne accès à des données sensibles. Ne la partagez pas avec des services non fiables. Si elle est compromise, désactivez-la immédiatement et générez-en une nouvelle.
</Warning>
### Attribuer un rôle à une clé API
Pour une meilleure sécurité, attribuez un rôle spécifique pour limiter l'accès :
1. Allez dans **Paramètres → Membres → Rôles**
2. Cliquez sur le rôle à attribuer
3. Ouvrez l'onglet **Attribution**
4. Sous **Clés API**, cliquez sur **+ Attribuer à une clé API**
5. Sélectionnez la clé API
La clé héritera des autorisations de ce rôle. Voir [Autorisations](/l/fr/user-guide/permissions-access/capabilities/permissions) pour plus de détails.
### Gérer les clés API
**Régénérer** : Paramètres → API & Webhooks → Cliquez sur la clé → **Régénérer**
**Supprimer** : Paramètres → API & Webhooks → Cliquez sur la clé → **Supprimer**
## Bac à sable API
Testez vos API directement dans le navigateur avec notre bac à sable intégré — disponible pour **REST** et **GraphQL**.
### Accéder au bac à sable
1. Allez dans **Paramètres → API & Webhooks**
2. Créer une clé API (obligatoire)
3. Cliquez sur **REST API** ou **GraphQL API** pour ouvrir le bac à sable
### Ce que vous obtenez
* **Documentation interactive** : Générée pour votre modèle de données spécifique
* **Tests en direct** : Exécutez de véritables appels API sur votre espace de travail
* **Explorateur de schéma** : Parcourez les objets, champs et relations disponibles
* **Générateur de requêtes** : Construisez des requêtes avec l'autocomplétion
Le bac à sable reflète vos objets et champs personnalisés, de sorte que la documentation est toujours précise pour votre espace de travail.
## Opérations par Lot
REST et GraphQL prennent en charge les opérations par lot :
* **Taille du lot** : Jusqu'à 60 enregistrements par requête
* **Opérations** : Créer, mettre à jour, supprimer plusieurs enregistrements
**Fonctionnalités spécifiques à GraphQL :**
* **Upsert par lot** : Créer ou mettre à jour en un seul appel
* Utilisez des noms d'objets au pluriel (par exemple, `CreateCompanies` au lieu de `CreateCompany`)
## Limites de débit
Les requêtes API sont limitées pour garantir la stabilité de la plateforme :
| Limite | Valeur |
| ----------------- | ---------------------------- |
| **Requêtes** | 100 appels par minute |
| **Taille du lot** | 60 enregistrements par appel |
<Tip>
Utilisez les opérations par lot pour maximiser le débit — traitez jusqu'à 60 enregistrements dans un seul appel API au lieu d'effectuer des requêtes individuelles.
</Tip>

View File

@@ -0,0 +1,112 @@
---
title: Webhooks
description: Recevez des notifications en temps réel lorsque des événements se produisent dans votre CRM.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Les webhooks envoient des données à vos systèmes en temps réel lorsque des événements se produisent dans Twenty — aucune interrogation n'est nécessaire. Utilisez-les pour maintenir les systèmes externes synchronisés, déclencher des automatisations ou envoyer des alertes.
## Créer un Webhook
1. Allez dans **Paramètres → API & Webhooks → Webhooks**
2. Cliquez sur **+ Créer un webhook**
3. Saisissez votre URL de webhook (doit être publiquement accessible)
4. Cliquez sur **Enregistrer**
Le webhook s'active immédiatement et commence à envoyer des notifications.
<VimeoEmbed videoId="928786708" title="Création d'un webhook" />
### Gérer les Webhooks
**Modifier**: Cliquez sur le webhook → Mettre à jour l'URL → **Enregistrer**
**Supprimer**: Cliquez sur le webhook → **Supprimer** → Confirmer
## Événements
Twenty envoie des webhooks pour ces types d'événements :
| Événement | Exemple |
| ----------------------------- | ---------------------------------------------------------- |
| **Enregistrement créé** | `person.created`, `company.created`, `note.created` |
| **Enregistrement mis à jour** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Enregistrement supprimé** | `person.deleted`, `company.deleted` |
Tous les types d'événements sont envoyés à votre URL de webhook. Le filtrage des événements pourrait être ajouté dans de prochaines versions.
## Format de la charge utile
Chaque webhook envoie une requête HTTP POST avec un corps JSON :
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| Champ | Description |
| ------------ | ----------------------------------------------------------- |
| `événement` | Ce qui s'est passé (par exemple, `person.created`) |
| `données` | L'enregistrement complet qui a été créé/mis à jour/supprimé |
| `horodatage` | Quand l'événement s'est produit (UTC) |
<Note>
Répondez avec un **statut HTTP 2xx** (200-299) pour accuser réception. Les réponses non 2xx sont consignées comme des échecs de livraison.
</Note>
## Validation des Webhooks
Twenty signe chaque requête de webhook pour des raisons de sécurité. Validez les signatures pour garantir l'authenticité des requêtes.
### En-têtes
| En-tête | Description |
| ---------------------------- | ------------------------ |
| `X-Twenty-Webhook-Signature` | Signature HMAC SHA256 |
| `X-Twenty-Webhook-Timestamp` | Horodatage de la requête |
### Étapes de validation
1. Récupérez l'horodatage depuis `X-Twenty-Webhook-Timestamp`
2. Créez la chaîne : `{timestamp}:{JSON payload}`
3. Calculez le HMAC SHA256 en utilisant votre secret de webhook
4. Comparez avec `X-Twenty-Webhook-Signature`
### Exemple (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const isValid = expectedSignature === req.headers["x-twenty-webhook-signature"];
```
## Webhooks vs Workflows
| Méthode | Direction | Cas d'utilisation |
| -------------------------------------- | --------- | ------------------------------------------------------------------------------------- |
| **Webhooks** | SORTIE | Notifier automatiquement les systèmes externes de toute modification d'enregistrement |
| **Workflow + requête HTTP** | SORTIE | Envoyer des données avec une logique personnalisée (filtres, transformations) |
| **Déclencheur de webhook de workflow** | ENTRÉE | Recevoir des données dans Twenty depuis des systèmes externes |
Pour recevoir des données externes, voir [Configurer un déclencheur de webhook](/l/fr/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).

View File

@@ -0,0 +1,32 @@
---
title: Étendre
description: Étendez les fonctionnalités de Twenty avec des API, des webhooks et des applications personnalisées.
---
<Frame>
<img src="/images/user-guide/integrations/plug.png" alt="IA" />
</Frame>
## Vue d'ensemble
Twenty est conçu pour être extensible. Utilisez nos API, nos webhooks et notre framework d'applications pour vous intégrer à vos outils existants et créer des fonctionnalités personnalisées.
## Ce que vous pouvez faire
* **API** : Interrogez et modifiez vos données CRM par programmation à l'aide de REST ou de GraphQL
* **Webhooks** : Recevez des notifications en temps réel lorsque des événements se produisent dans Twenty
* **Applications** : Créez des applications personnalisées qui étendent les capacités de Twenty
## Prise en main
<CardGroup cols={2}>
<Card title="API" icon="code" href="/l/fr/developers/extend/api">
Connectez-vous à Twenty par programmation
</Card>
<Card title="Webhooks" icon="bell" href="/l/fr/developers/extend/webhooks">
Recevez des notifications d'événements en temps réel
</Card>
<Card title="Applications" icon="puzzle-piece" href="/l/fr/developers/extend/apps/getting-started/quick-start">
Créez des personnalisations sous forme de code
</Card>
</CardGroup>

View File

@@ -0,0 +1,189 @@
---
title: OAuth
icon: key
description: Flux de code d’autorisation avec PKCE et identifiants client pour l’accès serveur à serveur.
---
Twenty implémente OAuth 2.0 avec code d’autorisation + PKCE pour les applications destinées aux utilisateurs et des identifiants client pour l’accès serveur à serveur. Les clients sont enregistrés dynamiquement via [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — aucune configuration manuelle dans un tableau de bord.
## Quand utiliser OAuth
| Scénario | Méthode d’authentification |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Scripts internes, automatisation | [Clé API](/l/fr/developers/extend/api#authentication) |
| Application externe agissant au nom d’un utilisateur | **OAuth — Code d’autorisation** |
| Serveur à serveur, aucun contexte utilisateur | **OAuth — Identifiants client** |
| Application Twenty avec des extensions d’interface utilisateur | [Applications](/l/fr/developers/extend/apps/getting-started/quick-start) (OAuth est géré automatiquement) |
## Enregistrer un client
Twenty prend en charge **l’enregistrement dynamique de clients** conformément à [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). Aucune configuration manuelle requise — enregistrez le client par programmation :
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**Réponse:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
Stockez le `client_secret` en toute sécurité — il ne peut pas être récupéré ultérieurement.
</Warning>
## Périmètres
| Périmètre | Accès |
| --------- | ------------------------------------------------------------ |
| `api` | Accès complet en lecture/écriture aux API Core et Metadata |
| `profil` | Lire les informations de profil de l’utilisateur authentifié |
Demandez les périmètres sous forme de chaîne séparée par des espaces : `scope=api profile`
## Flux de code d’autorisation
Utilisez ce flux lorsque votre application agit au nom d’un utilisateur Twenty.
### 1. Rediriger l’utilisateur vers l’écran d’autorisation
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| Paramètre | Obligatoire | Description |
| ----------------------- | ----------- | ---------------------------------------------------------------- |
| `client_id` | Oui | Votre identifiant client enregistré |
| `response_type` | Oui | Doit être `code` |
| `redirect_uri` | Oui | Doit correspondre à une URI de redirection enregistrée |
| `scope` | Non | Périmètres séparés par des espaces (par défaut : `api`) |
| `état` | Recommandé | Chaîne aléatoire pour empêcher les attaques CSRF |
| `code_challenge` | Recommandé | Défi PKCE (hachage SHA-256 du vérificateur, encodé en base64url) |
| `code_challenge_method` | Recommandé | Doit être `S256` lors de l’utilisation de PKCE |
L’utilisateur voit un écran de consentement et approuve ou refuse l’accès.
### 2. Gérer la redirection
Après l’autorisation, Twenty redirige vers votre `redirect_uri` :
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
Vérifiez que `state` correspond à ce que vous avez envoyé.
### 3. Échanger le code contre des jetons
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**Réponse:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. Utiliser le jeton d’accès
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. Actualiser à l’expiration
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## Flux des identifiants client
Pour les intégrations serveur à serveur sans interaction utilisateur :
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
Le jeton renvoyé dispose d’un accès au niveau de l’espace de travail, non lié à un utilisateur spécifique.
## Découverte du serveur
Twenty publie sa configuration OAuth à un point de découverte standard :
```
GET /.well-known/oauth-authorization-server
```
Cela renvoie tous les points de terminaison, les types d’octroi pris en charge, les périmètres et les capacités — utile pour créer des clients OAuth génériques.
## Résumé des points de terminaison API
| Point de terminaison | Objectif |
| ----------------------------------------- | ------------------------------------- |
| `/.well-known/oauth-authorization-server` | Découverte des métadonnées du serveur |
| `/oauth/register` | Enregistrement dynamique de clients |
| `/oauth/authorize` | Autorisation de l’utilisateur |
| `/oauth/token` | Échange et actualisation de jetons |
| Environnement | URL de base |
| ---------------- | ------------------------ |
| **Cloud** | `https://api.twenty.com` |
| **Auto-hébergé** | `https://{your-domain}` |
## OAuth vs clés API
| | Clés API | OAuth |
| ------------------------ | ---------------------------------------- | ------------------------------------------------------ |
| **Installation** | Générer dans Paramètres | Enregistrer un client, implémenter le flux |
| **Contexte utilisateur** | Aucun (au niveau de l’espace de travail) | Autorisations d’un utilisateur spécifique |
| **Idéal pour** | Scripts, outils internes | Applications externes, intégrations multi-utilisateurs |
| **Rotation des jetons** | Manuel | Automatique via des jetons d’actualisation |
| **Accès par périmètre** | Accès complet à l’API | Granulaire via des périmètres |

View File

@@ -0,0 +1,117 @@
---
title: Webhooks
icon: satellite-dish
description: Recevez une notification lorsque les enregistrements changent — HTTP POST vers votre point de terminaison à chaque création, mise à jour ou suppression.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty envoie une requête HTTP POST vers votre URL chaque fois qu'un enregistrement est créé, mis à jour ou supprimé. Tous les types d'objets sont pris en charge, y compris les objets personnalisés.
## Créer un Webhook
1. Allez dans **Paramètres → API & Webhooks → Webhooks**
2. Cliquez sur **+ Créer un webhook**
3. Saisissez votre URL de webhook (doit être publiquement accessible)
4. Cliquez sur **Enregistrer**
Le webhook s'active immédiatement et commence à envoyer des notifications.
<VimeoEmbed videoId="928786708" title="Création d'un webhook" />
### Gérer les Webhooks
**Modifier**: Cliquez sur le webhook → Mettre à jour l'URL → **Enregistrer**
**Supprimer**: Cliquez sur le webhook → **Supprimer** → Confirmer
## Événements
Twenty envoie des webhooks pour ces types d'événements :
| Événement | Exemple |
| ----------------------------- | ---------------------------------------------------------- |
| **Enregistrement créé** | `person.created`, `company.created`, `note.created` |
| **Enregistrement mis à jour** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Enregistrement supprimé** | `person.deleted`, `company.deleted` |
Tous les types d'événements sont envoyés à votre URL de webhook. Le filtrage des événements pourrait être ajouté dans de prochaines versions.
## Format de la charge utile
Chaque webhook envoie une requête HTTP POST avec un corps JSON :
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| Champ | Description |
| ----------- | ----------------------------------------------------------- |
| `event` | Ce qui s'est passé (par exemple, `person.created`) |
| `data` | L'enregistrement complet qui a été créé/mis à jour/supprimé |
| `timestamp` | Quand l'événement s'est produit (UTC) |
<Note>
Répondez avec un **statut HTTP 2xx** (200-299) pour accuser réception. Les réponses non 2xx sont consignées comme des échecs de livraison.
</Note>
## Validation des Webhooks
Twenty signe chaque requête de webhook pour des raisons de sécurité. Validez les signatures pour garantir l'authenticité des requêtes.
### En-têtes
| En-tête | Description |
| ---------------------------- | ------------------------ |
| `X-Twenty-Webhook-Signature` | Signature HMAC SHA256 |
| `X-Twenty-Webhook-Timestamp` | Horodatage de la requête |
### Étapes de validation
1. Récupérez l'horodatage depuis `X-Twenty-Webhook-Timestamp`
2. Créez la chaîne : `{timestamp}:{JSON payload}`
3. Calculez le HMAC SHA256 en utilisant votre secret de webhook
4. Comparez avec `X-Twenty-Webhook-Signature`
### Exemple (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## Webhooks vs Workflows
| Méthode | Direction | Cas d'utilisation |
| -------------------------------------- | --------- | ------------------------------------------------------------------------------------- |
| **Webhooks** | SORTIE | Notifier automatiquement les systèmes externes de toute modification d'enregistrement |
| **Workflow + requête HTTP** | SORTIE | Envoyer des données avec une logique personnalisée (filtres, transformations) |
| **Déclencheur de webhook de workflow** | ENTRÉE | Recevoir des données dans Twenty depuis des systèmes externes |
Pour recevoir des données externes, voir [Configurer un déclencheur de webhook](/l/fr/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).

View File

@@ -0,0 +1,28 @@
---
title: Développeurs
description: Créez des applications, utilisez l'API, auto-hébergez ou contribuez à la base de code.
---
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={3}>
<Card href="/l/fr/developers/extend/apps/getting-started/quick-start" img="/images/user-guide/halftone/dev-apps.png">
<CardTitle>Applications</CardTitle>
Étendez Twenty avec des objets personnalisés, de la logique côté serveur, des composants d'interface utilisateur et des agents IA — le tout sous forme de packages TypeScript.
</Card>
<Card href="/l/fr/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
<CardTitle>API</CardTitle>
API REST et GraphQL, webhooks et OAuth.
</Card>
<Card href="/l/fr/developers/self-host/capabilities/docker-compose" img="/images/user-guide/halftone/dev-self-host.png">
<CardTitle>Auto-héberger</CardTitle>
Exécutez Twenty sur votre propre infrastructure.
</Card>
<Card href="/l/fr/developers/contribute/capabilities/local-setup" img="/images/user-guide/halftone/dev-contribute.png">
<CardTitle>Contribuer</CardTitle>
Configurez le monorepo en local et soumettez des PR.
</Card>
</CardGroup>

View File

@@ -0,0 +1,262 @@
---
title: Docker Compose
icon: docker
---
<Warning>
Les conteneurs Docker sont destinés à l'hébergement en production ou à l'auto-hébergement. Pour contribuer, veuillez consulter la [configuration locale](/l/fr/developers/contribute/capabilities/local-setup).
</Warning>
## Vue d'ensemble
Ce guide fournit des instructions pas-à-pas pour installer et configurer l'application Twenty à l'aide de Docker Compose. L'objectif est de simplifier le processus et d'éviter les erreurs courantes qui pourraient compromettre votre configuration.
**Important :** Modifiez uniquement les paramètres explicitement mentionnés dans ce guide. Modifier d'autres configurations peut entraîner des problèmes.
Consultez [Configurer les variables d'environnement](/l/fr/developers/self-host/capabilities/setup) pour la configuration avancée. Toutes les variables d'environnement doivent être déclarées dans le fichier `docker-compose.yml` au niveau du serveur et/ou du worker, selon la variable.
## Exigences du Système
* RAM : Assurez-vous que votre environnement dispose d'au moins 2 Go de RAM. Une mémoire insuffisante peut provoquer des plantages des processus.
* Docker & Docker Compose : Assurez-vous que les deux sont installés et à jour.
## Option 1 : Script en une seule ligne
Installez la dernière version stable de Twenty avec une seule commande :
```bash
bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
```
Pour installer une version ou une branche spécifique :
```bash
VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
```
* Remplacez x.y.z par le numéro de version désiré.
* Remplacez nom-branche par le nom de la branche que vous souhaitez installer.
## Option 2 : Étapes manuelles
Suivez ces étapes pour une installation manuelle.
### Étape 1 : Configurez le Fichier Environnement
1. **Créez le Fichier .env**
Copiez le fichier d'exemple d'environnement vers un nouveau fichier .env dans votre répertoire de travail :
```bash
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
```
2. **Générer une clé de chiffrement**
Exécutez la commande suivante pour générer une chaîne aléatoire unique :
```bash
openssl rand -base64 32
```
**Important :** Gardez cette valeur secrète / ne la partagez pas. La perte de `ENCRYPTION_KEY` signifie la perte d’accès à chaque secret stocké dans la base de données (jetons OAuth, variables d’application, secrets TOTP, etc.).
3. **Mettez à jour le `.env`**
Remplacez la valeur de l'espace réservé dans votre fichier .env par le jeton généré :
```ini
ENCRYPTION_KEY=random_string
```
Voir le [guide de rotation des clés](/l/fr/developers/self-host/capabilities/key-rotation) pour obtenir des instructions sur la rotation de la clé sans interruption de service.
4. **Définissez le Mot de Passe de Postgres**
Mettez à jour la valeur `PG_DATABASE_PASSWORD` dans le fichier .env avec un mot de passe fort sans caractères spéciaux.
```ini
PG_DATABASE_PASSWORD=my_strong_password
```
### Étape 2 : Obtenez le Fichier Docker Compose
Téléchargez le fichier `docker-compose.yml` dans votre répertoire de travail :
```bash
curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/docker-compose.yml
```
### Étape 3 : Lancer l'Application
Démarrez les conteneurs Docker :
```bash
docker compose up -d
```
### Étape 4 : Accéder à l'Application
Si vous hébergez twentyCRM sur votre ordinateur, ouvrez votre navigateur et allez à [http://localhost:3000](http://localhost:3000).
Si vous l'hébergez sur un serveur, vérifiez que le serveur fonctionne et que tout est en ordre avec
```bash
curl http://localhost:3000
```
## Configuration
### Exposer Twenty à un Accès Externe
Par défaut, Twenty fonctionne sur `localhost` au port `3000`. L’interface utilisateur résout l’API à partir de l’origine propre du navigateur, elle se charge donc à partir de n’importe quel nom d’hôte qui atteint le conteneur — mais définissez tout de même `SERVER_URL` sur votre URL publique : le serveur l’utilise pour les liens générés (e‑mails, callbacks OAuth) et pour détecter le HTTPS pour les cookies sécurisés.
#### Comprendre `SERVER_URL`
* **Protocole :** Utilisez `http` ou `https` selon votre configuration.
* Utilisez `http` si vous n'avez pas configuré SSL.
* Utilisez `https` si vous avez configuré SSL.
* **Domaine/IP :** Il s'agit du nom de domaine ou de l'adresse IP où votre application est accessible.
* **Port :** Incluez le numéro de port si vous n'utilisez pas les ports par défaut (`80` pour `http`, `443` pour `https`).
### Exigences SSL
Le SSL (HTTPS) est requis pour que certaines fonctionnalités des navigateurs fonctionnent correctement. Bien que ces fonctionnalités puissent fonctionner lors du développement local (car les navigateurs traitent localhost différemment), une configuration SSL appropriée est nécessaire lors de l'hébergement de Twenty sur un domaine régulier.
Par exemple, l'API du presse-papiers pourrait nécessiter un contexte sécurisé - certaines fonctionnalités comme les boutons de copie dans toute l'application pourraient ne pas fonctionner sans HTTPS activé.
Nous vous recommandons vivement de mettre en place Twenty derrière un proxy inverse avec terminaison SSL pour une sécurité et une fonctionnalité optimales.
#### Configurer `SERVER_URL`
1. **Déterminez Votre URL d'Accès**
* **Sans Proxy Inverse (Accès Direct):**
Si vous accédez directement à l'application sans un proxy inverse :
```ini
SERVER_URL=http://your-domain-or-ip:3000
```
* **Avec Proxy Inverse (Ports Standards):**
Si vous utilisez un proxy inverse comme Nginx ou Traefik et que vous avez SSL configuré :
```ini
SERVER_URL=https://your-domain-or-ip
```
* **Avec Proxy Inverse (Ports Personnalisés):**
Si vous utilisez des ports non standards :
```ini
SERVER_URL=https://your-domain-or-ip:custom-port
```
2. **Mettez à jour le Fichier `.env`**
Ouvrez votre fichier `.env` et mettez à jour le `SERVER_URL`:
```ini
SERVER_URL=http(s)://your-domain-or-ip:your-port
```
**Exemples :**
* Accès direct sans SSL :
```ini
SERVER_URL=http://123.45.67.89:3000
```
* Accès via domaine avec SSL :
```ini
SERVER_URL=https://mytwentyapp.com
```
3. **Redémarrez l'Application**
Pour que les changements prennent effet, redémarrez les conteneurs Docker :
```bash
docker compose down
docker compose up -d
```
#### Considérations
* **Configuration du Proxy Inverse :**
Assurez-vous que votre proxy inverse redirige les requêtes vers le port interne correct (`3000` par défaut). Configurez la terminaison SSL et tous les en-têtes nécessaires.
* **Paramètres de Pare-feu :**
Ouvrez les ports nécessaires dans votre pare-feu pour permettre l'accès externe.
* **Cohérence :**
Le `SERVER_URL` doit correspondre à la façon dont les utilisateurs accèdent à votre application dans leurs navigateurs.
#### Persistance
* **Volumes de Données :**
La configuration de Docker Compose utilise des volumes pour persister les données pour la base de données et le stockage du serveur.
* **Environnements sans État :**
Si vous déployez dans un environnement sans état (par exemple, certains services cloud), configurez un stockage externe pour persister les données.
## Sauvegarde et restauration
Des sauvegardes régulières protègent les données de votre CRM contre la perte.
### Créer une sauvegarde de la base de données
```bash
docker exec twenty-postgres pg_dump -U postgres twenty > backup_$(date +%Y%m%d).sql
```
### Automatiser les sauvegardes quotidiennes
Ajoutez à votre crontab (`crontab -e`) :
```bash
0 2 * * * docker exec twenty-postgres pg_dump -U postgres twenty > /backups/twenty_$(date +\%Y\%m\%d).sql
```
### Restaurer à partir d'une sauvegarde
1. Arrêtez l'application :
```bash
docker compose stop server worker
```
2. Restaurez la base de données :
```bash
docker exec -i twenty-postgres psql -U postgres twenty < backup_20240115.sql
```
3. Redémarrez les services :
```bash
docker compose up -d
```
### Bonnes pratiques de sauvegarde
* **Testez les restaurations régulièrement** — vérifiez que les sauvegardes fonctionnent réellement
* **Stockez les sauvegardes hors site** — utilisez un stockage cloud (S3, GCS, etc.)
* **Chiffrez les données sensibles** — protégez les sauvegardes à l'aide du chiffrement
* **Conservez plusieurs copies** — gardez des sauvegardes quotidiennes, hebdomadaires et mensuelles
## Résolution des problèmes
Si vous rencontrez un problème, consultez [Dépannage](/l/fr/developers/self-host/capabilities/troubleshooting) pour des solutions.
## Hébergement infogéré
<Tip>
Vous préférez ne pas héberger et maintenir Twenty vous-même ? [Trouvez un partenaire d’hébergement Twenty certifié](https://twenty.com/partners/list?categories=HOSTING\&ref=docs-docker-compose) pour déployer, héberger et gérer votre instance de bout en bout. *(Ou contactez notre équipe : [contact@twenty.com](mailto:contact@twenty.com).)*
</Tip>

View File

@@ -0,0 +1,60 @@
---
title: Rotation des clés
icon: rotate
---
Twenty possède deux familles de clés indépendantes :
* **Clés de signature JWT** — paires de clés asymétriques ES256 (avec balise `kid`) stockées dans `core."signingKey"`, utilisées pour signer et vérifier les jetons d’accès / de rafraîchissement.
* **Clé de chiffrement au repos** — `ENCRYPTION_KEY`, utilisée pour chiffrer les jetons OAuth, les variables d’application, les clés privées de clés de signature, les valeurs de configuration sensibles et les secrets TOTP à l’intérieur d’une enveloppe `enc:v2:`.
`APP_SECRET` est un secret hérité conservé pour la rétrocompatibilité : lorsque `ENCRYPTION_KEY` n’est pas défini, il agit comme mécanisme de secours pour le chiffrement au repos / le cookie de session, et il continue de vérifier les jetons d’accès HS256 préexistants. Il sera déprécié.
## Clés de signature JWT
Chaque clé contient une `publicKey` (conservée indéfiniment afin de pouvoir vérifier les jetons émis précédemment), une `privateKey` chiffrée (utilisée uniquement tant que la clé est actuelle), un indicateur `isCurrent` (exactement une ligne à la fois) et un champ `revokedAt` facultatif.
### Faire pivoter la clé actuelle
Définissez `SIGNING_KEY_ROTATION_DAYS` pour l’activer : une tâche cron quotidienne émet alors une nouvelle clé actuelle dès que l’ancienne dépasse ce seuil. Les clés précédentes ne sont *pas* révoquées, donc les jetons signés avec celles-ci restent vérifiables. Laissez la variable non définie pour désactiver la rotation automatique.
<Note>La rotation automatique est disponible à partir de la v2.6+.</Note>
### Révoquer une clé (fuite / urgence uniquement)
**Settings → Admin Panel → Signing keys → Revoke** sur une ligne non actuelle. Efface le matériel privé chiffré, définit `revokedAt` et rejette chaque jeton existant signé avec ce `kid`.
## Faire pivoter `ENCRYPTION_KEY`
<Note>La commande `secret-encryption:rotate` décrite ci-dessous est fournie à partir de la v2.6+.</Note>
Chaque valeur chiffrée est encapsulée sous la forme `enc:v2:\<keyId>:\<payload>`, où `\<keyId>` est un préfixe hexadécimal sur 8 caractères dérivé de la clé brute. La rotation est en ligne et reprenable.
1. **Générez une nouvelle clé** : `openssl rand -base64 32`.
2. **Configurez les deux clés côte à côte** dans `.env`, puis redémarrez :
```ini
ENCRYPTION_KEY=NEW_VALUE
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
```
Les nouvelles écritures utilisent la nouvelle clé, les lignes existantes se déchiffrent toujours via la clé de secours.
3. **Rechiffrez les lignes existantes** :
```bash
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
```
La commande parcourt six sites (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Un filtre SQL ignore les lignes déjà sur le nouveau `\<keyId>`, de sorte que la commande est idempotente : interrompez-la et relancez-la si nécessaire. Quitte avec un code de sortie non nul si une ligne échoue — relancez pour réessayer.
| Option | Description |
| ---------------------------------------- | ----------------------------------------------------------------- |
| `-s, --site \<site>` | Limiter à un seul site. |
| `-b, --batch-size \<n>` | Lignes par lot (valeur par défaut `200`, maximum `5000`). |
| `-d, --dry-run` | Déchiffrer + rechiffrer en mémoire, ignorer la commande `UPDATE`. |
4. **Supprimez la clé de secours** une fois que `--dry-run` n’affiche plus aucune ligne restante : supprimez `FALLBACK_ENCRYPTION_KEY` et redémarrez.
## Prise en charge héritée de `APP_SECRET`
Les instances plus anciennes qui n’ont jamais défini `ENCRYPTION_KEY` utilisent `APP_SECRET` comme clé de chiffrement au repos (et comme secret de cookie de session, dérivé de celle-ci). Ce chemin est conservé pour la rétrocompatibilité mais est **déprécié** — définissez une `ENCRYPTION_KEY` dédiée et suivez la procédure de rotation ci-dessus pour migrer. `APP_SECRET` reste lui-même utilisé pour vérifier les anciens jetons d’accès HS256.

View File

@@ -0,0 +1,413 @@
---
title: Installation
icon: gear
---
# Gestion de la configuration
<Warning>
**Première installation ?** Suivez le [Guide d’installation de Docker Compose](/l/fr/developers/self-host/capabilities/docker-compose) pour faire fonctionner Twenty, puis revenez ici pour la configuration.
</Warning>
Twenty offre **deux modes de configuration** pour répondre à différents besoins de déploiement :
**Accès au panneau d'administration :** Seuls les utilisateurs avec des privilèges d'administrateur (`canAccessFullAdminPanel: true`) peuvent accéder à l'interface de configuration.
## 1. Configuration du panneau d'administration (par défaut)
```bash
IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
```
**La plupart des configurations se fait via l'interface utilisateur** après l'installation :
1. Accédez à votre instance Twenty (généralement `http://localhost:3000`)
2. Allez dans **Paramètres / Panneau d’administration / Variables de configuration**
3. Configurez les intégrations, les e-mails, le stockage et plus encore
4. Les modifications prennent effet immédiatement (dans les 15 secondes pour les déploiements multi-conteneurs)
<Warning>
**Déploiements multi-conteneurs :** En utilisant la configuration de la base de données (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), les conteneurs serveur et travailleur lisent à partir de la même base de données. Les modifications du panneau d'administration affectent les deux automatiquement, éliminant le besoin de dupliquer les variables d'environnement entre les conteneurs (sauf pour les variables d'infrastructure).
</Warning>
**Ce que vous pouvez configurer via le panneau d'administration :**
* **Authentification** - Google/Microsoft OAuth, paramètres de mot de passe
* **E-mail** - Paramètres SMTP, modèles, vérification
* **Stockage** - Configuration S3, chemins de stockage local
* **Intégrations** - Gmail, Google Agenda, services Microsoft
* **Workflow & Limitation de débit** - Limites d'exécution, limitation de l'API
* **Et bien plus encore...**
![Variables de configuration du panneau d'administration](/images/user-guide/setup/admin-panel-config-variables.png)
<Warning>
Chaque variable est documentée avec des descriptions dans votre panneau d'administration sous **Paramètres → Panneau d'administration → Variables de configuration**.
Certains paramètres d'infrastructure comme les connexions de base de données (`PG_DATABASE_URL`), les URL du serveur (`SERVER_URL`) et les secrets (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) ne peuvent être configurés que via le fichier `.env`.
[Référence technique complète →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
</Warning>
## Clés de chiffrement
Twenty utilise deux clés de chiffrement uniquement définissables via des variables d'environnement :
| Variable | Objectif | Obligatoire |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `ENCRYPTION_KEY` | Clé principale utilisée pour chiffrer les secrets au repos (jetons OAuth, variables d'application, clés privées de signature, secrets TOTP, valeurs de configuration sensibles). | Oui pour les nouvelles installations (les anciennes installations peuvent s'appuyer à la place sur `APP_SECRET` — voir ci-dessous) |
| `FALLBACK_ENCRYPTION_KEY` | Clé utilisée uniquement pour la vérification. Définie lors d'une rotation sur l’*ancienne* valeur de `ENCRYPTION_KEY` afin que les lignes existantes restent déchiffrables. | Uniquement pendant la rotation |
Pour assurer la rétrocompatibilité, si `ENCRYPTION_KEY` n'est pas défini, Twenty se rabat sur `APP_SECRET` pour le chiffrement au repos, ce qui correspond au comportement historique des anciens déploiements. Les nouvelles installations doivent toujours définir une `ENCRYPTION_KEY` dédiée.
Générez les valeurs avec `openssl rand -base64 32` et stockez-les dans un endroit sûr (un gestionnaire de secrets, une configuration scellée, etc.). Perdre `ENCRYPTION_KEY` signifie perdre l'accès à tous les secrets stockés dans la base de données.
Pour faire pivoter `ENCRYPTION_KEY` sans temps d'arrêt, consultez le [guide de rotation des clés](/l/fr/developers/self-host/capabilities/key-rotation).
## 2. Configuration réservée à l'environnement
```bash
IS_CONFIG_VARIABLES_IN_DB_ENABLED=false
```
**Toute la configuration est gérée via les fichiers `.env` :**
1. Définissez `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` dans votre fichier `.env`
2. Ajoutez toutes les variables de configuration à votre fichier `.env`
3. Redémarrez les conteneurs pour que les modifications prennent effet
4. Le panneau d'administration affichera les valeurs actuelles mais ne pourra pas les modifier
## Mode multi-espaces de travail
Par défaut, Twenty fonctionne en **mode à espace de travail unique** — idéal pour la plupart des déploiements auto-hébergés où vous avez besoin d'une seule instance CRM pour votre organisation.
### Mode à espace de travail unique (par défaut)
```bash
IS_MULTIWORKSPACE_ENABLED=false # default
```
* Un espace de travail par instance Twenty
* Le premier utilisateur devient automatiquement administrateur avec tous les privilèges (`canImpersonate` et `canAccessFullAdminPanel`)
* Les nouvelles inscriptions sont désactivées après la création du premier espace de travail
* Structure d'URL simple : `https://your-domain.com`
### Activation du mode multi-espaces de travail
```bash
IS_MULTIWORKSPACE_ENABLED=true
DEFAULT_SUBDOMAIN=app # default value
```
Activez le mode multi-espaces de travail pour des déploiements de type SaaS où plusieurs équipes indépendantes ont besoin de leurs propres espaces de travail sur la même instance Twenty.
**Principales différences par rapport au mode à espace de travail unique :**
* Plusieurs espaces de travail peuvent être créés sur la même instance
* Chaque espace de travail possède son propre sous-domaine (par exemple, `sales.your-domain.com`, `marketing.your-domain.com`)
* Les utilisateurs s'inscrivent et se connectent sur `{DEFAULT_SUBDOMAIN}.your-domain.com` (par exemple, `app.your-domain.com`)
* Aucun privilège d'administrateur automatique — le premier utilisateur de chaque espace de travail est un utilisateur standard
* Des paramètres spécifiques à l'espace de travail, comme le sous-domaine et le domaine personnalisé, deviennent disponibles dans les paramètres de l'espace de travail
<Warning>
**Paramètre réservé à l'environnement :** `IS_MULTIWORKSPACE_ENABLED` ne peut être configuré que via le fichier `.env` et nécessite un redémarrage. Il ne peut pas être modifié via le panneau d'administration.
</Warning>
### Configuration DNS pour le mode multi-espaces de travail
Lorsque vous utilisez le mode multi-espaces de travail, configurez votre DNS avec un enregistrement générique (wildcard) pour permettre la création dynamique de sous-domaines :
```
*.your-domain.com -> your-server-ip
```
Cela active le routage automatique des sous-domaines pour les nouveaux espaces de travail, sans configuration DNS manuelle.
### Restreindre la création d'espaces de travail
En mode multi-espaces de travail, vous pouvez souhaiter limiter qui peut créer de nouveaux espaces de travail :
```bash
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
```
Lorsqu'il est activé, seuls les utilisateurs disposant de `canAccessFullAdminPanel` peuvent créer des espaces de travail supplémentaires. Les utilisateurs peuvent toujours créer leur premier espace de travail lors de l'inscription initiale.
## Intégration Gmail & Google Agenda
### Créer un projet Google Cloud
1. Allez dans [Google Cloud Console](https://console.cloud.google.com/)
2. Créez un nouveau projet ou sélectionnez-en un existant
3. Activez ces API :
* [API Gmail](https://console.cloud.google.com/apis/library/gmail.googleapis.com)
* [API Google Agenda](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com)
* [API People](https://console.cloud.google.com/apis/library/people.googleapis.com)
### Configurez OAuth
1. Allez dans [Identifiants](https://console.cloud.google.com/apis/credentials)
2. Créez un identifiant client OAuth 2.0
3. Ajoutez ces URI de redirection :
* `https://{your-domain}/auth/google/redirect` (pour le SSO)
* `https://{your-domain}/auth/google-apis/get-access-token` (pour les intégrations)
### Configurez dans Twenty
1. Allez dans **Paramètres → Panneau d’administration → Variables de configuration**
2. Retrouvez la section **Google Auth**
3. Définissez ces variables :
* `MESSAGING_PROVIDER_GMAIL_ENABLED=true`
* `CALENDAR_PROVIDER_GOOGLE_ENABLED=true`
* `AUTH_GOOGLE_CLIENT_ID={client-id}`
* `AUTH_GOOGLE_CLIENT_SECRET={client-secret}`
* `AUTH_GOOGLE_CALLBACK_URL=https://{your-domain}/auth/google/redirect`
* `AUTH_GOOGLE_APIS_CALLBACK_URL=https://{your-domain}/auth/google-apis/get-access-token`
<Warning>
**Mode réservé à l'environnement :** Si vous définissez `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, ajoutez ces variables à votre fichier `.env` à la place.
</Warning>
**Scopes requis** (configurés automatiquement) :
[Voir le code source pertinent](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts#L4-L10)
* `https://www.googleapis.com/auth/calendar.events`
* `https://www.googleapis.com/auth/gmail.readonly`
* `https://www.googleapis.com/auth/profile.emails.read`
### Si votre application est en mode test
Si votre application est en mode test, vous devez ajouter des utilisateurs test à votre projet.
Sous [Écran de consentement OAuth](https://console.cloud.google.com/apis/credentials/consent), ajoutez vos utilisateurs test à la section "Utilisateurs test".
## Intégration Microsoft 365
<Warning>
Les utilisateurs doivent avoir une [licence Microsoft 365](https://admin.microsoft.com/Adminportal/Home) pour pouvoir utiliser les API Calendrier et Messagerie. Ils ne pourront pas synchroniser leur compte sur Twenty sans cela.
</Warning>
### Créez un projet dans Microsoft Azure
Vous devrez créer un projet dans [Microsoft Azure](https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2) et obtenir les identifiants.
### Activez les API
Sur la console Microsoft Azure, activez les API suivantes dans "Autorisations" :
* Microsoft Graph : Mail.ReadWrite
* Microsoft Graph : Mail.Send
* Microsoft Graph : Calendars.Read
* Microsoft Graph : User.Read
* Microsoft Graph : openid
* Microsoft Graph : email
* Microsoft Graph : profile
* Microsoft Graph : offline_access
<Note>
"Mail.ReadWrite" et "Mail.Send" ne sont obligatoires que si vous souhaitez envoyer des e-mails en utilisant nos actions de flux de travail. Vous pouvez utiliser "Mail.Read" à la place si vous souhaitez uniquement recevoir des e-mails.
</Note>
<Note>
"Calendars.ReadWrite" est obligatoire si vous souhaitez créer des événements de calendrier en utilisant nos actions de flux de travail. Si vous souhaitez uniquement synchroniser des événements de calendrier, utilisez "Calendars.Read".
</Note>
### URI de redirection autorisées
Vous devez ajouter les URI de redirection suivantes à votre projet :
* `https://{your-domain}/auth/microsoft/redirect` si vous souhaitez utiliser Microsoft SSO
* `https://{your-domain}/auth/microsoft-apis/get-access-token`
### Configurez dans Twenty
1. Allez dans **Paramètres → Panneau d’administration → Variables de configuration**
2. Retrouvez la section **Microsoft Auth**
3. Définissez ces variables :
* `MESSAGING_PROVIDER_MICROSOFT_ENABLED=true`
* `CALENDAR_PROVIDER_MICROSOFT_ENABLED=true`
* `AUTH_MICROSOFT_ENABLED=true`
* `AUTH_MICROSOFT_CLIENT_ID={client-id}`
* `AUTH_MICROSOFT_CLIENT_SECRET={client-secret}`
* `AUTH_MICROSOFT_CALLBACK_URL=https://{your-domain}/auth/microsoft/redirect`
* `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://{your-domain}/auth/microsoft-apis/get-access-token`
<Warning>
**Mode réservé à l'environnement :** Si vous définissez `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, ajoutez ces variables à votre fichier `.env` à la place.
</Warning>
### Configurer les scopes
[Voir le code source pertinent](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts#L2-L9)
* 'openid'
* 'email'
* 'profil'
* 'offline_access'
* 'Mail.ReadWrite'
* 'Mail.Send'
* 'Calendars.Read'
### Si votre application est en mode test
Si votre application est en mode test, vous devez ajouter des utilisateurs test à votre projet.
Ajoutez vos utilisateurs test à la section "Utilisateurs et groupes".
## Tâches en arrière-plan pour Calendrier & Messagerie
Après avoir configuré les intégrations Gmail, Google Agenda ou Microsoft 365, vous devez démarrer les tâches en arrière-plan qui synchronisent les données.
Enregistrez les emplois récurrents suivants dans votre conteneur de travailleur :
```bash
# from your worker container
yarn command:prod cron:messaging:messages-import
yarn command:prod cron:messaging:message-list-fetch
yarn command:prod cron:calendar:calendar-event-list-fetch
yarn command:prod cron:calendar:calendar-events-import
yarn command:prod cron:messaging:ongoing-stale
yarn command:prod cron:calendar:ongoing-stale
yarn command:prod cron:workflow:automated-cron-trigger
```
## Configuration des Emails
1. Allez dans **Paramètres → Panneau d’administration → Variables de configuration**
2. Retrouvez la section **Email**
3. Configurez vos paramètres SMTP :
<Tabs>
<Tab title="Gmail">
Vous devrez créer un [mot de passe d'application](https://support.google.com/accounts/answer/185833).
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.gmail.com
* EMAIL_SMTP_PORT=465
* EMAIL_SMTP_USER=gmail_email_address
* EMAIL_SMTP_PASSWORD='gmail_app_password'
</Tab>
<Tab title="Office365">
Gardez à l'esprit que si vous avez activé la validation en deux étapes (2FA), vous devrez mettre en place un [mot de passe d'application](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.office365.com
* EMAIL_SMTP_PORT=587
* EMAIL_SMTP_USER=office365_email_address
* EMAIL_SMTP_PASSWORD='office365_password'
</Tab>
<Tab title="smtp4dev">
**smtp4dev** est un serveur SMTP factice pour le développement et les tests.
* Exécutez l'image smtp4dev : `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
* Accédez à l'interface utilisateur smtp4dev ici : [http://localhost:8090](http://localhost:8090)
* Définissez les variables suivantes :
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=localhost
* EMAIL_SMTP_PORT=2525
</Tab>
</Tabs>
<Warning>
**Mode réservé à l'environnement :** Si vous définissez `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, ajoutez ces variables à votre fichier `.env` à la place.
</Warning>
## Stockage S3
<Warning>
Par défaut, Twenty stocke les fichiers téléversés sur le système de fichiers local. Pour les déploiements en production, utilisez S3 ou un service compatible S3 (MinIO, DigitalOcean Spaces, etc.). afin de garantir la persistance des fichiers lors des redémarrages des conteneurs et la montée en charge sur plusieurs instances de serveur.
</Warning>
Définissez `STORAGE_TYPE=S_3` et configurez les variables `STORAGE_S3_*` via le panneau d'administration ou `.env`. Voir la [référence config-variables.ts](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts) pour la liste complète des variables S3.
Lorsque vous utilisez S3 avec des fonctionnalités dépendantes de CORS (par exemple les téléchargements de fichiers dans le navigateur), assurez-vous que votre compartiment S3 autorise l'origine de votre frontend Twenty dans sa configuration CORS.
## Fonctions logiques et interpréteur de code
Twenty prend en charge les fonctions logiques pour les flux de travail et l'interpréteur de code pour l'analyse de données par IA. Les deux exécutent du code fourni par l'utilisateur et nécessitent une configuration explicite pour la sécurité.
### Paramètres de sécurité par défaut
**En production (NODE_ENV=production):** Les fonctions logiques et l'interpréteur de code sont par défaut sur **Désactivé**. Vous devez les activer explicitement avec `LOGIC_FUNCTION_TYPE` et `CODE_INTERPRETER_TYPE` si vous avez besoin de ces fonctionnalités.
**En développement (NODE_ENV=development):** Les deux sont par défaut sur **LOCAL** pour plus de commodité lors de l'exécution en local.
<Warning>
**Avis de sécurité :** Le pilote local (`LOGIC_FUNCTION_TYPE=LOCAL` ou `CODE_INTERPRETER_TYPE=LOCAL`) exécute le code directement sur l'hôte dans un processus Node.js sans sandboxing. Il ne doit être utilisé que pour du code approuvé en développement. Pour les déploiements en production traitant du code non fiable, utilisez `LOGIC_FUNCTION_TYPE=LAMBDA` ou `CODE_INTERPRETER_TYPE=E2B` (avec exécution en bac à sable), ou laissez-les désactivés.
</Warning>
### Fonctions logiques - Pilotes disponibles
| Pilote | Variable d'environnement | Cas d'utilisation | Niveau de sécurité |
| --------- | ------------------------------ | ---------------------------------------------- | ------------------------------------ |
| Désactivé | `LOGIC_FUNCTION_TYPE=DISABLED` | Désactiver complètement les fonctions logiques | N/A |
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Environnements de développement et approuvés | Faible (aucun sandboxing) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Production avec du code non approuvé | Élevé (isolement au niveau matériel) |
### Fonctions logiques - Configuration recommandée
**Pour le développement:**
```bash
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Pour la production (AWS):**
```bash
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Pour désactiver les fonctions logiques:**
```bash
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Interpréteur de code - Pilotes disponibles
| Pilote | Variable d'environnement | Cas d'utilisation | Niveau de sécurité |
| --------- | -------------------------------- | ---------------------------------------- | -------------------------- |
| Désactivé | `CODE_INTERPRETER_TYPE=DISABLED` | Désactiver l'exécution de code par l'IA | N/A |
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Développement uniquement | Faible (aucun sandboxing) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Production avec exécution en bac à sable | Élevée (bac à sable isolé) |
<Note>
Lorsque vous utilisez `LOGIC_FUNCTION_TYPE=DISABLED` ou `CODE_INTERPRETER_TYPE=DISABLED`, toute tentative d'exécution renverra une erreur. Cela est utile si vous souhaitez exécuter Twenty sans ces capacités.
</Note>
## API et interface front-end sur des hôtes séparés
Uniquement pertinent si le back-end ne sert pas l’interface front-end. Les déploiements de même origine
n’ont besoin de rien ici.
Déclarez toutes les origines de navigateur qui communiquent avec l’API, séparées par des virgules. `SERVER_URL`
et `FRONTEND_URL` sont déjà approuvées. Le serveur consigne un avertissement lorsqu’un navigateur
l’appelle depuis une origine non déclarée.
```bash
AUTH_COOKIE_ALLOWED_ORIGINS=https://app.example.com
```
<Warning>
Le front-end envoie les identifiants à chaque requête, et les navigateurs rejettent une
réponse avec des identifiants provenant d’une origine non déclarée, de sorte que l’application ne parvient pas à se charger tant que vous n’avez pas défini ce paramètre.
</Warning>
Si les deux hôtes se trouvent sur des domaines enregistrables différents (et pas seulement sur des sous-domaines différents), définissez également `AUTH_COOKIE_SAME_SITE=none`, ce qui nécessite HTTPS.
Avec le mode multi-espaces de travail activé, ne répertoriez pas ici chaque sous-domaine d’espace de travail.
Au lieu de cela, acheminez les chemins d’API (`/graphql`, `/metadata`, `/rest`, `/auth/`,
`/client-config`, `/file`) vers le back-end sur chaque nom d’hôte d’espace de travail, afin que chaque
espace de travail reste de même origine — c’est ainsi que fonctionne Twenty Cloud.

View File

@@ -0,0 +1,254 @@
---
title: Résolution des problèmes
icon: wrench
---
## Résolution des problèmes
Si vous rencontrez un problème lors de la configuration de l'environnement pour le développement, la mise à niveau de votre instance ou l'auto-hébergement, voici quelques solutions pour les problèmes courants.
### Auto-hébergement
#### La première installation entraîne l'erreur `l'authentification du mot de passe a échoué pour l'utilisateur "postgres"`
🚨 **IMPORTANT : Cette solution est UNIQUEMENT pour les nouvelles installations** 🚨
Si vous avez une instance Twenty existante avec des données de production, **NE SUIVEZ PAS** ces étapes, car elles supprimeront définitivement votre base de données !
Lors de l'installation de Twenty pour la première fois, vous pouvez vouloir changer le mot de passe par défaut de la base de données.
Le mot de passe que vous définissez lors de la première installation est stocké de façon permanente dans le volume de la base de données. Si vous essayez ensuite de changer ce mot de passe dans votre configuration sans supprimer l'ancien volume, vous obtiendrez des erreurs d'authentification car la base de données utilise toujours le mot de passe original.
⚠️ AVERTISSEMENT : Les étapes suivantes SUPPRIMERONT DÉFINITIVEMENT toutes les données de la base de données ! ⚠️
N'avancez que s'il s'agit d'une nouvelle installation sans données importantes.
Pour mettre à jour le `PG_DATABASE_PASSWORD`, vous devez :
```sh
# Update the PG_DATABASE_PASSWORD in .env
docker compose down --volumes
docker compose up -d
```
#### Retours à la ligne CR trouvés [Windows]
Cela se produit en raison des caractères de fin de ligne Windows combinés à votre configuration Git. Essayez de lancer :
```
git config --global core.autocrlf false
```
Ensuite, supprimez le dépôt et clonez-le de nouveau.
#### Schéma de métadonnées manquant
Pendant l'installation de Twenty, vous devez approvisionner votre base de données postgres avec les bons schémas, extensions et utilisateurs.
Si vous réussissez cet approvisionnement, vous devriez avoir des schémas `default` et `metadata` dans votre base de données.
Si vous ne le faites pas, assurez-vous que vous n'avez pas plus d'une instance PostgreSQL fonctionnant sur votre ordinateur.
#### Impossible de trouver le module 'twenty-emails' ou les déclarations de type correspondantes.
Vous devez construire le paquet `vingt-emails` (`npx nx exécuter vingt-emails:build`) avant d'exécuter l'initialisation de la base de données.
#### Package twenty-x manquant
Assurez-vous d'exécuter yarn dans le répertoire racine, puis d'exécuter `npx nx server:dev twenty-server`. Si cela ne fonctionne toujours pas, essayez de construire le package manquant manuellement.
#### Lint à l'enregistrement ne fonctionne pas
Cela devrait fonctionner directement avec l'extension Oxc (`oxc.oxc-vscode`) installée. Si cela ne fonctionne pas, essayez d'ajouter ceci à vos paramètres de code VS (portée du conteneur de développement):
```
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
}
```
#### Lors de l'exécution de `npx nx start` ou `npx nx start twenty-front`, une erreur de mémoire est levée
Lancez uniquement les services dont vous avez besoin, au lieu de `npx nx start`. Par exemple, si vous travaillez sur le serveur, lancez seulement `npx nx worker twenty-server`
**Si cela ne fonctionne pas :**
Si vous avez essayé de lancer uniquement `npx nx run twenty-server:start` sur WSL et que cela échoue avec l'erreur de mémoire ci-dessous :
`ERREUR FATALE : Les compactages inefficaces près de la limite du tas Allocation échouée - tas JavaScript sans mémoire`
Une solution de contournement est d'exécuter la commande suivante dans votre terminal, ou de l'ajouter à votre profil `.bashrc` afin qu'il soit appliqué automatiquement:
`export NODE_OPTIONS="--max-old-space-size=8192"`
L'option --max-old-space-size=8192 fixe une limite supérieure de 8 Go pour le tas Node.js ; l'utilisation s'adapte à la demande de l'application.
Référence : https://stackoverflow.com/questions/56982005/where-do-i-set-node-options-max-old-space-size-2048
**Si cela ne fonctionne pas :**
Examinez quels processus prennent la plupart de la mémoire vive de votre machine. À Twenty, nous avons remarqué que certaines extensions VS Code prenaient beaucoup de RAM, donc nous les avons temporairement désactivées.
**Si cela ne fonctionne pas :**
Redémarrer votre machine aide à nettoyer les processus fantômes.
#### Pendant l'exécution de `npx nx start`, il y a des [0] et [1] étranges dans les journaux
C'est attendu car la commande `npx nx start` exécute plus de commandes en arrière-plan
#### Aucun e-mail n'est envoyé
La plupart du temps, c'est parce que le `worker` ne fonctionne pas en arrière-plan. Essayez de lancer
```
npx nx worker twenty-server
```
#### Impossible de connecter mon compte Microsoft 365 à 20
La plupart du temps, c'est parce que votre administrateur n'a pas activé la licence Microsoft 365 pour votre compte. Vérifiez [https://admin.microsoft.com/](https://admin.microsoft.com/Adminportal/Home).
Si vous avez un code d'erreur `AADSTS50020`, cela signifie probablement que vous utilisez un compte personnel Microsoft. Cela n'est pas encore pris en charge. Plus d'informations [ici](https://learn.microsoft.com/fr-fr/troubleshoot/entra/entra-id/app-integration/error-code-aadsts50020-user-account-identity-provider-does-not-exist)
#### En exécutant `yarn`, les avertissements apparaissent dans la console
Les avertissements informent du tirage de dépendances supplémentaires qui ne sont pas explicitement spécifiées dans `package.json`, donc tant qu'aucune erreur bloquante n'apparaît, tout devrait fonctionner comme prévu.
#### Lorsqu'un utilisateur accède à la page de connexion, une erreur à propos d'un utilisateur non autorisé essayant d'accéder à un espace de travail apparaît dans les journaux
Ceci est attendu car l'utilisateur n'est pas autorisé lors de sa déconnexion, car son identité n'a pas été vérifiée.
#### Comment vérifier si votre worker fonctionne ?
* Allez sur [webhook-test.com](https://webhook-test.com/) et copiez **Votre URL Webhook Unique**.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-test.jpg" alt="Test de Webhook" />
</div>
* Ouvrez votre application Twenty, naviguez jusqu'à `/settings`, et activez le bouton **Avancé** en bas à gauche de l'écran.
* Créez un nouveau webhook.
* Collez **Votre URL Webhook Unique** dans le champ **URL de l'Endpoint** de Twenty. Réglez les **Filtres** sur `Sociétés` et `Créé`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-settings.jpg" alt="Paramètres du Webhook" />
</div>
* Accédez à `/objects/companies` et créez un nouvel enregistrement de société.
* Retournez sur [webhook-test.com](https://webhook-test.com/) et vérifiez si une nouvelle **requête POST** a été reçue.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-test-result.jpg" alt="Résultat du test du Webhook" />
</div>
* Si une **requête POST** est reçue, votre worker fonctionne avec succès. Sinon, vous devez résoudre les problèmes de votre worker.
#### Front-end échoue à démarrer et retourne l'erreur TS5042 : l'option 'project' ne peut pas être mélangée avec les fichiers source sur la ligne de commande
Commenter le plugin checker dans `packages/twenty-ui/vite-config.ts`, comme montré dans l'exemple ci-dessous:
```
plugins: [
react({ jsxImportSource: 'react' }),
tsconfigPaths(),
svgr(),
dts(dtsConfig),
// checker(checkersConfig),
wyw({
include: [
'**/OverflowingTextWithTooltip.tsx',
'**/Chip.tsx',
'**/Tag.tsx',
'**/Avatar.tsx',
'**/AvatarChip.tsx',
],
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},
}),
],
```
#### Panneau d'administration inaccessible
Exécutez `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` dans le conteneur de base de données pour obtenir l'accès au panneau d'administration.
#### Lors de l'exécution d'un workflow, l'exécution du workflow échoue avec "L'exécution des fonctions logiques est désactivée. Définissez LOGIC_FUNCTION_TYPE sur LOCAL ou LAMBDA pour les activer."
En production, les fonctions logiques sont désactivées par défaut. Définissez la variable d'environnement `LOGIC_FUNCTION_TYPE` sur `LOCAL` ou `LAMBDA` pour les activer. Cela peut être configuré via des variables d'environnement ou via les variables de base de données du panneau d'administration. Voir le [guide de configuration des fonctions logiques](/l/fr/developers/self-host/capabilities/setup#logic-functions-available-drivers) pour plus de détails.
#### Quand je tape un message dans le chat IA, il ne répond pas immédiatement et je dois actualiser la page pour voir la réponse
Cela signifie que votre proxy inversé manque la configuration des réponses API basées sur le stream. Voici un exemple de configuration Nginx :
```
location ~* ^/api/.*stream|\/graphql\/stream {
proxy_pass http://twenty_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
proxy_cache off;
gzip off;
#chunked_transfer_encoding off;
add_header X-Accel-Buffering no always;
}
```
### Composer Docker en 1 clic
#### Impossible de se connecter
Si vous ne pouvez pas vous connecter après la configuration :
1. Exécutez les commandes suivantes :
```bash
docker exec -it twenty-server-1 yarn
docker exec -it twenty-server-1 npx nx database:reset --configuration=no-seed
```
2. Redémarrez les conteneurs Docker :
```bash
docker compose down
docker compose up -d
```
Notez que la commande `database:reset` effacera complètement votre base de données et la recréera de zéro.
#### Problèmes de connexion derrière un proxy inverse
Si vous exécutez Twenty derrière un proxy inverse et rencontrez des problèmes de connexion :
1. **Vérifiez SERVER_URL :**
Assurez-vous que `SERVER_URL` dans votre fichier `.env` correspond à l'URL d'accès externe, incluant `https` si SSL est activé.
2. **Vérifiez les paramètres du proxy inverse :**
* Confirmez que votre proxy inverse transfère correctement les requêtes au serveur Twenty.
* Assurez-vous que les en-têtes comme `X-Forwarded-For` et `X-Forwarded-Proto` sont correctement configurés.
3. **Redémarrez les services :**
Après avoir apporté des modifications, redémarrez à la fois le proxy inverse et les conteneurs Twenty.
#### Erreur lors du téléchargement d'une image - permission refusée
Changer la propriété du dossier de données sur l'hôte de root à un autre utilisateur et groupe résout ce problème.
#### Erreur « File Not Found » lors du test de la fonction Logic dans le workflow
Exécutez la commande `yarn command:prod application:rebuild-default-deps` dans votre conteneur twenty-server pour résoudre le problème de dépendances manquantes.
## Obtenir de l'aide
Si vous rencontrez des problèmes non couverts par ce guide :
* Vérifiez les journaux :
Consultez les journaux de conteneur pour les messages d'erreur :
```bash
docker compose logs
```
* Support communautaire :
Contactez la [communauté Twenty](https://github.com/twentyhq/twenty/issues) ou les [canaux de support](https://discord.gg/cx5n4Jzs57) pour obtenir de l'aide.

View File

@@ -0,0 +1,106 @@
---
title: Guide de mise à niveau
icon: arrow-up-right-dots
---
## Consignes générales
**Sauvegardez toujours votre base de données avant de commencer le processus de mise à niveau** en exécutant :
```bash
docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql
```
Pour restaurer à partir d'une sauvegarde :
```bash
cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}
```
Si vous utilisez Docker Compose, suivez ces étapes :
1. Arrêtez Twenty : `docker compose down`
2. Modifiez la valeur de `TAG` dans le fichier `.env` situé à côté de votre `docker-compose.yml`
3. Démarrez Twenty : `docker compose up -d`
Le serveur exécute automatiquement au démarrage toutes les migrations de mise à niveau requises. Aucune commande manuelle n'est nécessaire.
## Mise à niveau vers v2.34+ — PostgreSQL 15 minimum
Twenty v2.34 nécessite PostgreSQL 15 ou une version ultérieure. La configuration officielle de Docker Compose utilise PostgreSQL 16. Si votre instance se connecte à une base de données gérée en externe, mettez-la à niveau avant de démarrer le serveur v2.34.
## Mises à niveau entre versions (v1.23+)
À partir de **v1.23**, Twenty prend en charge les mises à niveau entre versions. Vous pouvez passer directement de n'importe quelle version prise en charge à la dernière version sans devoir passer par chaque version intermédiaire.
Par exemple, la mise à niveau de la v1.23 directement vers la v2.0 est entièrement prise en charge.
## Mise à niveau vers la v2.5+ — enveloppe de chiffrement au repos
À partir de la **v2.5**, Twenty stocke les secrets au repos (jetons OAuth, variables d’application, clés privées de signature, valeurs de configuration sensibles, secrets TOTP) à l’intérieur d’une enveloppe versionnée `enc:v2:` chiffrée avec `ENCRYPTION_KEY` (ou `APP_SECRET` si `ENCRYPTION_KEY` n’est pas défini).
Le premier démarrage sur la v2.5 exécute des commandes de mise à niveau lentes qui **rétro-remplissent** les lignes existantes dans la nouvelle enveloppe. Elles sont idempotentes — si vous interrompez puis redémarrez le serveur, le processus reprend là où il s’était arrêté — mais elles peuvent prendre du temps sur les grandes bases de données. Vous pouvez surveiller la progression avec `upgrade:status`.
Vous devez définir une `ENCRYPTION_KEY` dédiée **avant** la mise à niveau vers la v2.5 afin que le rétro-remplissage écrive les lignes sous cette clé dès le départ. Changer de clé après le rétro-remplissage nécessite une [rotation](/l/fr/developers/self-host/capabilities/key-rotation).
## Rotation des secrets et des clés de signature
Pour les tâches opérationnelles quotidiennes comme la rotation de `ENCRYPTION_KEY`, la rotation de la clé de signature JWT ou la révocation d’une clé de signature divulguée, consultez le [Guide de rotation des clés](/l/fr/developers/self-host/capabilities/key-rotation) dédié.
## Vérification de l'état de la mise à niveau
La commande `upgrade:status` vous permet d'inspecter l'état actuel de votre instance et des migrations d'espaces de travail. Elle est utile pour diagnostiquer les problèmes de mise à niveau ou lors de la création d'une demande d'assistance.
Exécutez-la depuis le conteneur du serveur :
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status
```
Exemple de résultat:
```sh
APP_VERSION: v1.23.0
Instance
Inferred version: 1.23.0
Latest command: 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:43:58.823Z
Workspace
Apple (20202020-1c25-4d02-bf25-6aeccf7ea419)
Inferred version: 1.23.0
Latest command: 1.23.0_UpdateGlobalObjectContextCommandMenuItemsCommand_1780000005000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:44:09.361Z
Summary
Instance: Up to date
Workspaces: 1 up to date, 0 behind, 0 failed (1 total)
```
### Options
| Option | Description |
| ------------------------- | --------------------------------------------------------------------------------- |
| `-w, --workspace-id <id>` | Limiter à un espace de travail spécifique. Peut être passé plusieurs fois. |
| `-f, --failed-only` | Masquez les espaces de travail à jour, n'affichez que ceux en retard et en échec. |
## Résolution des problèmes
Si la mise à niveau échoue sur certains espaces de travail, le serveur ne dépassera pas l'étape en échec. Le redémarrage du serveur (`docker compose up -d`) relancera la mise à niveau à partir de l'endroit où elle s'est arrêtée.
Pour identifier rapidement les problèmes, exécutez :
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status --failed-only
```
Cela affiche uniquement les espaces de travail en retard ou en échec, ainsi que le message d'erreur pour chaque échec.
## Avant la v1.23
Si votre instance est antérieure à la v1.23, vous devez effectuer une mise à niveau progressive à travers chaque version majeure marquée (de v1.6 à v1.7, puis de v1.7 à v1.8, et ainsi de suite) jusqu'à atteindre la v1.23. À partir de là, vous pouvez passer directement à la dernière version.

View File

@@ -0,0 +1,26 @@
---
title: Auto-hébergement
description: Déployez et gérez Twenty sur votre propre infrastructure.
---
<Frame>
<img src="/images/user-guide/what-is-twenty/20.png" alt="IA" />
</Frame>
## Vue d'ensemble
Twenty peut être auto-hébergé sur votre propre infrastructure, vous offrant un contrôle total sur vos données et votre déploiement.
## Pourquoi l'auto-hébergement ?
* **Propriété des données** : Conservez toutes les données CRM sur vos propres serveurs
* **Conformité** : Répondez aux exigences réglementaires en matière de résidence des données
* **Personnalisation** : Accès complet pour modifier et étendre la plateforme
## Prise en main
<CardGroup cols={2}>
<Card title="Docker Compose" icon="docker" href="/l/fr/developers/self-host/capabilities/docker-compose">
Configuration rapide avec Docker
</Card>
</CardGroup>