feat: 收录 inkreach-teable-plugin skill;install.sh 安装全部技能
skills/ 现包含 inkreach-teable + inkreach-teable-plugin, npx skills add 一并安装两个;install.sh 改为遍历安装 skills/*。
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
# 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(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.
|
||||
|
||||
```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:<pluginId>:<key>`.
|
||||
|
||||
```typescript
|
||||
// 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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```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<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:
|
||||
|
||||
```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<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).
|
||||
|
||||
```typescript
|
||||
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`.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
const { NativeGrid } = window.__TEABLE_HOST__;
|
||||
|
||||
function MyGridView({ filter }) {
|
||||
return <NativeGrid externalFilter={filter} />;
|
||||
}
|
||||
```
|
||||
|
||||
### `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)
|
||||
Reference in New Issue
Block a user