i18n - docs translations (#22617)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22617?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
07a921f8ca
commit
18ca89bcdd
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
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 conversation 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>
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
---
|
||||
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.
|
||||
|
||||
```ts filename="src/views/documents.view.ts"
|
||||
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"
|
||||
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 { 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"
|
||||
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 commandes — 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 { 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"
|
||||
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="/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',
|
||||
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="/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>
|
||||
+135
@@ -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>
|
||||
+239
@@ -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 workflow**.
|
||||
|
||||
## 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 Person par rapport à une Company 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>
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
---
|
||||
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 des applis sont servies dans `/s` sur votre
|
||||
Serveur Vingt (par exemple `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
## 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>
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: "Tutoriel : Générateur de documents"
|
||||
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) | Livrez-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>
|
||||
+137
@@ -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
|
||||
. Mettez un logo dans `public/` et faites-le référence avec `logoUrl`.
|
||||
|
||||
```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.',
|
||||
logoUrl: '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 PNGs dans
|
||||
`public/gallery/` et référencez-les avec `screenshots` — ils apparaissent comme une galerie
|
||||
sur la page de liste.
|
||||
|
||||
```ts filename="src/application-config.ts"
|
||||
export default defineApplication({
|
||||
// ...identity from above
|
||||
screenshots: [
|
||||
'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 dev --once --dry-run # preview the metadata diff
|
||||
```
|
||||
|
||||
La course à sec imprime exactement ce qui pourrait changer sur le serveur sans l'appliquer —
|
||||
une bonne vérification de l'état d'esprit. 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>
|
||||
Reference in New Issue
Block a user