Plugin SDK

This is the developer reference for building a ROPARC plugin. For installing one, see Managing plugins; for using one on a dashboard, see Widgets & Plugins.

A plugin is a self-contained built app (HTML/JS/CSS) that runs inside a sandboxed iframe on a dashboard and talks to ROPARC over a postMessage bridge.

How plugins work

  • A plugin is static files — there is no plugin server. Everything it needs from ROPARC goes through the bridge.
  • The plugin never sees the auth token. Every ctx.api.* call is a request the host runs on the viewer’s behalf, automatically scoped to the current view context (active baseline, change-request branch, or variant). A plugin shows the right data in every view mode for free.
  • Plugins are git-committed and version-pinned by the installer, so a dashboard viewed at a historic baseline serves the contemporaneous build.

initRoparcPlugin() and ctx

Two small, dependency-free files form the in-iframe SDK — copy them into your plugin’s repo verbatim and build against RoparcPluginCtx:

  • pluginProtocol.ts — the postMessage contract + types
  • roparcPluginSdk.tsinitRoparcPlugin() returning ctx
import { initRoparcPlugin } from './roparcPluginSdk';

const ctx = await initRoparcPlugin();   // resolves once the host sends `init`

initRoparcPlugin() announces readiness to the host and resolves with ctx when the host replies. Outside a ROPARC host it never resolves — guard with a timeout in development.

ctx fields

FieldTypeMeaning
ctx.workareaIdstringThe active workarea.
ctx.baselinestring | nullActive baseline name, or null at HEAD.
ctx.dnastring | nullActive variant, or null.
ctx.paramsRecord<string, unknown>The widget’s Parameters (JSON) from the config dialog.
ctx.dashboardIdstring | nullThe dashboard hosting this widget (null in previews).
ctx.widgetIdstringThis widget instance’s stable id — namespace your per-instance config by it.
ctx.user{ id, displayName }The viewer.
ctx.theme{ mode: 'light' | 'dark' }Current theme.
ctx.apiRoparcPluginApiBackend calls (below).
ctx.uiRoparcPluginUiHost-rendered UI (rich text).
ctx.onThemeChange(cb)(mode) ⇒ unsubscribeSubscribe to theme switches.
ctx.setHeight(px)(number) => voidAsk the host to size the iframe.

ctx.api — backend calls

All return Promises and run with the viewer’s credentials at the current view context. Items and documents are queried with the same query language as the Items page.

MethodSignatureNotes
query(jql?, {page?, pageSize?}) → {items, total, page, pageSize}Query items.
getItem(id) → Item
createItem(payload) → Item{type, title, description?, custom?, links?, commitMessage?}
updateItem(id, payload) → ItemPartial; same shape as create.
deleteItem(id) → void
getLinks(id) → {links_to, linked_from}
getDocument(id) → DocumentIncludes sections.
listDocuments(jql?, {page?, pageSize?}) → {documents, total}
createDocument(payload) → Document
updateDocument(id, payload) → Document
getConfig() → ResolvedWorkareaConfigitem types, link types, enums, fields.
updateConfig(patch) → configPatches workarea config (for in-widget config wizards).
getUsers() → {users:[{id, display_name}]}
getCurrentUser() → {id, display_name}
listAttachments(entity, id) → {attachments:[…]}entity = 'item' | 'document' | 'dashboard'.
uploadAttachment(entity, id, {filename, mimeType, dataBase64}) → {url, filename}
getAttachment(entity, id, filename) → {mimeType, dataBase64}Blob, base64-encoded.
raw(path, init?) → {status, ok, body}Escape hatch — unversioned /api/... call, token injected by host, view context not auto-applied.

ctx.ui — host-rendered rich text

A plugin can’t embed ROPARC’s editor (it lives in the host, and image uploads need the token), so the host renders it for you and returns plain data. This is how a plugin stores descriptions that render identically everywhere else in ROPARC.

MethodSignatureNotes
editRichText(value, opts?) → string | nullOpens the host’s editor in a modal seeded with value (ROPARC rich-text JSON or ''). Resolves the edited JSON, or null on cancel. opts = {title?, placeholder?, attachmentTarget?}; pass attachmentTarget: {kind:'item'|'document', id} to enable image uploads.
renderRichText(value) → stringROPARC rich-text JSON (or legacy markdown) → sanitised HTML for read-only display.

Packaging a plugin

A plugin zip must contain an index.html at its root. A single wrapping folder is auto-stripped, so zipping either dist/ or the contents of dist/ both work.

