Files
inkreach-teable-cli/skills/inkreach-teable-plugin/dynamic-plugins/dev-workflow.md
T
yeuimu e09ed1bc0f feat: 收录 inkreach-teable-plugin skill;install.sh 安装全部技能
skills/ 现包含 inkreach-teable + inkreach-teable-plugin,
npx skills add 一并安装两个;install.sh 改为遍历安装 skills/*。
2026-09-23 06:23:46 +00:00

6.8 KiB

Plugin development workflow

The standard recipe for creating, building, and deploying a dynamic React component plugin. Each plugin is a standalone webpack project that outputs an IIFE bundle.

Output contract

Every plugin task ends with:

  1. A built IIFE bundle (dist/main.js + optional dist/styles.css)
  2. A plugin.json manifest
  3. Uploaded to the host via POST /api/plugin/:id/upload-bundle
  4. Enabled via PUT /api/base/:baseId/plugin-config/:pluginId
  5. Installed to the target position (the install API varies by position)

Step 1 — Understand the request

Clarify what the user wants, then pick the position using overview.md. State the choice: "This is a ContextMenu plugin because..."

Step 2 — Scaffold the plugin project

The plugin-cli lives in the repo at packages/plugin-cli/ (not yet published to npm). Run it directly:

cd /tmp
node /root/deployments/inkreach-teable/packages/plugin-cli/dist/index.js create my-plugin
cd my-plugin
pnpm install

This generates:

my-plugin/
├── src/
│   └── index.tsx          # entry: calls definePlugin()
├── plugin.json            # manifest
├── webpack.config.js      # pre-configured externals
├── tsconfig.json
└── package.json

If the CLI doesn't work for some reason, manually create the structure by copying plugins/batch-edit-pro/ as a template.

Step 3 — Write the plugin code

Edit src/index.tsx. The shape every plugin follows:

const React = window.__TEABLE_HOST__.React;
const host = window.__TEABLE_HOST__;
const { useFields, useTableId, useViewId } = host.SDK;
const { Button } = host.UILib;
const { getRecords } = host.OpenAPI;
const { definePlugin, useSelection, getSelectionRecords } = host.PluginSDK;

function MyComponent() {
  const tableId = useTableId();
  const fields = useFields();
  // ... business logic
  return React.createElement('div', null, 'Hello from plugin!');
}

definePlugin({
  id: 'my-plugin',
  name: 'My Plugin',
  version: '1.0.0',
  minAppVersion: '1.0.0',
  onLoad(ctx) {
    ctx.addContextMenuAction('myAction', MyComponent);
    // or: ctx.addPanel('myPanel', MyComponent);
    // or: ctx.addDashboardWidget('myWidget', MyComponent);
    // or: ctx.addView('myView', MyComponent);
    // or: ctx.addRoute('/my-page', MyComponent);
  },
  onUnload(ctx) {
    // Optional cleanup; ctx auto-disposes registered resources
  },
});

Rules

  • Never hardcode table IDs, view IDs, or tokens — use useTableId(), useViewId(), and the event bus for contextual data.
  • Use React.createElement or JSX (webpack handles transpilation). The host's React instance is the single source of truth.
  • Loading / error / empty states are required. Use useState to manage async data loading and surface errors in the UI.
  • For contextMenu plugins that use getSelectionRecords, handle the case where nothing is selected (it returns an empty array).
  • Plug in to events for reactivity: ctx.on('selection.changed', ...), ctx.on('record.updated', ...).

Editable plugin.json

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "minAppVersion": "1.0.0",
  "entry": "main.js",
  "style": "styles.css",
  "positions": ["contextMenu"],
  "permissions": ["data.read", "data.write"],
  "author": "developer@example.com",
  "icon": "Pencil"
}
  • id must be unique, lowercase-kebab-case.
  • icon is a lucide-react icon name (optional).
  • positions must include the position you register in onLoad.

Step 4 — Build

NODE_OPTIONS='--max-old-space-size=2048' npx webpack --config webpack.config.js

Or if the plugin has a build script:

pnpm build
# or: npm run build

Outputs: dist/main.js (IIFE) + dist/styles.css (if styles exist).

Verify the output:

ls -la dist/main.js dist/styles.css plugin.json

Step 5 — Upload the bundle

The upload URL includes the plugin's id from plugin.json. Extract it first, then upload.

SESSION=$(grep auth_session /tmp/cookie_hr.txt 2>/dev/null | awk '{print $NF}')
if [ -z "$SESSION" ]; then
  SESSION=$(curl -s -c - http://localhost:3003/api/auth/signin \
    -X POST -H 'Content-Type: application/json' \
    -d '{"email":"hr_admin@xingchen.tech","password":"Xingchen@2026"}' \
    | grep auth_session | awk '{print $NF}')

> Account note (2026-08): `hr_admin@xingchen.tech` no longer exists on the
> deployed instance. Create a throwaway user via `POST /api/auth/signup`
> and add a `plugin_developer` row for it before uploading (see SKILL.md).
fi

PLUGIN_ID=$(python3 -c "import json; print(json.load(open('plugin.json'))['id'])")

curl -s -b "auth_session=$SESSION" \
  -F "js=@dist/main.js" \
  -F "css=@dist/styles.css" \
  -F "manifest=@plugin.json" \
  "http://localhost:3003/api/plugin/$PLUGIN_ID/upload-bundle"

echo "PLUGIN_ID=$PLUGIN_ID"
  • The plugin id in the URL must match the id in plugin.json. On first upload, the plugin is created; on subsequent uploads, the existing plugin is updated.
  • Auth supports both session cookies (auth_session) and Personal Access Tokens (Authorization: Bearer <PAT>).
  • Returns 201 on success.

Step 6 — Enable (REQUIRED before install)

curl -s -b "auth_session=$SESSION" -X PUT \
  "http://localhost:3003/api/base/bseAi6Hazmw7jPWZbfO/plugin-config/$PLUGIN_ID" \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true,"tableIds":[]}'

tableIds:[] = base-wide; tableIds:["tblX"] = per-table. Use base-wide unless the user wants to restrict to specific tables.

Step 7 — Install to the position

The install API depends on position. See deployment.md for the exact call.

Example — contextMenu on the test table:

curl -s -b "auth_session=$SESSION" -X POST \
  "http://localhost:3003/api/table/tbldtYd4ZhvkcyAaada/plugin-context-menu/install" \
  -H 'Content-Type: application/json' \
  -d "{\"pluginId\":\"$PLUGIN_ID\",\"name\":\"My Plugin\"}"

Step 8 — Verify in the browser

Open the relevant page and confirm the plugin renders and works:

  • ContextMenu: right-click a record in grid view → "Plugins" group
  • Panel: bottom-right Plugins button in a table view
  • View: view-type dropdown
  • Dashboard: space dashboard page

Use browser-act to verify or ask the user to check.

Pre-deliver checklist

Before claiming done:

  • dist/main.js built and is a valid IIFE bundle
  • plugin.json at project root with correct id, entry, positions
  • No hardcoded URLs/table IDs/tokens — all via host SDK
  • Loading, error, and empty states handled in the UI
  • definePlugin() called with onLoad(ctx) that registers at least one component
  • Upload returns 201, pluginId captured
  • Enable (plugin-config) called before install
  • Install returns 2xx
  • Plugin renders and works

If any box is unchecked, fix it.