> ## 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. Construirea interfeței

> Vizualizări, navigare bară laterală, o comandă și componente frontale.

În acest moment, obiectele sunt accesibile doar prin Setări. Hai să oferim aplicației o prezență reală
în UI: vizualizare listă, intrări sidebar, un singur click
**Generează document** comandă, o filă de înregistrare a componentei frontale la **previzualizarea** un document
şi o filă nativă bog-text **editor** pentru şabloane.

## Vizualizări și navigare

Un [view](/l/ro/developers/extend/apps/layout/views) este o listă salvată a unui obiect dat.
Un [element meniu de navigare](/l/ro/developers/extend/apps/layout/navigation-menu-items)
introduce această vizualizare în bara laterală.

```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,
});
```

Adaugă aceeași pereche pentru șabloane. Ambele sunt prezentate acum pe partea laterală:

<Frame caption="Documente şi şabloane din bara laterală, documentul generat fiind enumerat.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/imagini/documente/dezvoltatori/extinderi/aplica%C8%9Bii/generator/04-documents-view.png" alt="Vizualizare documente cu un document generat" />
</Frame>

## O componentă față

O [componentă frontală](/l/ro/developers/extend/apps/layout/front-components) este o componentă React
inserată în Twenty. Ours citeşte înregistrarea selectată, încarcă şabloanele pentru persoane
prin `CoreApiClient`, şi POST-uri pe ruta de la ultimul capitol
.

```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>
  Stilul cu variabilele CSS înline (`var(--t-color-blue)`), nu valorile importate din
  `douăzeci enty-ui`. SDK se dublează că pachetul a fost construit, astfel încât importurile constantelor de temă
  la nivel de modul ar fi `nedefinită`. Vezi
  [componenta completă](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>

## O comandă pentru a o deschide

Un [element meniu de comandă](/l/ro/developers/extend/apps/layout/command-menu-items) cu
\`availabilityType: 'RECORD\_SELECTION'' apare atunci când o Persoană este selectată, și
deschide componenta în panoul lateral.

```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,
});
```

## Încercați întregul flux

Deschideți **oameni**, bifați o persoană și apăsați <kbd>£K</kbd> / <kbd>Ctrl K</kbd>.
"Generează document" apare, etichetat cu aplicația ta:

<Frame caption="Comanda apare atunci când o persoană este selectată.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/imagini/documente/dezvoltatori/extinderi/aplica%C8%9Bii/generator/06-command-menu.png" alt="Meniul de comenzi cu Generarea documentului" />
</Frame>

Rulează — componenta ta se deschide în panoul lateral. Alege un șablon, fă clic pe
**Generează** și pe un nou teren de înregistrări în **documente**.

<Frame caption="Componenta frontală, șabloanele de încărcare și generarea unui clic.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/imagini/documente/dezvoltatori/extinderi/aplica%C8%9Bii/generator/06b-front-componente.png" alt="Generare panou de document" />
</Frame>

Fiecare document generat înregistrează aplicația dvs. ca autor:

<Frame caption="Creat de Generatorul de Document Stare generată.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/imagini/documente/dezvoltatori/extensii/aplica%C8%9Bii/generator/document-document-record.png" alt="Înregistrare de documente generată" />
</Frame>

## Previzualizați un document pe pagina sa de înregistrare

O componentă frontală nu este doar pentru meniurile de comenzi — poți canta una ca \*\*tab pe pagina de înregistrare
\*\*. Hai să adăugăm o filă *Preview* la înregistrarea de document care face corpul
Markdown ca o pagină șlefuită.

Componenta citește id-ul de înregistrare curent din contextul de execuție, încarcă documentul
și îl redă. Componentele frontului rulează într-un **sandbox** care permite doar o albire
a etichetelor HTML - injectarea brută HTML (`dangerouslySetInnerHTML`) și
`\<style>` sunt blocate - așa că am redat Markdown ca elemente React cu linia
stiluri mici [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
hel.

```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,
});
```

Montaţi cu un [aspect de pagină](/l/ro/developers/extend/apps/layout/page-layouts). Un layout
`RECORD_PAGE` adaugă file la vizualizarea unui obiect de înregistrare; un widget `FRONT_COMPONENT`
într-o filă `CANVAS` găzduieşte componenta:

```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,
      },
    }],
  }],
});
```

Deschide orice document — o filă **Preview** o face frumoasă, cu link-uri către pagina web partajabilă
și PDF:

<Frame caption="Tab-ul de Previzualizare face documentul cu stiluri inline, plus link-uri rapide.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/imagini/documente/dezvoltatori/extensii/aplica%C8%9Bii/generator/09-document-viewer.png" alt="Vizualizator document componenta frontală într-o filă de înregistrare pagină" />
</Frame>

## Editați un șablon cu editorul de text bogat

Șabloanele nu au nevoie deloc de o componentă personalizată. Deoarece `body` este un câmp
`RICH_TEXT`, Douăzeci de ani oferă deja un editor complet de text pentru el — același
cu Nota standard și obiectul de activitate. Îl înfățișăm doar pe pagina de înregistrare a șablonului

Adăugați o filă cu un widget `FIELD` în modul de afișare `EDITOR`, îndreptat în câmpul `body`
prin `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 câmp `RICH_TEXT` stochează atât blocul editorului JSON cât și un proiecție Markdown
. Conducta de generare citeşte că Markdown projection, deci
placeholders, PDF, și pagina web partajabilă continuă să funcționeze neschimbat —
vezi tot
[`template-record. layout-ul vârstei`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
Editorii scriu șabloane într-un adevărat editor de text bogat:

<Frame caption="Tab-ul modelului: Editorul nativ de text bogat al Twenty's legat de câmpul corpului.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/images/documente/dezvoltatori/extends/apps/document-generator/10-template-editor.png" alt="Șablon de înregistrare cu fila nativă de editor de text bogat" />
</Frame>

**După acest pas:** documentele previzualizează frumos și șabloanele sunt editabile
în aplicație. Apoi, lasă un agent AI să le genereze dintr-o conversație.

<Card title="Următorul: un agent AI →" icon="robot" href="/dezvoltator/extindere/aplicații/tutoriale/document-generator/ai-agent">
  Adăugați un agent și o abilitate care vă sună unelta.
</Card>
