skills/ 现包含 inkreach-teable + inkreach-teable-plugin, npx skills add 一并安装两个;install.sh 改为遍历安装 skills/*。
13 KiB
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.
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).
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(path: string, component: React.FC): Disposable;
Events
ctx.on(event, handler): Disposable
Subscribe to host events. Returns a Disposable that unsubscribes when called.
All subscriptions are auto-disposed on unload.
// 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.
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:<pluginId>:<key>.
// Read (synchronous)
const config = ctx.storage.get<MyConfig>('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.
const { useSelection } = window.__TEABLE_HOST__.PluginSDK;
const selection = useSelection();
getSelectionRecords(tableId, viewId, selection, options?): Promise<IGetSelectionRecordsVo>
Fetch the actual record data for the current selection. Used by contextMenu plugins to get the records the user right-clicked on.
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:
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:
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:
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).
interface IOpenFormModalConfig {
formId: string; // target form view ID (= viewId)
defaults?: Record<string, unknown>; // prefill values, keyed by fieldId
// (overrides the form's own defaultValue)
onSubmit?: ( // hook called AFTER framework writes the record
fields: Record<string, unknown>,
createdRecord: unknown // the full created record
) => void | Promise<void>;
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:
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<INativeFormProps>
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' | <fieldId> | <index>; 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.<key>
via ctx.setValue in a slot; they are auto-stripped before submitOverride
(and should be removed in interceptSubmit if you read them there).
const { NativeForm } = window.__TEABLE_HOST__;
function MyFormPanel() {
return (
<NativeForm
formId="viwIssueForm"
hiddenFieldIds={['fld_internalNote']]}
slots={[
{
position: 'bottom',
render: ({ values, setValue }) => (
<label>
<input
type="checkbox"
checked={Boolean(values['__virtual.urgent'])}
onChange={(e) => setValue('__virtual.urgent', e.target.checked)}
/>
加急处理
</label>
),
},
]}
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.
const { NativeFormScope, NativeFormField, useNativeForm } = window.__TEABLE_HOST__;
function SteppedForm() {
return (
<NativeFormScope formId="viwIssueForm">
<StepArea />
</NativeFormScope>
);
}
function StepArea() {
const form = useNativeForm(); // { ready, error, fields, values, setValue, errors, verify, reset, submit }
const [step, setStep] = useState(0);
if (!form.ready) return <Spinner />;
const stepFields = form.fields.slice(step * 3, step * 3 + 3);
return (
<div>
<h4>第 {step + 1} 步</h4>
{stepFields.map((f) => (
<NativeFormField key={f.id} fieldId={f.id} />
))}
<button onClick={() => (step === 0 ? setStep(1) : form.verify() && form.submit())}>
{step === 0 ? '下一步' : '提交'}
</button>
</div>
);
}
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.
const { NativeGrid } = window.__TEABLE_HOST__;
function MyGridView({ filter }) {
return <NativeGrid externalFilter={filter} />;
}
host.events: { on, off, emit }
Event bus for cross-plugin communication.
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:
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,useFieldsfrom host SDK - Uses
useSelectionandgetSelectionRecordsfrom host PluginSDK - Uses
updateRecordsfrom host OpenAPI - Uses
Button,BaseSingleSelect,FilterInput,FilterCheckbox,SelectTagfrom 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)