フロントエンドとバックエンドが連携するデータ管理プラグインを作る

これまでの例は純粋なクライアントサイド(ブロック、フィールド、操作)か、クライアント + シンプルなインターフェース(設定ページ)でした。この例ではより完全なシーンを示します。サーバーサイドでデータテーブルを定義し、クライアントで TableBlockModel を継承して完全なテーブル機能を取得し、さらにカスタムフィールドコンポーネントとカスタム操作ボタンを追加して、CRUD を備えたデータ管理プラグインを構成します。

この例は、これまでに学んだブロック、フィールド、操作をまとめて、完全なプラグインの開発フローを示します。

前提知識

以下の内容を事前に理解しておくと、開発がスムーズになります:

最終的な効果

「ToDo」データ管理プラグインを作ります。以下の機能を含みます:

  • サーバーサイドで todoItems データテーブルを定義し、プラグインインストール時にサンプルデータを自動投入
  • クライアントで TableBlockModel を継承し、すぐに使えるテーブルブロック(フィールド列、ページネーション、操作バーなど)
  • カスタムフィールドコンポーネント — カラー Tag で priority フィールドをレンダリング
  • カスタム操作ボタン — 「新規 ToDo」ボタンをクリックするとダイアログでフォーム入力してレコードを作成

完全なソースコードは @nocobase-example/plugin-custom-table-block-resource を参照してください。ローカルで動作確認したい場合:

yarn pm enable @nocobase-example/plugin-custom-table-block-resource

以下、ゼロからこのプラグインを構築していきます。

ステップ1:プラグインスケルトンの作成

リポジトリのルートで実行します:

yarn pm create @my-project/plugin-custom-table-block-resource

詳しくははじめてのプラグインを書くをご覧ください。

ステップ2:データテーブルの定義(サーバーサイド)

src/server/collections/todoItems.ts を新規作成します。NocoBase はこのディレクトリ配下の collection 定義を自動的に読み込みます:

// src/server/collections/todoItems.ts
import { defineCollection } from '@nocobase/database';

export default defineCollection({
  name: 'todoItems',
  title: 'Todo Items',
  fields: [
    { name: 'title', type: 'string', title: 'Title' },
    {
      name: 'completed',
      type: 'boolean',
      title: 'Completed',
      defaultValue: false,
    },
    {
      name: 'priority',
      type: 'string',
      title: 'Priority',
      defaultValue: 'medium',
    },
  ],
});

設定ページの例とは異なり、ここでは手動で resource を登録する必要はありません。NocoBase が各 collection に対して標準的な CRUD インターフェース(listgetcreateupdatedestroy)を自動生成します。

ステップ3:権限設定とサンプルデータ(サーバーサイド)

src/server/plugin.ts を編集し、load() で ACL 権限を設定、install() でサンプルデータを投入します:

// src/server/plugin.ts
import { Plugin } from '@nocobase/server';

export class PluginDataBlockServer extends Plugin {
  async load() {
    // ログインユーザーが todoItems の CRUD を実行可能
    this.app.acl.allow('todoItems', ['list', 'get', 'create', 'update', 'destroy'], 'loggedIn');
  }

  async install() {
    // プラグインの初回インストール時にサンプルデータを投入
    const repo = this.db.getRepository('todoItems');
    const count = await repo.count();
    if (count === 0) {
      await repo.createMany({
        records: [
          { title: 'Learn NocoBase plugin development', completed: true, priority: 'high' },
          { title: 'Build a custom block', completed: false, priority: 'high' },
          { title: 'Write documentation', completed: false, priority: 'medium' },
          { title: 'Add unit tests', completed: false, priority: 'low' },
        ],
      });
    }
  }
}

export default PluginDataBlockServer;

重要なポイント:

  • acl.allow()['list', 'get', 'create', 'update', 'destroy'] で完全な CRUD 権限を開放、'loggedIn' はログインユーザーがアクセス可能であることを意味
  • install() — プラグインの初回インストール時にのみ実行され、初期データの投入に適している
  • this.db.getRepository() — collection 名でデータ操作オブジェクトを取得
  • resourceManager.define() は不要 — NocoBase が collection に対して CRUD インターフェースを自動生成

ステップ4:ブロックモデルの作成(クライアント)

