160 lines
9.8 KiB
Markdown
160 lines
9.8 KiB
Markdown
---
|
|
name: inkreach-teable-plugin
|
|
description: >-
|
|
Develop dynamic React component plugins for inkreach-teable — IIFE bundles
|
|
loaded at runtime via window.__TEABLE_HOST__, no iframe sandbox, full host
|
|
API access. Scaffold with plugin-cli, build with webpack, deploy via
|
|
upload-bundle API, register via definePlugin + PluginContext. Positions:
|
|
contextMenu, panel, view, dashboard, route. Use this whenever the user
|
|
mentions plugins, 插件, batch-edit, context-menu actions, panel widgets,
|
|
dashboard charts, custom views, plugin.json, definePlugin, bundle plugin,
|
|
plugin-cli, plugin-sdk, plugin-host, or window.__TEABLE_HOST__. Even if
|
|
the user just says "add a right-click action" or "make a side panel" or
|
|
"I need a dashboard widget" — or wants the native form embedded or
|
|
extended inside a plugin (原生表单, 表单注入, 表单虚拟字段, custom form
|
|
layout, NativeForm, NativeFormScope, form panel) — load this skill.
|
|
Reference example at `plugins/batch-edit-pro/` (on the deployment host: `/root/deployments/inkreach-teable/plugins/batch-edit-pro/`).
|
|
---
|
|
|
|
# inkreach-teable dynamic plugin development
|
|
|
|
This skill develops React component plugins that load at runtime without
|
|
rebuilding the host Next.js app. Plugins are IIFE bundles — they ship as plain
|
|
JS that executes in the host page and accesses host APIs through
|
|
`window.__TEABLE_HOST__`.
|
|
|
|
> **Contract**: A plugin = a webpack (or vite) IIFE bundle (`dist/main.js` +
|
|
> optional lazy chunks `*.chunk.js`, source maps `*.js.map`, `styles.css`) +
|
|
> a `plugin.json` manifest, uploaded to the host via
|
|
> `POST /api/plugin/:id/upload-bundle` (or `teable-plugin deploy`, which
|
|
> handles all assets). It runs in the same React tree as the host and has
|
|
> full access to the host's SDK, UI library, and OpenAPI helpers.
|
|
>
|
|
> **Dev loop**: `teable-plugin dev` (or `pnpm dev` in scaffolded projects)
|
|
> watch-builds and auto-uploads with a `devMode` manifest; the open host page
|
|
> remounts the plugin within ~4s without a page refresh. See
|
|
> `dynamic-plugins/dev-workflow.md`.
|
|
>
|
|
> **Full-page apps**: install at position `route` and the host serves the
|
|
> plugin on `/base/<baseId>/p/<pluginId>` (whole viewport, own router OK —
|
|
> HashRouter recommended).
|
|
>
|
|
> **Critical constraint**: no RUNTIME imports from `@teable/sdk`, `@teable/ui-lib`,
|
|
> `@teable/openapi` — a value import via monorepo aliases inlines the whole
|
|
> package source (via window-global externals it compiles to `undefined`).
|
|
> `import type { X } from '@teable/...'` IS fine (erased at compile time).
|
|
> `react`/`react-dom` may be imported when your webpack `externals` maps them
|
|
> to `window.__TEABLE_HOST__.React/ReactDOM` (see the plugin-showcase webpack
|
|
> config; `react/jsx-runtime` is intentionally bundled — element factories
|
|
> only, hooks stay on the host singleton). For everything else, read from the
|
|
> host global: `const { useFields } = window.__TEABLE_HOST__.SDK`. See
|
|
> `dynamic-plugins/troubleshooting.md` Pitfall 1 for details and symptoms.
|
|
|
|
## 0. Two efficiency rules (read first)
|
|
|
|
- **Trust these docs.** The `dynamic-plugins/` documents are the verified contract
|
|
for this deployment. Answer plugin capability/behavior questions directly from
|
|
them — do NOT re-derive behavior by spelunking `nestjs-backend/src` or the host
|
|
source tree; that burns minutes and adds nothing. Source diving is only for
|
|
debugging a concrete discrepancy the docs can't explain.
|
|
- **Don't upload demo plugins to verify knowledge.** For how-to/consultation
|
|
questions, a written answer (with code) is the deliverable. Only upload a bundle
|
|
when the user actually asked to deploy — test uploads belong in a throwaway
|
|
sandbox base, never the production plugin registry.
|
|
|
|
## 1. When to use this skill
|
|
|
|
- User says "写个插件 / 做个插件 / make a plugin" — yes.
|
|
- User wants a context-menu action (batch edit / process selected records) — yes.
|
|
- User wants a panel gadget (real-time stats / summary) — yes.
|
|
- User wants a custom view (kanban, gallery, timeline) — yes.
|
|
- User wants a dashboard chart — yes.
|
|
- User wants to embed / extend the NATIVE FORM in a plugin (原生表单, form
|
|
injection, virtual form fields, custom multi-step form with native field
|
|
editors, `host.NativeForm`, `NativeFormScope`) — yes, see `sdk-api.md`.
|
|
- User mentions `definePlugin`, `plugin-cli`, `plugin-sdk`, `window.__TEABLE_HOST__`, bundle plugin — yes.
|
|
- User wants to manage tables/automations (not plugins) — use `inkreach-teable` skill instead.
|
|
|
|
## 2. Module map (read in order when developing)
|
|
|
|
| Stage | File | Read it when... |
|
|
|---|---|---|
|
|
| 1. Understand architecture | `dynamic-plugins/overview.md` | always first — how `window.__TEABLE_HOST__`, PluginLoader, PluginSlot work |
|
|
| 2. Dev workflow | `dynamic-plugins/dev-workflow.md` | when building a plugin — the step-by-step recipe from scaffold to deploy |
|
|
| 3. SDK API | `dynamic-plugins/sdk-api.md` | when writing plugin logic — definePlugin, PluginContext, events, storage, useSelection, native form embedding (`NativeForm` Tier-1 injection / `NativeFormScope`+`useNativeForm` Tier-2 kit), NativeGrid |
|
|
| 3b. Native form quick answer | `dynamic-plugins/native-form.md` | **when the user ASKS whether/how to embed native field editors, custom form layout, or multi-step forms — read this FIRST and answer directly (consultative questions need no scaffold/build)** |
|
|
| 4. Deployment | `dynamic-plugins/deployment.md` | when uploading + enabling + authority matrix permissions |
|
|
| 5. Troubleshooting | `dynamic-plugins/troubleshooting.md` | before writing logic — common pitfalls (host globals, developer whitelist, AM, empty states, chunk-name flattening, terser banners) |
|
|
| Ref: plugin-showcase | `plugins/plugin-showcase/` | the v2 example — lazy chunks + source maps + Tailwind + HashRouter route app |
|
|
| Ref: batch-edit-pro | `plugins/batch-edit-pro/` | the classic example — single-file contextMenu plugin, full React + SDK |
|
|
|
|
## 3. Quick decision: which position?
|
|
|
|
| User wants | Position | `definePlugin` registration |
|
|
|---|---|---|
|
|
| Right-click action on records | **ContextMenu** | `ctx.addContextMenuAction(id, component)` |
|
|
| Side gadget while viewing a table | **Panel** | `ctx.addPanel(id, component)` |
|
|
| A new view type | **View** | `ctx.addView(id, component)` |
|
|
| Card on the space dashboard | **Dashboard** | `ctx.addDashboardWidget(id, component)` |
|
|
| A new page/route | **Route** | `ctx.addRoute(path, component)` |
|
|
|
|
## 4. Auth & test credentials
|
|
|
|
- Endpoint: `http://localhost:3003` (backend; `:3000` is taken by an unrelated
|
|
docker container on this host — see AGENTS.md), frontend `http://localhost:3002`.
|
|
- Plugin upload requires a `plugin_developer` row for the uploading user
|
|
(`INSERT INTO plugin_developer (id, user_id, email, created_time, created_by)
|
|
VALUES ('plgd<name>', '<userId>', '<email>', now(), '<userId>')`).
|
|
- **Sandbox base for any hands-on test**: create a throwaway base
|
|
(`POST /api/base` with `{"spaceId":"<spaceId>","name":"Sandbox(可删)"}`),
|
|
do all table/install testing inside it, and delete it (`DELETE /api/base/:id`)
|
|
when done. Never hardcode base/table IDs here — they rot; resolve them at
|
|
runtime (`inkreach-teable base list`) instead.
|
|
- Auth: PAT (`Authorization: Bearer <PAT>`; upload-bundle supports it) or
|
|
session cookie `auth_session`. Note: some admin-gated routes (e.g.
|
|
`DELETE /api/plugin/:id`) pass with an admin session but 403 with a plain PAT
|
|
even for the same user — fall back to the session for those.
|
|
|
|
## 5. Start here
|
|
|
|
For any plugin request, read `dynamic-plugins/overview.md` then
|
|
`dynamic-plugins/dev-workflow.md`. Reference `plugins/batch-edit-pro/` for a
|
|
complete working example.
|
|
|
|
## 6. Plugin backend services — MUST use the same-origin proxy (2026-08-21)
|
|
|
|
The instance is publicly served at `https://table.inkreach.cc` (frp + cloud Caddy);
|
|
browsers on the public internet CANNOT reach LAN addresses like
|
|
`http://192.168.124.92:8001`. Plugin bundles that need their own backend service
|
|
MUST call it through the session-gated dynamic proxy instead:
|
|
|
|
- **URL contract**: `fetch('/api/plugin-proxy/<service>/<path...>')` — same-origin,
|
|
works from LAN and public internet alike. NEVER hardcode LAN IPs / `localhost`
|
|
in a bundle.
|
|
- **Registry**: one line in `PLUGIN_API_SERVICES` (JSON, in
|
|
`apps/nextjs-app/.env` AND `.env.development`), then restart
|
|
`teable-backend.service`. No rebuild, no code change.
|
|
Example: `PLUGIN_API_SERVICES={"track":"http://127.0.0.1:8001"}`
|
|
- **Implementation**: NestJS controller
|
|
`apps/nestjs-backend/src/features/plugin/plugin-proxy.controller.ts`
|
|
(global AuthGuard gates login; upstream whitelist = registry; session cookies
|
|
are never forwarded upstream). Do NOT move this to a Next.js pages-router API
|
|
route — the `/api/:path*` rewrite in `next.config.js` outranks catch-all file
|
|
routes, and files outside `pages/api/` get compiled as SSR pages (observed on
|
|
Next 16.1.6/turbopack).
|
|
- **CSP**: same-origin fetches need nothing; truly external origins must be
|
|
declared in the plugin manifest `apiOrigins` (appended to connect-src by
|
|
`plugin-runtime.controller.ts`).
|
|
- Re-uploading a plugin zip OVERWRITES the bundle — re-check for hardcoded LAN
|
|
URLs afterwards (`grep -o 'http://192.168[^\"']*' <bundle>/main.js`).
|
|
Live example: `apps/nestjs-backend/.assets/plugins/bundle/track-panel/`
|
|
(patched from LAN URL to `/api/plugin-proxy/track`; backups kept as
|
|
`main.js.bak-*`).
|
|
- **Root fix (2026-08-22 recurrence)**: a developer re-uploaded track-panel at
|
|
02:23 with the LAN URL back in the source-built bundle, breaking public
|
|
search with a CORS error within hours of the last patch. Patching
|
|
`main.js` on disk only fixes the CURRENT artifact — the plugin SOURCE must
|
|
use `/api/plugin-proxy/track` before building/uploading, otherwise every
|
|
re-upload reintroduces the bug. The backend serves bundle files from disk
|
|
per-request, so a disk patch takes effect without a backend restart.
|