@viingx/extension-sdk
v0.1.0-preview.11
Published
SDK for building viingx web-client extensions (action dialogs, entity/page form elements)
Readme
@viingx/extension-sdk
Official SDK for building viingx web-client extensions — custom UI that runs inside the
viingx Content Hub client. An extension is a small web app (any framework, or none) that the
host loads in a sandboxed iframe and talks to over a typed postMessage bridge. This package
provides that typed bridge.
The @viingx family: @viingx/create-extension (npm create @viingx/extension — scaffold a starter) · @viingx/content-hub-sdk (talk to the Hub's REST API) · @viingx/content-hub-codegen (generate a typed client) · this package.
Preview release (
0.1.0-preview.x). The API may change before1.0.AI agents: this package ships an
AGENTS.mdwith the non-guessable rules (incl. thecontent-hub-sdkinterop recipe) — point your agent at it.
Extension types
| Type | Where it renders | Init function |
| ------------------- | --------------------------------------------------------- | ----------------------------------- |
| ActionDialog | A modal dialog or side panel launched from an action | initActionDialogAPI() |
| EntityFormElement | A custom element inside the entity editor form | initEntityFormElementAPI() |
| PageFormElement | A custom element inside a page / dashboard | initPageFormElementAPI() |
| configuration UI | The admin dialog that configures a configurable extension | initConfigurationFormElementAPI() |
A side panel and a modal dialog are the same extension point: both are
type: "ActionDialog"initialised withinitActionDialogAPI(), differing only byproperties.style("sidePanel"vs"dialog") inextension.json. (npm create @viingx/extensionoffers them as separate starters.)
Install
npm install @viingx/extension-sdkQuick start (an action dialog)
import { initActionDialogAPI } from "@viingx/extension-sdk";
const api = await initActionDialogAPI(); // optionally initActionDialogAPI<MyConfig>() to type getConfiguration()
// Let the host auto-size the iframe to your content (or use api.common.setHeight(px)).
await api.common.setAutomaticHeightAdjust(true);
// What the action was invoked on (e.g. the selected entities).
const input = await api.actionDialog.getInput();
await api.actionDialog.setTitle(input.definition.ui.label);
// Call the Content Hub REST API — the host provides the base URL and a session token.
// Recommended: the typed client from @viingx/content-hub-sdk (see below). No dependency
// required though — `api.common.getAPIURL()` + `getAuthToken()` also work with bare `fetch`.
// getAPIURL() may be relative; resolve to absolute so signUrl() works. The connection accepts the
// full /api/v1.0 base and strips the suffix itself.
const apiUrl = new URL(await api.common.getAPIURL(), window.location.href).href;
const hub = createContentHub(tokenConnection(apiUrl, () => api.common.getAuthToken()));
const types = await hub.types.list();
// Finish: hand a result back to the action handler.
await api.actionDialog.close({/* ... */});// The imports for the snippet above:
import { createContentHub, tokenConnection } from "@viingx/content-hub-sdk";Two things to know: getAPIURL() includes /api/v1.0 and may be relative — pass it straight to
the connection (it accepts the full base and normalizes the suffix); resolve it to absolute (as
above) only so signUrl() works, since a relative base makes the SDK's URL builders emit relative
URLs signUrl() can't parse. And the session token rotates across re-logins, so pass it as an
async provider that always fetches the current one.
initEntityFormElementAPI() / initPageFormElementAPI() follow the same shape, exposing
common, entity, and a type-specific namespace (e.g. entityFormElement.updateEntity(...),
entityFormElement.addEntityUpdateListener(...)).
Typed payloads & configuration
Every factory takes optional type parameters (defaulting to the untyped JSON shapes, so existing
code is unaffected). Pair them with a generated payload type from
@viingx/content-hub-codegen and the
entity draft becomes fully type-checked:
import type { ArticlePayload } from "./content-hub.generated";
type MyConfig = { mode: "compact" | "full" };
const api = await initEntityFormElementAPI<ArticlePayload, MyConfig>();
api.entityFormElement.addEntityUpdateListener((entity) => {
entity.content.payload.title; // typed — a typo is a compile error
});
const config = await api.entityFormElement.getConfiguration(); // MyConfig | undefinedShared APIs
Every extension gets:
common—setHeight/setAutomaticHeightAdjust,getAPIURL,getAuthToken,getUserInfo(UI locale, time zone, theme palette),getType(name)/getTypes().entity(actions & form elements) —openEntityEditor(...),openEntityPicker(...).
Calling the Content Hub API — with @viingx/content-hub-sdk
Raw fetch works (see the quick start), but for anything beyond a call or two, use the
server SDK — typed collections, queries,
files, workflow. Two things to know when wiring the two packages together:
common.getAPIURL()returns the REST base including/api/v1.0, possibly relative — pass it straight to the connection (it accepts the full base and normalizes/api/v1.0itself). Resolve it to absolute (new URL(url, location.href).href) only if you usesignUrl.common.getAuthToken()returns the user's current session token, which can rotate across re-logins — pass it as an async provider, not a captured string.- Showing Hub images? A plain
<img src>can't authenticate from the sandboxed iframe (opaque origin) — mint thesrcwithsignUrl+getAuthToken(), or fetch the bytes via thedownload…twins. A bare URL-builder URL in<img src>gets a 401.
import { initActionDialogAPI } from "@viingx/extension-sdk";
import { createContentHub, tokenConnection } from "@viingx/content-hub-sdk";
const api = await initActionDialogAPI();
// getAPIURL() includes /api/v1.0 (may be relative) — the connection normalizes it; resolve to
// absolute only for signUrl.
const apiUrl = new URL(await api.common.getAPIURL(), location.href).href;
const hub = createContentHub(tokenConnection(apiUrl, () => api.common.getAuthToken()));
// When creating entities, use the host's default-content-locale contract (the same fallback logic
// the client's own editor applies): `(await api.common.getUserInfo()).defaultContentLocale` —
// `{ behavior: "use", locale }` apply silently; `{ behavior: "propose", locale }` pre-select but
// let the user confirm; `{ behavior: "choose" }` / `undefined` (older host) the user must pick.
// The user's `uiLocale` is a display language — never a content source locale.
const articles = await hub.entities.of("articles").query({}, { paging: { type: "offset", offset: 0, limit: 10 } });For fully typed payloads, generate a client from your tenant's types with
@viingx/content-hub-codegen and use
its createHub(connection) — entity payloads inside form elements can then be cast to the
generated XxxPayload types.
Packaging an extension
An extension is a zip containing an extension.json descriptor plus your built assets
(index.html + JS). Minimal descriptor:
{
"name": "myExtension",
"type": "ActionDialog",
"properties": { "style": "dialog", "width": "medium" }
}name must match ^[a-zA-Z][a-zA-Z0-9]*$ — a letter first, then letters/digits only (no spaces
or hyphens); the Hub rejects the upload otherwise. It's the extension's unique key per tenant:
uploading again with the same name replaces it.
Upload the zip to a tenant via the admin UI, or programmatically with
@viingx/content-hub-sdk:
hub.extensions.upload(zipBlob).
properties are extension-point-specific. For action dialogs: style is "dialog" or
"sidePanel"; width sizes it — dialog: small (default) / medium / large; side panel:
medium (default) / large / extraLarge. Height is not a descriptor field — set it at
runtime with common.setAutomaticHeightAdjust(true) or setHeight(px). configurable: true adds
an admin configuration dialog (initConfigurationFormElementAPI()), read via getConfiguration()
— omit it if there's nothing to configure.
Starter templates
An extension needs no framework or scaffolding — any web app that calls the matching init*API()
and zips up with an extension.json works (see the quick start and packaging sections above).
For a ready-to-run starter (React + Fluent UI + webpack, one per extension type, with a local dev
server and a one-command deploy), scaffold a project with:
npm create @viingx/extension@latestLicense
Apache-2.0 © viingx AG