src/client-v2/models/TodoBlockModel.tsx を新規作成します。TableBlockModel を継承すると、完全なテーブルブロック機能がすぐに使えます。フィールド列、操作バー、ページネーション、ソートなどが含まれ、renderComponent を自分で書く必要はありません。

20260408164204

ヒント

実際のプラグイン開発では、TableBlockModel のカスタマイズが不要な場合、このブロックを継承・登録する必要はなく、ユーザーにブロック追加時に「テーブル」を選択してもらえば十分です。この記事ではブロックモデルの定義と登録のフローを示すために、TodoBlockModelTableBlockModel を継承しています。TableBlockModel がその他すべて(フィールド列、操作バー、ページネーションなど)を処理します。

// src/client-v2/models/TodoBlockModel.tsx
import { TableBlockModel } from '@nocobase/client-v2';
import type { Collection } from '@nocobase/flow-engine';
import { tExpr } from '../locale';

export class TodoBlockModel extends TableBlockModel {
  // todoItems データテーブルでのみ使用可能に制限
  static filterCollection(collection: Collection) {
    return collection.name === 'todoItems';
  }
}

TodoBlockModel.define({
  label: tExpr('Todo block'),
});

filterCollection でこのブロックを todoItems データテーブルでのみ使用可能に制限しています。ユーザーが「Todo block」を追加する際、データテーブル選択リストには todoItems のみが表示され、関係のないテーブルは表示されません。

20260408170026

ステップ5:カスタムフィールドコンポーネントの作成(クライアント)

src/client-v2/models/PriorityFieldModel.tsx を新規作成します。カラー Tag で priority フィールドをレンダリングし、プレーンテキストよりも直感的にします:

20260408163645

// src/client-v2/models/PriorityFieldModel.tsx
import React from 'react';
import { ClickableFieldModel } from '@nocobase/client-v2';
import { DisplayItemModel } from '@nocobase/flow-engine';
import { Tag } from 'antd';
import { tExpr } from '../locale';

const priorityColors: Record<string, string> = {
  high: 'red',
  medium: 'orange',
  low: 'green',
};

export class PriorityFieldModel extends ClickableFieldModel {
  public renderComponent(value: string) {
    if (!value) return <span>-</span>;
    return <Tag color={priorityColors[value] || 'default'}>{value}</Tag>;
  }
}

PriorityFieldModel.define({
  label: tExpr('Priority tag'),
});

// input(単行テキスト)タイプのフィールドインターフェースにバインド
DisplayItemModel.bindModelToInterface('PriorityFieldModel', ['input']);

登録後、テーブルの priority 列の設定で、「フィールドコンポーネント」ドロップダウンから「Priority tag」に切り替えられます。

ステップ6:カスタム操作ボタンの作成(クライアント)

src/client-v2/models/NewTodoActionModel.tsx を新規作成します。「新規 ToDo」ボタンをクリックすると、ctx.viewer.dialog() でダイアログを開き、フォーム入力後にレコードを作成します:

20260408163810

// src/client-v2/models/NewTodoActionModel.tsx
import React from 'react';
import { ActionModel, ActionSceneEnum } from '@nocobase/client-v2';
import { MultiRecordResource, observable, observer } from '@nocobase/flow-engine';
import { Button, Form, Input, Select, Space, Switch } from 'antd';
import { ButtonProps } from 'antd';
import { tExpr } from '../locale';

// observable でローディング状態を管理。useState の代わり
const formState = observable({
  loading: false,
});

// ダイアログ内のフォームコンポーネント。observer でラップして observable の変化に応答
const NewTodoForm = observer(function NewTodoForm({
  onSubmit,
  onCancel,
}: {
  onSubmit: (values: any) => Promise<void>;
  onCancel: () => void;
}) {
  const [form] = Form.useForm();

  const handleSubmit = async () => {
    const values = await form.validateFields();
    formState.loading = true;
    try {
      await onSubmit(values);
    } finally {
      formState.loading = false;
    }
  };

  return (
    <Form form={form} layout="vertical" initialValues={{ priority: 'medium', completed: false }}>
      <Form.Item label="Title" name="title" rules={[{ required: true, message: 'Please enter title' }]}>
        <Input placeholder="Enter todo title" />
      </Form.Item>
      <Form.Item label="Priority" name="priority">
        <Select
          options={[
            { label: 'High', value: 'high' },
            { label: 'Medium', value: 'medium' },
            { label: 'Low', value: 'low' },
          ]}
        />
      </Form.Item>
      <Form.Item label="Completed" name="completed" valuePropName="checked">
        <Switch />
      </Form.Item>
      <Form.Item>
        <Space>
          <Button type="primary" onClick={handleSubmit} loading={formState.loading}>
            OK
          </Button>
          <Button onClick={onCancel}>Cancel</Button>
        </Space>
      </Form.Item>
    </Form>
  );
});

