> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 4. Construire l'interface utilisateur

> 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.

```ts filename="src/views/documents.view.ts" theme={null}
import { defineView, ViewKey } from 'twenty-sdk/define';

export default defineView({
  universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
  name: 'All documents',
  objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
  icon: 'IconFile',
  key: ViewKey.INDEX,
  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" theme={null}
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="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/04-documents-view.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=26d26efeea7825a0e740c5ed21d545f7" alt="Vue des documents avec un document généré" width="2880" height="1800" data-path="images/docs/developers/extends/apps/document-generator/04-documents-view.png" />
</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" theme={null}
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
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 apiBaseUrl = process.env.TWENTY_API_URL;
    const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
    const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      body: JSON.stringify({ templateId, recordId }),
    }).then((r) => r.json());
    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" theme={null}
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="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/06-command-menu.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=7f565368b8e4f21b0899b89505cc4f34" alt="Menu de commande avec Générer un document" width="2880" height="1800" data-path="images/docs/developers/extends/apps/document-generator/06-command-menu.png" />
</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="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/06b-front-component.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=4fb7f799ef6db5e0d336591a8f61f72a" alt="Générer le panneau latéral du document" width="2880" height="1800" data-path="images/docs/developers/extends/apps/document-generator/06b-front-component.png" />
</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="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/05-document-record.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=735261a59834a51277284303a9d6f845" alt="Un enregistrement de document généré" width="2880" height="1800" data-path="images/docs/developers/extends/apps/document-generator/05-document-record.png" />
</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" theme={null}
import { CoreApiClient } from 'twenty-client-sdk/core';
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 = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?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 mise en page
`RECORD_PAGE` ajoute des onglets à la vue d'un enregistrement d'un objet ; un widget `FRONT_COMPONENT`
dans un onglet `CANVAS` héberge le composant:

```ts filename="src/page-layouts/document-record.page-layout.ts" theme={null}
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.CANVAS,
    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="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/09-document-viewer.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=e56c0d51f60f1597b55f497c35245e7e" alt="Composant frontal de la visionneuse de documents dans un onglet de page d'enregistrement" width="2880" height="1800" data-path="images/docs/developers/extends/apps/document-generator/09-document-viewer.png" />
</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" theme={null}
{
  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',
    gridPosition: { 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).
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="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/10-template-editor.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=4e630b22cb2fbfa57423cb42ad169da5" alt="Enregistrement du modèle avec l'onglet de l'éditeur natif de texte riche" width="2880" height="1800" data-path="images/docs/developers/extends/apps/document-generator/10-template-editor.png" />
</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>
