# Plugin SDK API Reference The SDK lives at `packages/plugin-sdk/` and is also exposed as `window.__TEABLE_HOST__.PluginSDK`. ## Registration — `definePlugin` The single entry point for every plugin. Must be called at the top level of the IIFE bundle. ```typescript definePlugin({ id: 'my-plugin', // unique, lowercase-kebab name: 'My Plugin', // display name version: '1.0.0', // semver minAppVersion: '1.0.0', // minimum host version required onLoad(ctx: IPluginContext) { // Register components, subscribe to events ctx.addContextMenuAction('myAction', MyComponent); ctx.on('selection.changed', (payload) => { console.log('Selection changed:', payload.selection); }); }, onUnload?(ctx: IPluginContext) { // Optional manual cleanup; ctx auto-disposes all registered resources }, }); ``` `onUnload` is optional — `ctx` tracks all registered components and event listeners and disposes them automatically on unload. ## PluginContext Available as `ctx` in `onLoad` and `onUnload`. ### `ctx.pluginId: string` The plugin's unique ID. ### `ctx.baseId: string` The current base ID. ### `ctx.tableId?: string` The current table ID (if the plugin is loaded in a table context). ### `ctx.viewId?: string` The current view ID (if available). ### Component Registration Each method takes a unique `id` (per-plugin scope) and a React component. Returns a `Disposable` that can be used to manually unregister before unload (rarely needed — ctx auto-disposes). ```typescript ctx.addContextMenuAction(id: string, component: React.FC): Disposable; ctx.addPanel(id: string, component: React.FC): Disposable; ctx.addDashboardWidget(id: string, component: React.FC): Disposable; ctx.addView(id: string, component: React.FC): Disposable; ctx.addRoute(id: string, component: React.FC): Disposable; ``` **Route position** (full-page apps, since 2026-10): the component renders at `/base//p/` — the whole main-area viewport inside the base layout, so SDK hooks like `host.SDK.useBaseId()` work. Install the plugin at position `"route"` (`POST /api/base/:baseId/plugin-installs`) and include `"route"` in `manifest.positions`. The plugin brings its own router — use HashRouter (e.g. `react-router-dom` bundled as a normal npm dep) so Next.js routing is untouched. Working example: `plugins/plugin-showcase/src/App.tsx`. ### Events #### `ctx.on(event, handler): Disposable` Subscribe to host events. Returns a `Disposable` that unsubscribes when called. All subscriptions are auto-disposed on unload. ```typescript // Subscribe const sub = ctx.on('selection.changed', (payload) => { console.log('Selection:', payload.selection); }); // Unsubscribe early if needed sub.dispose(); ``` #### `ctx.emit(event, ...args): void` Emit custom events to the host event bus. Other plugins can listen. ```typescript ctx.emit('my-custom-event', { data: 'hello' }); ``` ### Typed Events | Event | Payload | When emitted | |---|---|---| | `record.created` | `{ tableId, recordId }` | A record is created | | `record.updated` | `{ tableId, recordId, fieldIds }` | A record is updated | | `record.deleted` | `{ tableId, recordId }` | A record is deleted | | `view.changed` | `{ viewId, tableId }` | User switches views | | `selection.changed` | `{ selection }` | Row selection changes | | `plugin.activated` | `{ pluginId, position }` | A plugin component is mounted | ### Storage Per-install persistent storage backed by `localStorage`. Namespaced to `plugin::`. ```typescript // Read (synchronous) const config = ctx.storage.get('config'); // Write (async, returns Promise) await ctx.storage.set('config', { theme: 'dark', ... }); // Delete await ctx.storage.delete('config'); ``` ## PluginSDK on window.__TEABLE_HOST__ ### `useSelection(): ISelection | undefined` React hook that returns the current table row selection. Returns `undefined` if nothing is selected or if used outside a table context. ```typescript const { useSelection } = window.__TEABLE_HOST__.PluginSDK; const selection = useSelection(); ``` ### `getSelectionRecords(tableId, viewId, selection, options?): Promise` Fetch the actual record data for the current selection. Used by contextMenu plugins to get the records the user right-clicked on. ```typescript const { getSelectionRecords } = window.__TEABLE_HOST__.PluginSDK; const res = await getSelectionRecords( tableId, viewId, selection, { skip: 0, take: 200 } ); console.log(res.records); // IRecord[] ``` Returns an empty array if nothing is selected — does NOT throw. ## Host SDK (window.__TEABLE_HOST__.SDK) Full `@teable/sdk` namespace. Commonly used hooks: ```typescript const host = window.__TEABLE_HOST__; const { useFields, // () => IField[] useRecords, // (query?) => IRecord[] useTableId, // () => string | undefined useViewId, // () => string | undefined useView, // () => IView | undefined useFieldStaticGetter,// () => getFieldStatic BaseSingleSelect, // UI component FilterInput, // UI component FilterCheckbox, // UI component SelectTag, // UI component getSelectColorPairs, // (color, theme) => { color, backgroundColor } } = host.SDK; ``` ## Host UILib (window.__TEABLE_HOST__.UILib) Full `@teable/ui-lib/shadcn` namespace. Commonly used components: ```typescript const { Button, Dialog, Select, Input, // ... all shadcn components exported by ui-lib } = window.__TEABLE_HOST__.UILib; ``` ## Host OpenAPI (window.__TEABLE_HOST__.OpenAPI) Full `@teable/openapi` namespace. Commonly used functions: ```typescript const { getRecords, updateRecords, createRecords, deleteRecords, getFields, // ... all REST API helpers } = window.__TEABLE_HOST__.OpenAPI; ``` These pass through the existing permission system — users can only read/write data their role allows. ## Host Methods (window.__TEABLE_HOST__) Top-level methods on the host object (not inside PluginSDK/SDK/UILib/OpenAPI sub-namespaces). ### `host.openFormModal(config): void` Opens a **native form view** in a modal dialog — rendered with the EXACT native form component tree (`FormPreviewer`/`FormCellEditor`, the same code the built-in form view and the share page use): covers/logos, colored select tags, date picker, attachment/link/user editors, required-field validation with scroll-to-error, localStorage drafts, and submission through `formSubmit` (which fires "When form submitted" automations). The plugin specifies which form (view) to open via `formId`; the framework resolves the table, renders the form, writes the record with the current user's permissions, and manages the modal lifecycle (close on success, stay-open on error). ```typescript interface IOpenFormModalConfig { formId: string; // target form view ID (= viewId) defaults?: Record; // prefill values, keyed by fieldId // (overrides the form's own defaultValue) onSubmit?: ( // hook called AFTER framework writes the record fields: Record, createdRecord: unknown // the full created record ) => void | Promise; onClose?: () => void; // called when user cancels/closes } // Returns void — closing is automatic. host.openFormModal(config); ``` **Permission**: submission runs as the current logged-in user (via `formSubmit` — triggers `formSubmitted` automations exactly like the native form). If the user lacks `record_create` permission on the target table, the write fails → a toast shows the error and the modal stays open (user can fix and retry, or cancel). **Field rendering**: 100% native — the real `FormCellEditor` per field type (same as the built-in form view), NOT a re-implementation. **defaults override**: plugin `defaults` take priority over both the saved draft and the form view's configured `defaultValue`. Keys must be `fieldId` (not field name). **Example**: a context-menu plugin that opens a form to log an issue for the selected record: ```typescript const host = window.__TEABLE_HOST__; const { getSelectionRecords } = host.PluginSDK; function logIssueForSelected() { const records = getSelectionRecords(...); const orderNumber = records[0]?.fields['fld_orderNumber']; host.openFormModal({ formId: 'viwIssueForm', // form view ID defaults: { 'fld_relatedOrder': orderNumber, // prefill from selected record }, onSubmit: (fields, record) => { console.log('Issue logged:', record.id); // Framework already wrote the record — no need to call createRecord. }, onClose: () => { console.log('User cancelled'); }, }); } ``` ### `host.NativeForm: React.FC` The embeddable sibling of `NativeGrid`: renders the full NATIVE form (same `FormPreviewer` tree as above) inline in your own panel/view component instead of a modal. Useful for "form embedded in a side panel" or form-style data entry views. **Tier-1 injection props** (all optional; base props still work unchanged): | Prop | What it does | |------|--------------| | `visibleFieldIds: string[]` | Restrict + reorder rendered fields (session-only, nothing persisted; computed/lookup always excluded) | | `hiddenFieldIds: string[]` | Hide specific fields (ignored when `visibleFieldIds` set) | | `slots: INativeFormSlot[]` | Render custom content at `position: 'top' \| 'bottom' \| \| `; `render(ctx)` receives `{ values, setValue, errors, fields }` | | `interceptSubmit: (fields) => fields` | Transform the payload before submit — **the native submit (and formSubmitted automations) still runs** | | `submitOverride: (fields) => Promise` | Replace submit entirely — **formSubmitted automation will NOT fire** | **Virtual fields pattern**: store plugin-only values under `__virtual.` via `ctx.setValue` in a slot; they are auto-stripped before `submitOverride` (and should be removed in `interceptSubmit` if you read them there). ```typescript const { NativeForm } = window.__TEABLE_HOST__; function MyFormPanel() { return ( ( ), }, ]} interceptSubmit={(fields) => { const next = { ...fields }; const urgent = next['__virtual.urgent']; delete next['__virtual.urgent']; if (urgent) next.fld_priority = 'high'; // map virtual → physical next.fld_internalNote = 'auto-injected'; // fill a hidden field return next; }} onSubmitted={(fields, record) => console.log('created', record)} /> ); } ``` ### `host.NativeFormScope` + `host.NativeFormField` + `host.useNativeForm()` (Tier-2 granular kit) Build fully custom layouts (grouping, steps, mixed tables) while every field editor stays 100% native. `NativeFormScope` owns the state (values / localStorage draft / required validation — same semantics as the native form); drive it with the headless hook and render single native rows with `NativeFormField`. ```typescript const { NativeFormScope, NativeFormField, useNativeForm } = window.__TEABLE_HOST__; function SteppedForm() { return ( ); } function StepArea() { const form = useNativeForm(); // { ready, error, fields, values, setValue, errors, verify, reset, submit } const [step, setStep] = useState(0); if (!form.ready) return ; const stepFields = form.fields.slice(step * 3, step * 3 + 3); return (

