Saltar al contenido principal
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 es una lista guardada de un objeto dado. Un elemento del menú de navegación pone esa vista en la barra lateral.
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,
});
Añadir el mismo par para las plantillas. Ambos ahora se muestran en la barra lateral:
Vista de documentos con un documento generado

Un componente frontal

Un componente delantero 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 .
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,
});
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.

Un comando para abrirlo

Un elemento del menú de comandos con availabilityType: 'RECORD_SELECTION' aparece cuando se selecciona una Persona, y abre el componente en el panel lateral.
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 mañK / Ctrl K. “Generar documento” aparece, etiquetado con tu aplicación:
Menú de comandos con Generar documento
Ejecutarlo — su componente se abre en el panel lateral. Elige una plantilla, haz clic en Generar, y un nuevo registro de tierras en Documentos.
Generar panel lateral del documento
Cada documento generado registra tu aplicación como su autor:
Un registro de documento generado

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.
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,
});
Montarlo con un diseño de página. Un diseño RECORD_PAGE añade pestañas a la vista de registro de un objeto; un widget FRONT_COMPONENT en una pestaña CANVAS aloja el componente:
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,
      },
    }],
  }],
});
Abre cualquier documento — una pestaña de Vista previa lo renderiza hermosamente, con enlaces a la página web compartible y el PDF:
Componente frontal del visor de documentos en una pestaña de página de registro

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:
{
  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 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. Ahora editores escriben plantillas en un editor de texto rich:
Registro de plantillas con la pestaña nativa del editor de texto
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.

Siguiente: un agente de IA →

Añade un agente y una habilidad que llame a tu herramienta.