Passer au contenu principal
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 est une liste enregistrée d’un objet donné. Un élément du menu de navigation place cette vue dans la barre latérale.
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 },
  ],
});
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 :
Vue des documents avec un document généré

Un composant frontal

Un composant frontal 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 .
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,
});
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.

Une commande pour l’ouvrir

Un lien de menu de commande avec availabilityType: 'RECORD_SELECTION' s’affiche quand une personne est sélectionnée, et ouvre le composant dans le panneau latéral.
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 ΩK / Ctrl K. “Générer un document” apparaît, marqué avec votre application :
Menu de commande avec Générer un document
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.
Générer le panneau latéral du document
Chaque document généré enregistre votre application comme auteur:
Un enregistrement de document généré

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 .
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:
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 :
Composant frontal de la visionneuse de documents dans un onglet de page d'enregistrement

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:
{
  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. Maintenant, les éditeurs écrivent des modèles dans un éditeur de texte riche approprié:
Enregistrement du modèle avec l'onglet de l'éditeur natif de texte riche
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.

Suivant : un agent IA →

Ajoutez un agent et une compétence qui appellent votre outil.