export class NewTodoActionModel extends ActionModel {
  static scene = ActionSceneEnum.collection;

  defaultProps: ButtonProps = {
    type: 'primary',
    children: tExpr('New todo'),
  };
}

NewTodoActionModel.define({
  label: tExpr('New todo'),
});

NewTodoActionModel.registerFlow({
  key: 'newTodoFlow',
  title: tExpr('New todo'),
  on: 'click', // ボタンクリックイベントをリッスン
  steps: {
    openForm: {
      async handler(ctx) {
        const resource = ctx.blockModel?.resource as MultiRecordResource;
        if (!resource) return;

        // ctx.viewer.dialog でダイアログを開く
        ctx.viewer.dialog({
          content: (view) => (
            <NewTodoForm
              onSubmit={async (values) => {
                await resource.create(values);
                ctx.message.success(ctx.t('Created successfully'));
                view.close();
              }}
              onCancel={() => view.close()}
            />
          ),
        });
      },
    },
  },
});

重要なポイント:

  • ActionSceneEnum.collection — ボタンがブロック上部の操作バーに表示
  • on: 'click'registerFlow でボタンの click イベントをリッスン
  • ctx.viewer.dialog() — NocoBase 組み込みのダイアログ機能。content は関数を受け取り、引数 view から view.close() でダイアログを閉じることが可能
  • resource.create(values) — データテーブルの create インターフェースを呼び出してレコードを作成。作成後、テーブルが自動更新
  • observable + observer — flow-engine が提供するリアクティブ状態管理で useState を置き換え。コンポーネントが formState.loading の変化に自動的に応答

ステップ7:多言語ファイルの追加

プラグインの src/locale/ 配下の翻訳ファイルを編集します:

// src/locale/zh-CN.json
{
  "Todo block": "待办事项区块",
  "Priority tag": "优先级标签",
  "New todo": "新建待办",
  "Todo form": "待办表单",
  "Title": "标题",
  "Priority": "优先级",
  "Completed": "已完成",
  "Created successfully": "创建成功"
}
// src/locale/en-US.json
{
  "Todo block": "Todo block",
  "Priority tag": "Priority tag",
  "New todo": "New todo",
  "Todo form": "Todo form",
  "Title": "Title",
  "Priority": "Priority",
  "Completed": "Completed",
  "Created successfully": "Created successfully"
}
注意

初めて言語ファイルを追加した場合、アプリの再起動が必要です。

翻訳ファイルの書き方と tExpr() のその他の使い方については、i18n 国際化をご覧ください。

ステップ8:プラグインへの登録(クライアント)

src/client-v2/plugin.tsx を編集します。2つのことが必要です:モデルの登録と、todoItems のクライアントデータソースへの登録。

注意

プラグインコード内で addCollection を使ってデータテーブルを手動登録するのは稀な方法です。ここではフロントエンド・バックエンド連携の完全なフローをデモするためだけに行っています。実際のプロジェクトでは、データテーブルは通常 NocoBase の画面上で作成・設定するか、API / MCP などで管理するため、プラグインのクライアントコードで明示的に登録する必要はありません。

defineCollection で定義したテーブルはサーバー内部テーブルで、デフォルトではブロックのデータテーブル選択リストに表示されません。addCollection で手動登録すると、ユーザーがブロック追加時に todoItems を選択できるようになります。

20260408164023

// src/client-v2/plugin.tsx
import { Plugin } from '@nocobase/client-v2';

const todoItemsCollection = {
  name: 'todoItems',
  title: 'Todo Items',
  // filterTargetKey は必須。設定しないと collection がブロックのデータテーブル選択リストに表示されない
  filterTargetKey: 'id',
  fields: [
    {
      type: 'bigInt',
      name: 'id',
      primaryKey: true,
      autoIncrement: true,
      interface: 'id',
    },
    {
      type: 'string',
      name: 'title',
      interface: 'input',
      uiSchema: { type: 'string', title: 'Title', 'x-component': 'Input' },
    },
    {
      type: 'boolean',
      name: 'completed',
      interface: 'checkbox',
      uiSchema: { type: 'boolean', title: 'Completed', 'x-component': 'Checkbox' },
    },
    {
      type: 'string',
      name: 'priority',
      interface: 'input',
      uiSchema: { type: 'string', title: 'Priority', 'x-component': 'Input' },
    },
  ],
};

