> ## Documentation Index
> Fetch the complete documentation index at: https://twenty-claude-cool-pascal-5ay683.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 4. Construyendo la interfaz de usuario

> Visualizaciones, navegación en la barra lateral, un comando y componentes frontales.

Ahora mismo los objetos sólo son accesibles a través de Configuración. Vamos a dar a la aplicación una presencia
real en la interfaz de usuario: vistas de lista, entradas de la barra lateral, un comando de
**Generar documento**, un componente frontal de página de registro para **previsualizar** un documento
y una pestaña de **editor** de texto nativo para plantillas.

## Vistas y navegación

Una [view](/l/es/developers/extend/apps/layout/views) es una lista guardada de un objeto dado.
Un [elemento del menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items)
pone esa vista en la barra lateral. El servidor ya aprovisiona automáticamente la vista predeterminada "All ..." de cada objeto, por lo que las vistas que define una aplicación son adicionales y seleccionadas.

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

export default defineView({
  universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
  name: 'All documents',
  objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
  icon: 'IconFile',
  position: 0,
  fields: [
    { universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
      fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
      position: 0, isVisible: true, size: 280 },
    { universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
      fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
      position: 1, isVisible: true, size: 120 },
    { universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
      fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
      position: 2, isVisible: true, size: 200 },
  ],
});
```

```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts" 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,
});
```

Añadir el mismo par para las plantillas. Ambos ahora se muestran en la barra lateral:

<Frame caption="Documentos y plantillas en la barra lateral con el documento generado listado.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty-claude-cool-pascal-5ay683/images/docs/desarrolladores/extends/apps/document-generator/04-documents-view.png" alt="Vista de documentos con un documento generado" />
</Frame>

## Un componente frontal

Un [componente delantero](/l/es/developers/extend/apps/layout/front-components) es un componente de React
en el interior de Twenty. Nuestra nuestra lee el registro seleccionado, carga las plantillas
persona a través de `CoreApiClient`, y POSTs a la ruta desde el último capítulo
.

```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 { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';

const GenerateDocumentForm = () => {
  const selectedRecordIds = useSelectedRecordIds();
  const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
  const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
  const [templateId, setTemplateId] = useState('');

  useEffect(() => {
    new CoreApiClient()
      .query({ documentTemplates: {
        __args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
        edges: { node: { id: true, name: true } } } })
      .then(({ documentTemplates }) => {
        const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
        setTemplates(list);
        if (list[0]) setTemplateId(list[0].id);
      });
  }, []);

  const generate = async () => {
    const res = await new RestApiClient().post<{ success: boolean }>(
      '/s/documents/generate',
      { templateId, recordId },
    );
    await enqueueSnackbar({
      message: res.success ? 'Document generated.' : 'Generation failed.',
      variant: res.success ? 'success' : 'error',
    });
  };

  // ...render a <select> of templates and a Generate button
};

export default defineFrontComponent({
  universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
  name: 'generate-document-form',
  component: GenerateDocumentForm,
});
```

<Warning>
  Estilo con variables CSS en línea (`var(--t-color-blue)`), no valores importados de
  `twenty-ui`. El SDK simula ese paquete durante la compilación, por lo que las importaciones a nivel de módulo de
  constantes de tema serían `undefined`. Ver el
  [componente completo](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>

## Un comando para abrirlo

Un [elemento del menú de comandos](/l/es/developers/extend/apps/layout/command-menu-items) con
`availabilityType: 'RECORD_SELECTION'` aparece cuando se selecciona una Persona, y
abre el componente en el panel 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,
});
```

## Prueba todo el flujo

Abre **Personas**, marca a una persona y pulsa <kbd>mañK</kbd> / <kbd>Ctrl K</kbd>.
"Generar documento" aparece, etiquetado con tu aplicación:

<Frame caption="El comando se muestra cuando se selecciona una Persona.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty-claude-cool-pascal-5ay683/images/docs/desarrolladores/extends/apps/document-generator/06-command-menu.png" alt="Menú de comandos con Generar documento" />
</Frame>

Ejecutarlo — su componente se abre en el panel lateral. Elige una plantilla, haz clic en
**Generar**, y un nuevo registro de tierras en **Documentos**.

<Frame caption="El componente frontal, cargando plantillas y generando al hacer clic.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty-claude-cool-pascal-5ay683/images/docs/desarrolladores/extends/apps/document-generator/06b-front-component.png" alt="Generar panel lateral del documento" />
</Frame>

Cada documento generado registra tu aplicación como su autor:

<Frame caption="Creado por Document Generator, estado Generado.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty-claude-cool-pascal-5ay683/images/docs/desarrolladores/extends/apps/document-generator/05-document-record.png" alt="Un registro de documento generado" />
</Frame>

## Vista previa de un documento en su página de registro

Un componente frontal no solo para los menús de comandos: puedes montar uno como una \*\*pestaña en una página de registro
. Añadamos una pestaña de *Vista previa* al registro de documentos que renderiza el cuerpo de Markdown
como una página pulida e imprimible.

El componente lee el id de registro actual de su contexto de ejecución, carga el documento
y lo renderiza. Los componentes de Front se ejecutan en una **sandbox** que solo permite una
lista blanca de etiquetas HTML: la inyección de HTML sin procesar (`dangerouslySetInnerHTML`) y
`\<style>` están bloqueados, por lo que representamos el Markdown como elementos de React con estilos
inline mediante un pequeño helper [`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 { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';

const DocumentViewer = () => {
  const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
  // ...load { content, file } for recordId, then derive the links:
  const pdfUrl = document.file?.[0]?.url;
  const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
    query: { id: recordId },
  });

  // Render the template body, plus quick links to the web page and the PDF.
  // Links open in a new tab so they don't navigate the embedded component.
  return (
    <div style={styles.scroll}>
      <div style={styles.actions}>
        <a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
          Open web page
        </a>
        {pdfUrl ? (
          <a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
            Download PDF
          </a>
        ) : null}
      </div>
      <div style={styles.paper}>
        <div style={styles.body}>
          <Markdown content={document.content} />
        </div>
      </div>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
  name: 'document-viewer',
  component: DocumentViewer,
});
```

Montarlo con un [diseño de página](/l/es/developers/extend/apps/layout/page-layouts). Un diseño de
`RECORD_PAGE` agrega pestañas a la vista de registro de un objeto; una pestaña con un solo widget
`FRONT_COMPONENT` aloja el componente a todo lo ancho:

```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.VERTICAL_LIST,
    widgets: [{
      universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
      title: 'Document preview',
      type: 'FRONT_COMPONENT',
      configuration: {
        configurationType: 'FRONT_COMPONENT',
        frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
      },
    }],
  }],
});
```

Abre cualquier documento — una pestaña de **Vista previa** lo renderiza hermosamente, con enlaces a la página web compartible
y el PDF:

<Frame caption="La pestaña Vista previa muestra el documento con estilos en línea, además de enlaces rápidos.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty-claude-cool-pascal-5ay683/images/docs/desarrolladores/extends/apps/document-generator/09-document-viewer.png" alt="Componente frontal del visor de documentos en una pestaña de página de registro" />
</Frame>

## Editar una plantilla con el editor de texto

Las plantillas no necesitan ningún componente personalizado. Porque el `body` es un campo
`RICH_TEXT`, Veinte ya proporciona un editor de texto completo para él — el mismo
que los objetos estándar de Nota y Task usan. Acabamos de superarlo en la página de registro
plantilla.

Añade una pestaña con un widget `FIELD` en el modo de visualización `EDITOR`, apuntando al campo `body`
a través de `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',
    position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
    configuration: {
      configurationType: 'FIELD',
      fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
      fieldDisplayMode: 'EDITOR',
    },
  }],
}
```

Un campo `RICH_TEXT` almacena tanto el bloque JSON del editor como una proyección
de Markdown. La generación de pipeline lee que Markdown proyecta, así que
marcadores de posición, el PDF, y la página web compartible siguen funcionando sin cambios —
vea el
completo [`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).
Los widgets de `FIELD` también admiten otros modos de visualización y opciones, incluido
listar registros de una relación anidada a dos saltos de distancia — consulta la
[referencia del widget de campo](/l/es/developers/extend/apps/layout/page-layouts#field-widgets).
Ahora editores escriben plantillas en un editor de texto rich:

<Frame caption="La pestaña Plantilla: Editor nativo de texto de texto de 20 años vinculado al campo del cuerpo.">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty-claude-cool-pascal-5ay683/images/docs/desarrolladores/extends/apps/document-generator/10-template-editor.png" alt="Registro de plantillas con la pestaña nativa del editor de texto" />
</Frame>

**Después de este paso:** vista previa de documentos hermosamente y plantillas son editables
en la aplicación. A continuación, deja que un agente de IA los genere a partir de un chat.

<Card title="Siguiente: un agente de IA →" icon="robot" href="/Developopers/extend/apps/tutorials/document-generator/ai-agent">
  Añade un agente y una habilidad que llame a tu herramienta.
</Card>
