The Item Detail Panel

A plugin can open ROPARC’s own item detail panel — the one the items table and the document view open — docked beside itself.

It is the host’s panel, not one you draw. That is the point: it arrives with every tab, field, permission rule and editor the rest of the application has, and it gains whatever they gain. A panel you drew yourself would drift from the real one the first time either changed, and you would be reimplementing the rich-text stack behind it.

const ctx = await initRoparcPlugin();

await ctx.ui.openItemPanel('WA-42');
await ctx.ui.closeItemPanel();

See the Plugin SDK reference for initRoparcPlugin() and the rest of ctx.

It is not modal

The panel docks beside your plugin rather than covering it. Your plugin keeps its input and the rest of its width, and stays interactive the whole time — reading one item while working on another is one task, not two.

Two things follow:

  • Opening is not a question. openItemPanel resolves as soon as the panel is up, not when it closes. There is no “the user finished with it” moment to await; the panel is a surface, not a dialog.
  • There is one panel. Calling openItemPanel again with another id re-points the panel that is already there. You never get two.

Mark the open item in your own UI. The user can see the panel, but not which of your rows or cards it belongs to.

Who saves: managed or delegated

ctx.ui.openItemPanel(itemId, { mode: 'managed' })    // the default
ctx.ui.openItemPanel(itemId, { mode: 'delegated' })

managed

The panel saves, exactly as it does in the items table: its own Save and Discard buttons, its own dirty state, its own commit.

Use it when your plugin has no save model of its own — a report, a chart, a read-mostly view where editing an item is a side errand.

delegated

The panel writes nothing. Every edit is reported to you as it is made, and committing it is your job.

Use it when your plugin already has unsaved changes of its own. Two Save buttons on one screen is a question the user should not have to answer, and answering it wrongly — pressing one and not the other — silently loses work. In delegated mode the panel shows no save bar at all.

The real payoff is that a panel edit and whatever else you are holding can land in one commit:

ctx.onItemPanelChange(({ itemId, fields }) => {
  pending.set(itemId, fields);       // hold it
  render();                          // show it as unsaved
});

// later, on your own Save
await ctx.api.batch({
  operations: { updates: [...] },    // including the panel's edit
  transitions: [...],                // and anything else queued
  commitMessage: 'Triage session',
});

The change payload

onItemPanelChange hands you the panel’s own update shape, not a flat map of fields:

{
  "itemId": "WA-42",
  "fields": {
    "title": "Emergency stop within reach of the helm",
    "description": "{\"type\":\"doc\",…}",   // ROPARC rich-text JSON
    "custom": { "owner": "u_17", "asil": "B" }
  }
}

Built-ins sit at the top; everything else is nested under custom. Pass it to api.batch unflattened and you will write a field literally called custom, containing your fields, while every real field keeps its old value. Nothing errors. The save looks like it worked and changed nothing.

Flatten it on the way in:

for (const [key, value] of Object.entries(fields)) {
  if (key === 'custom') {
    for (const [k, v] of Object.entries(value)) hold(k, v, { builtin: false });
  } else {
    hold(key, value, { builtin: true });
  }
}

and write them back the way they came:

// built-in
{ id, data: { title: value } }
// custom field
{ id, data: { custom: { [key]: value } } }

What does not come through

  • Links. They have their own endpoint and their own pending model. A link edited in a delegated panel is not reported and cannot be committed by you.
  • Status. Status never moves by writing a field, in a panel or anywhere else — it moves by executing a workflow transition, so guards, permissions, effects and the audit trail all apply. A status change made in the panel today fires immediately rather than joining your batch.

Both are worth telling your user about if your plugin’s Save implies “everything on screen”.

A worked example: the kanban board

The builtin kanban opens delegated, because it already batches dragged cards.

// A click opens the panel. A drag does not — a drag ends with a mouseup the
// browser also reports as a click, and opening a panel then is a panel nobody
// asked for.
card.addEventListener('click', () => {
  if (dragInProgress) return;
  void ctx.ui.openItemPanel(item.id, { mode: 'delegated' });
});

// Edits arrive unwritten. They become unsaved changes like any dragged card.
ctx.onItemPanelChange(({ itemId, fields }) => {
  for (const [key, value] of flatten(fields)) {
    pending.set(`${itemId}::${key}`, { itemId, key, value });
  }
  render();          // the card shows the edit immediately
});

One Save then commits the field the user typed and the three cards they moved, as one commit rather than four.

Layout

Do not give the panel a width. It sizes itself: the host wraps it in the same resizable panel the rest of the application uses, with a drag handle, a 320–960px clamp and a width remembered across sessions. A width set from the outside overrides all three, and the handle appears to do nothing.

Lay your side out to take the space that remains:

.plugin-root { flex: 1; min-width: 0; }   /* not width: 100% */

min-width: 0 matters. Without it a flex child refuses to shrink below its content, and the panel pushes your plugin off the edge instead of narrowing it.

Compatibility

openItemPanel throws on a host older than this SDK. Catch it — a board that cannot open panels is still a board:

void ctx.ui.openItemPanel(id, { mode: 'delegated' }).catch(() => {
  /* older host: no panel, everything else still works */
});

Gotchas

Report only what the user typed. If you mirror the panel’s edits into your own state, be aware the panel reports a diff against the item as loaded. Your own equivalent must not fire while state is still being seeded — otherwise an item is marked as edited the moment it is opened.

Iframes eat the mouse. Any drag handle in the host that the user drags over your plugin loses its mousemove events to your iframe’s document. The host handles this for panel resizing. If you add a host-side drag of your own, do the same.

The panel is outside your iframe. You cannot style it, read it, or attach to its DOM. Everything you know about it is what openItemPanel and onItemPanelChange tell you.