export class PluginCustomTableBlockResourceClientV2 extends Plugin {
  async load() {
    // ブロック、フィールド、操作モデルの登録
    this.flowEngine.registerModelLoaders({
      TodoBlockModel: {
        loader: () => import('./models/TodoBlockModel'),
      },
      PriorityFieldModel: {
        loader: () => import('./models/PriorityFieldModel'),
      },
      NewTodoActionModel: {
        loader: () => import('./models/NewTodoActionModel'),
      },
    });

    // todoItems をクライアントサイドのデータソースに登録。
    // ensureLoaded() が load() の後に実行され、setCollections() で
    // すべての collection をクリアして再設定するため、'dataSource:loaded' イベントを
    // リッスンする必要がある。イベントコールバック内で再登録することで
    // addCollection がリロード後も維持される。
    const addTodoCollection = () => {
      const mainDS = this.flowEngine.dataSourceManager.getDataSource('main');
      if (mainDS && !mainDS.getCollection('todoItems')) {
        mainDS.addCollection(todoItemsCollection);
      }
    };

    this.app.eventBus.addEventListener('dataSource:loaded', (event: Event) => {
      if ((event as CustomEvent).detail?.dataSourceKey === 'main') {
        addTodoCollection();
      }
    });
  }
}

export default PluginCustomTableBlockResourceClientV2;

重要なポイント:

  • registerModelLoaders — 遅延読み込みで3つのモデル(ブロック、フィールド、操作)を登録
  • this.app.eventBus — アプリケーションレベルのイベントバス、ライフサイクルイベントの監視に使用
  • dataSource:loaded イベント — データソースの読み込み完了後にトリガー。このイベントのコールバック内で addCollection を呼び出す必要がある。ensureLoaded()load() の後に実行され、すべての collection をクリアして再設定するため、load() 内で直接 addCollection を呼ぶと上書きされてしまう
  • addCollection() — collection をクライアントデータソースに登録。フィールドには interfaceuiSchema プロパティが必要で、NocoBase がレンダリング方法を判断できるようにする
  • filterTargetKey: 'id' — 必須設定。レコードの一意識別に使うフィールド(通常は主キー)を指定。設定しないと collection がブロックのデータテーブル選択リストに表示されない
  • サーバーの defineCollection は物理テーブルと ORM マッピングの作成を担当し、クライアントの addCollection は UI にテーブルの存在を知らせる。両方が連携して初めてフロントエンド・バックエンド連携が完成する

ステップ9:プラグインの有効化

yarn pm enable @my-project/plugin-custom-table-block-resource

有効化後:

  1. 新しいページを作成し、「ブロックの追加」をクリックして「Todo block」を選択、todoItems データテーブルにバインド
  2. テーブルが自動的にデータを読み込み、フィールド列、ページネーションなどを表示
  3. 「操作の設定」で「New todo」ボタンを追加し、クリックするとダイアログでフォーム入力してレコードを作成
  4. priority 列の「フィールドコンポーネント」で「Priority tag」に切り替えると、priority がカラー Tag で表示

完全なソースコード

まとめ

この例で使用した機能:

機能使い方ドキュメント
データテーブル定義defineCollection()サーバーサイド → Collections データテーブル
権限制御acl.allow()サーバーサイド → ACL アクセス制御
初期データinstall() + repo.createMany()サーバーサイド → Plugin プラグイン
テーブルブロックTableBlockModelFlowEngine → ブロック拡張
クライアントデータテーブル登録addCollection() + eventBus + filterTargetKeyPlugin プラグイン
カスタムフィールドClickableFieldModel + bindModelToInterfaceFlowEngine → フィールド拡張
カスタム操作ActionModel + registerFlow({ on: 'click' })FlowEngine → 操作拡張
ダイアログctx.viewer.dialog()Context → 共通機能
リアクティブ状態observable + observerComponent コンポーネント開発
モデル登録this.flowEngine.registerModelLoaders()Plugin プラグイン
遅延翻訳tExpr()i18n 国際化

関連リンク