my-plugin.zip
├── index.html          ← required, at the root
├── assets/
│   ├── app.js
│   └── app.css
├── manifest.json       ← optional (see below)
└── icon.svg            ← optional, referenced by manifest.icon
  • Use relative asset paths (./assets/app.js, not /assets/app.js). With Vite, set base: './'. Assets are served under a per-session signed path, so absolute paths break.
  • Limits: 25 MB / 2000 files per bundle; icon ≤ 256 KB.

manifest.json (optional)

Describes how the plugin appears in the Add Widget picker. All fields optional:

{
  "name": "Fault Tree",
  "description": "Build and edit fault tree analyses traced to requirements.",
  "icon": "icon.svg",
  "defaultSize": { "w": 10, "h": 10, "minW": 5, "minH": 5 }
}
FieldDefaultNotes
namethe plugin idTile title (shown instead of the raw id).
description""Tile subtitle.
iconnonePath relative to the plugin root; inlined as a data URI (≤ 256 KB). SVG or PNG.
defaultSize{w:8,h:8,minW:4,minH:4}Initial grid footprint when dropped.

Examples

Minimal plugin (vanilla, no bundler)

index.html:

<!doctype html>
<html>
  <body style="font-family: sans-serif; margin: 12px">
    <h3 id="title">…</h3>
    <ul id="list"></ul>
    <script type="module" src="./app.js"></script>
  </body>
</html>

app.js:

import { initRoparcPlugin } from './roparcPluginSdk.js';

const ctx = await initRoparcPlugin();
document.getElementById('title').textContent =
  `Hi ${ctx.user.displayName} — workarea ${ctx.workareaId}`;

const { items } = await ctx.api.query('type = "requirement"', { pageSize: 10 });
document.getElementById('list').innerHTML = items
  .map((i) => `<li>${i.id} — ${i.title}</li>`)
  .join('');

Read a configured parameter and react to theme

const ctx = await initRoparcPlugin();

// documentId comes from the widget's Parameters (JSON): { "documentId": "D-ROP-2" }
const docId = ctx.params.documentId;
const doc = docId ? await ctx.api.getDocument(docId) : null;

ctx.onThemeChange((mode) => (document.documentElement.dataset.theme = mode));

Edit a description through ROPARC’s own editor

const item = await ctx.api.getItem('ROP-42');

const edited = await ctx.ui.editRichText(item.description ?? '', {
  title: 'Edit ROP-42',
  attachmentTarget: { kind: 'item', id: 'ROP-42' },   // images upload onto this item
});

if (edited !== null) {
  await ctx.api.updateItem('ROP-42', { description: edited, commitMessage: 'Edit via plugin' });
}

// Display stored rich text read-only:
panel.innerHTML = await ctx.ui.renderRichText(item.description ?? '');

Per-instance configuration

A plugin dropped on two dashboards is two instances that may need different settings. Rather than cramming everything into ctx.params, store the instance’s config as a dashboard attachment keyed by ctx.widgetId:

const KEY = `${ctx.widgetId}.config.json`;

async function loadConfig() {
  if (!ctx.dashboardId) return {};                     // preview / unsaved dashboard
  try {
    const a = await ctx.api.getAttachment('dashboard', ctx.dashboardId, KEY);
    return JSON.parse(atob(a.dataBase64));
  } catch {
    return {};                                          // not set yet
  }
}

async function saveConfig(cfg) {
  if (!ctx.dashboardId) return;
  await ctx.api.uploadAttachment('dashboard', ctx.dashboardId, {
    filename: KEY,
    mimeType: 'application/json',
    dataBase64: btoa(JSON.stringify(cfg)),
  });
}

The plugin renders its own settings UI and persists the result as a git-backed attachment. Namespacing by widgetId keeps two instances on the same dashboard from clobbering each other.

Local development

Iterating through upload → install on every change is slow. In development builds only, the Add Widget dialog offers Plugin (by URL · dev) and the config dialog offers a Custom URL… option, so you can point a widget at a local dev server (e.g. a Vite server at http://localhost:5180/) and get hot reload. This path is stripped from production builds — a deployed dashboard can only ever load an installed, git-served plugin, never an arbitrary URL. When ready, build your plugin and install the zip from Settings → Plugins.

Security

  • Plugins run as the viewer. A plugin can do nothing the viewer couldn’t do through the API with their own session — there is no elevation.
  • The token never crosses the bridge. ctx.api.* calls are run by the host.
  • Installing is privileged — workarea installs need widgets:author; group and organization installs need a site administrator. See Managing plugins.