Salt la conținutul principal
Î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 este o listă salvată a unui obiect dat. Un element meniu de navigare introduce această vizualizare în bara 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,
});
Adaugă aceeași pereche pentru șabloane. Ambele sunt prezentate acum pe partea laterală:
Vizualizare documente cu un document generat

O componentă față

O componentă frontală 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 .
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,
});
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ă.

O comandă pentru a o deschide

Un element meniu de comandă cu `availabilityType: ‘RECORD_SELECTION” apare atunci când o Persoană este selectată, și deschide componenta în panoul 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,
});

Încercați întregul flux

Deschideți oameni, bifați o persoană și apăsați £K / Ctrl K. “Generează document” apare, etichetat cu aplicația ta:
Meniul de comenzi cu Generarea documentului
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.
Generare panou de document
Fiecare document generat înregistrează aplicația dvs. ca autor:
Înregistrare de documente generată

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 hel.
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ă. Un layout RECORD_PAGE adaugă file la vizualizarea unui obiect de înregistrare; un widget FRONT_COMPONENT într-o filă CANVAS găzduieşte componenta:
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:
Vizualizator document componenta frontală într-o filă de înregistrare pagină

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:
{
  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. Editorii scriu șabloane într-un adevărat editor de text bogat:
Șablon de înregistrare cu fila nativă de editor de text bogat
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.

Următorul: un agent AI →

Adăugați un agent și o abilitate care vă sună unelta.