メインコンテンツへスキップ
現在、オブジェクトは設定を通じてのみアクセス可能です。 アプリに本物の存在感を持たせて、UI にしっかり組み込みましょう。リストビュー、サイドバーエントリ、ワンクリックで実行できる Generate document コマンド、レコードページのフロントコンポーネントによるドキュメントのプレビュー、そしてテンプレート用のネイティブなリッチテキストエディタータブを追加します。

ビューとナビゲーション

view は、指定されたオブジェクトのリストです。 navigation menu item はそのビューをサイドバーに配置します。
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,
});
テンプレートに同じペアを追加します。 両方ともサイドバーに表示されます:
生成されたドキュメントを持つドキュメントビュー

フロントコンポーネント

front component は Twenty 内でサンドボックス化された React コンポーネントです。 選択したレコードを読み込み、CoreApiClient 経由で person テンプレートを読み込み、最後の チャプターからルートに POST を読み込みます。
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,
});
インラインCSS変数(var(--t-color-blue))のスタイルで、 21uiからインポートされた値ではありません。 ビルド中にSDKがパッケージをモックするので、 テーマ定数のモジュールレベルのインポートはundefinedになります。 full componentを参照してください。

開くためのコマンド

availabilityType: 'RECORD_SELECTION' を指定した command menu item は、Person が選択されているときに表示され、サイドパネルでコンポーネントを開きます。
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,
});

フロー全体を試してみてください

Peopleを開き、 ⌘K / Ctrl K を押します。 アプリでタグ付けされた「ドキュメントの生成」が表示されます:
ドキュメントを生成するコマンドメニュー
実行: コンポーネントがサイドパネルで開きます。 テンプレートを選択し、 生成をクリックし、ドキュメントに新しいレコードの土地を追加します。
ドキュメントサイドパネルを生成する
生成されたすべてのドキュメントはアプリを作成者として記録します:
生成されたドキュメント レコード

レコードページにドキュメントをプレビュー

フロントコンポーネントはコマンドメニューだけではありません — レコードページにタブとしてマウントできます。 Markdown 本文を洗練された印刷可能なページとしてレンダリングするドキュメントレコードに Preview タブを追加しましょう。 コンポーネントは現在のレコード ID を実行コンテキストから読み込み、 ドキュメントを読み込み、レンダリングします。 フロントコンポーネントは、HTMLタグの ホワイトリストのみを許可するサンドボックスで実行されます。生のHTMLインジェクション (dangerouslySetInnerHTML) と \<style>はブロックされているため、小さな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,
});
page layout でマウントします。 RECORD_PAGE レイアウトは、オブジェクトのレコードビューにタブを追加します。CANVAS タブの FronT_COMPONENT ウィジェットはコンポーネントをホストします。
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,
      },
    }],
  }],
});
プレビュータブを開くと、 共有可能なウェブページとPDFへのリンクが美しくレンダリングされます。
レコードページタブ内のドキュメントビューアフロントコンポーネント

リッチテキストエディタでテンプレートを編集

テンプレートにはカスタムコンポーネントは一切必要ありません。 bodyRICH_TEXT フィールドなので、Twenty にはすでにフル機能のリッチテキストエディターが用意されています。標準の Note および Task オブジェクトが使用しているものと同じエディターです。 テンプレートのレコードページでサーフェスを作成します。 EDITORディスプレイモードでFIELDウィジェットを持つタブを追加します。fieldMetadataIdを介してbody フィールドを指しています:
{
  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',
    },
  }],
}
RICH_TEXT フィールドには、エディタのブロック JSON と Markdown プロジェクションの両方が格納されます。 生成パイプラインはその Markdown プロジェクションを読み取るため、プレースホルダー、PDF、共有可能なウェブページはすべてそのまま問題なく動作します。詳しくは、template-record.page-layout.ts を参照してください。 編集者は適切なリッチテキストエディタでテンプレートを書き込むようになりました:
ネイティブリッチテキストエディタタブのテンプレートレコード
このステップの後: ドキュメントは美しくプレビューでき、テンプレートはアプリ内で編集可能になります。 次に、AIエージェントがチャットからそれらを生成します。

次へ: AI エージェント →

エージェントとツールと呼ぶスキルを追加します。