第 {step + 1} 步

{stepFields.map((f) => ( ))}
); } ``` `form.submit()` goes through the native formSubmit endpoint, so formSubmitted automations fire. Values are draft-backed (localStorage), identical to the native form. ### `host.NativeGrid: React.FC` A wrapper around the full native grid with all providers. Pass `externalFilter` to filter rows (merged via AND conjunction with existing view filter) and `selectedRecordIds` to control row selection. ```typescript const { NativeGrid } = window.__TEABLE_HOST__; function MyGridView({ filter }) { return ; } ``` ### `host.events: { on, off, emit }` Event bus for cross-plugin communication. ```typescript host.events.on('my-event', (payload) => { ... }); host.events.emit('my-event', { data: 'hello' }); ``` ## Disposable pattern All `ctx.add*` and `ctx.on` methods return a `Disposable`: ```typescript interface Disposable { dispose(): void; } ``` In `onLoad`, the ctx collects all disposables. In `onUnload`, they are all disposed automatically (components unregistered, event listeners removed). You rarely need to call `dispose()` manually. ## Batch-edit-pro: the canonical example See `plugins/batch-edit-pro/src/index.tsx` for a complete working plugin that: - Uses `useTableId`, `useViewId`, `useFields` from host SDK - Uses `useSelection` and `getSelectionRecords` from host PluginSDK - Uses `updateRecords` from host OpenAPI - Uses `Button`, `BaseSingleSelect`, `FilterInput`, `FilterCheckbox`, `SelectTag` from host SDK/UILib - Handles loading, error, empty, and applying states - Supports singleSelect, checkbox, date, text, and number field types - Has inline SVG icons (to avoid another dependency)