Skip to content

Move every panel and plugin caller onto the request bus - #325

Merged
sandesh-sp merged 6 commits into
developmentfrom
refactor/panel-plugin-command-cutover
Aug 27, 2026
Merged

Move every panel and plugin caller onto the request bus#325
sandesh-sp merged 6 commits into
developmentfrom
refactor/panel-plugin-command-cutover

Conversation

@sandesh-sp

@sandesh-sp sandesh-sp commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Why

With the request surface in place (#322), there were three ways to collapse a panel: a direct PanelManager_ call, an mmgisAPI.hidePanel method, and a core:hidePanel event. Two of them are strictly worse than the third.

core:* events are fire-and-forget, so a refusal has nowhere to go — a config naming a panel that does not exist produced nothing at all. The direct mmgisAPI methods cannot cross a sandbox boundary, since postMessage cannot carry a function reference. Both are removed here.

What

Removed: _initCoreCommandDispatcher and the seven core:* command events; showPanel, hidePanel, togglePanel, showPlugin, hidePlugin, loadPlugin, unloadPlugin, isPluginLoaded, isPluginHidden from mmgisAPI; togglePanelCollapsed from PanelManager_.

Panel controls issue requests and log the refusal reason rather than clicking dead, and decide whether to draw themselves from canSetState instead of reading allowedStates directly — which missed the float-position restriction, so a float panel offered a Minimize button that stranded it.

Config action strings resolve through one shared resolver, so a mission author reads the same request names the API documents rather than a second tool-specific vocabulary. core: is reserved rather than reused: its verbs have no expressible successor here (togglePanel is gone; unloadPlugin needs a state argument a colon-delimited string cannot carry), so a config still carrying core:showPlugin:LayersTool gets a warning naming the supported actions instead of emitting an event nothing listens for.

Consumers migrated: Chart (plugins:show on analysis-ready, plugins:setState on close), AOI (unload on close), Title (through the resolver), AddTempLayer.

Teardown announces itself. panels:changed and plugins:changed were silent when a layout came down, on the reasoning that teardown destroys the subscribers with it. The bus outlives the layout, so a plugin that subscribed is still subscribed and still holding a roster of panels and plugins that no longer exist. Both events now fire once more with an empty listing, which agrees with what the providers report from that point on. PanelManager_ gains clear() so that goes out once rather than once per panel.

Regression fixed

AddTempLayer was calling window.mmgisAPI.showPlugin / hidePlugin through a locally declared optional type. Those methods are removed here, and optional chaining would have turned both calls into silent no-ops. The tool starts hidden and ships no trigger of its own, so its only door would have stopped opening and its close button would have stopped closing — with no error anywhere. It goes through the shared client's plugin wrappers instead, which report a refusal.

Two other defects turned up while tracing impact — panels:show shrinking an already-visible panel, and a failed plugin transition reporting not-found. Both are in the state owners #322 reworks, so they are fixed there and written up in that PR's body; nothing about them is in this diff.

Testing

Specs for the panel controls, the config resolver, and each migrated consumer, including a negative check run on every new test — the behaviour was broken to confirm the test goes red, then restored.

1485 tests passing, tsc --noEmit clean.

Related

Third of a three-part stack: #324#322this. Merge in order.

@sandesh-sp
sandesh-sp force-pushed the refactor/panel-plugin-command-cutover branch from 90eb9ac to f24efa9 Compare August 19, 2026 14:14
@sandesh-sp
sandesh-sp force-pushed the refactor/panel-plugin-command-cutover branch 2 times, most recently from b547423 to 8b62c90 Compare August 19, 2026 15:57
@sandesh-sp
sandesh-sp force-pushed the refactor/panel-plugin-command-cutover branch from a45b708 to 6daf668 Compare August 20, 2026 19:57
@sandesh-sp
sandesh-sp force-pushed the refactor/panel-plugin-command-cutover branch from 6daf668 to b77156b Compare August 25, 2026 19:20
@sandesh-sp
sandesh-sp requested a review from CarsonDavis August 25, 2026 19:21
Base automatically changed from feat/panel-collapse-bus-api to development August 27, 2026 01:26
The core:* command events and the direct mmgisAPI panel and plugin methods
were a second way to do what the request bus now does, and the weaker one:
events are fire-and-forget, so a refusal had nowhere to go, and the direct
methods cannot cross a sandbox boundary at all. Both are removed.

The panel header and icon-tray controls issue requests and log a refusal
rather than clicking dead, and draw themselves from canSetState instead of
reading allowedStates and missing the float restriction.

Config action strings resolve through one shared resolver, so a mission author
reads the same request names the API documents. core: is reserved rather than
reused: its verbs have no expressible successor, so a config still carrying
core:showPlugin:<id> gets a warning naming the supported actions instead of
emitting an event nothing listens for.

Chart, AOI and AddTempLayer move to the plugin state requests. AddTempLayer
was reaching the removed methods through a window cast, which optional
chaining turned into a silent no-op.
panels:changed was documented as firing when a panel gains a tool. It doesn't
— only losing one notifies — so a reader following the docs would wait for an
event that never comes. Both call sites run before anything subscribes, so the
listing is right in practice; the sentence was the only thing wrong.

plugins:changed now names the batch broadcast, and both events say plainly
that teardown is silent.
A config action carrying no namespace is emitted exactly as written. Every
tool publishes under plugin:<toolId>:, so a bare name lands on a channel
nothing subscribes to and the button that triggered it does nothing at all —
no error, no clue.

It is still emitted, since a bare event name is legal, but it is reported with
the fully qualified form the author most likely wanted. This is the same
reasoning the reserved core namespace already follows: a config mistake is
worth surfacing rather than swallowing.
resolveAction requested first and caught afterwards, so a core that never
registered the action surfaced as "failed: Error: No handler for…" — the
shape of an internal fault rather than of an action this core does not offer.
Asking first gives both that case and a wholly absent bus the same plain
"no handler", which is what the surrounding comment already claimed happened.

Also covers the float-panel header. Deciding from allowedStates rather than
canSetState draws a Minimize button on a panel whose position forbids the
state it targets, stranding it; the rule was tested at the manager and the
provider but never through the control that had the bug.
Teardown was the one layout change that went unannounced, on the reasoning
that it destroys the subscribers along with the state they follow. The bus
does not work that way: it outlives the layout, so a plugin that subscribed to
panels:changed or plugins:changed is still subscribed after the panels and
plugins it was following have been dismantled, holding a roster of things that
no longer exist. An empty listing is what tells it otherwise, and it agrees
with what the providers report from that point on — empty listings from
getAll, layout-inactive from every command.

PanelManager gains clear(), so the empty listing goes out once instead of the
per-panel stream unregisterPanel produced when modern.js looped over the
layout. destroyAllTools broadcasts the same way, and both stay silent when
there was nothing to tear down.
The resolver named the four request strings a second time and issued them
itself, next to a client whose comment claims the bus names live there and
nowhere else. Two spellings of a name with nothing holding them together drift
into a layout-inactive refusal that logs nothing, and five typed wrappers sat
unused beside the duplicate. The action table maps a name to the wrapper that
runs it, so each name is written once and the wrappers are the only way across.

It still asks whether the handler exists before calling. A wrapper reports a
core that lacks the handler as layout-inactive, which reads to a config author
as a layout that is not up rather than a core too old to offer the action.

The panel header's Maximize button and the icon tray had no coverage: rendering
a button and wiring it to the right request look identical until it is clicked,
and the tray is where a click means focus or un-focus depending on panel state.
The rejected-request arm is covered too, without which a failed command escapes
as an unhandled rejection nothing surfaces.

Docs describe what the commands actually answer: an unrecognised state name as
bad-request rather than state-not-allowed, hasHandler as a question about the
core build rather than about what it currently holds, and panels:show as
lifting a panel out of collapsed only — which leaves the iconified left panel
every stock layout starts with exactly where it is, so revealing it is two
steps and the result says which one you are on.
sandesh-sp added a commit that referenced this pull request Aug 27, 2026
## Why

Panels and plugins can only be driven from inside core. `PanelManager_`
and `ToolControllerModern_` are reachable by direct import, which a
sandboxed plugin cannot do, so a plugin that wants to collapse the panel
it lives in — or reveal a sibling plugin — has no way to read the state
or act on it.

The state owners were also awkward to drive even from inside core. They
signalled refusal by throwing, which does not survive a bus boundary,
and panels offered only `togglePanelCollapsed`: a toggle acts on
whatever it finds rather than on what the caller asked for, so a control
working from a listing it read a moment ago flips the wrong way, and a
retry undoes itself.

## What

**Results instead of exceptions.** `setPanelState` returns `{ ok: true,
state, changed }` or `{ ok: false, reason }`. `canSetState` exposes the
same constraints, so a control can decide whether to draw itself at all
rather than discovering the answer by being clicked. `showPanel`
replaces the toggle and names the state it resolves to.

**Plugin lifecycle as states.** `ToolControllerModern_`'s separate
loaded and hidden flags become one of `unloaded`, `hidden`, `visible`,
reachable through `setPluginState`. Asking for `visible` on an unloaded
plugin loads it.

**Eight handlers on the bus** plugins already use for everything else:

| | |
|---|---|
| `panels:getAll` / `plugins:getAll` | the current listing |
| `panels:setState` / `plugins:setState` | the primitive |
| `panels:show` / `panels:hide` | sugar, resolving a target state |
| `plugins:show` / `plugins:hide` | same |

Handlers register at module load and stay registered. An absent layout
is reported as a `layout-inactive` **result**, not a missing handler, so
a caller sees one failure vocabulary whichever layer refused. The
managers are read through getters rather than captured at registration,
since the UI clears them on teardown.

**Two events.** `panels:changed` and `plugins:changed` carry the same
listing the `getAll` handlers return, so a subscriber never has to
re-request. `panels:changed` replaces `mmgis-panel-layout-changed`.
Payloads are frozen projections holding no core references, so they
survive a structured clone — which is what a future sandbox bridge will
have to do to them.

Matching typed wrappers land in
`src/essence/Tools/_shared/adapters/mmgisAPI.ts`, the one place the bus
name strings appear.

## Scope

Mostly additive, with two breaking changes worth knowing about before
merge:

- `PanelManager_.togglePanelCollapsed` is removed, from both the class
and the interface. A toggle acts on whatever it finds, which is the
problem this PR exists to fix; `showPanel` and `setPanelState` name the
state they resolve to instead.
- `mmgis-panel-layout-changed` is renamed to `panels:changed`.

Everything else is additive. The existing `mmgisAPI.showPanel` /
`hidePanel` / `togglePanel` methods and the `core:*` command events all
still work — they are reimplemented on the new primitives here and
removed in #325.

## Defects fixed along the way

Two, both in the state owners this PR reworks:

- `panels:show` on an already-visible panel resolved a target state and
so could *shrink* it. Every stock layout gives the left panel an
`iconified` default, so showing an expanded panel iconified it. Show now
only ever lifts a panel out of `collapsed`.
- A failed plugin transition reported `not-found`, pointing a reader at
the plugin id — the one thing already known to be good, since the id was
resolved before the transition was attempted. It reports
`transition-failed`.

## Testing

Provider specs for all eight handlers (refusal vocabulary, malformed
payloads, absent layout, the float-position restriction),
`PanelManager_` command specs, plugin state-transition specs, and the
typed wrappers.

**1458 tests passing, `tsc --noEmit` clean.** Every commit in the stack
was verified green in isolation, not just the tip.

## Related

Second of a three-part stack: #324 → **this** → #325.

Note for #323: this replaces the API that branch was written against,
and the break lands when **this** PR merges — not #325.
`panels:toggleCollapsed` is not registered (toggle was dropped
deliberately, see above), `collapsible` is not on the listing, and the
event is now `panels:changed`. #323 needs a follow-up to move onto
`panels:show` / `panels:hide` driven off the `state` it already reads.
@sandesh-sp
sandesh-sp force-pushed the refactor/panel-plugin-command-cutover branch from b77156b to 39baa7f Compare August 27, 2026 01:26
@sandesh-sp
sandesh-sp merged commit 32a5515 into development Aug 27, 2026
2 checks passed
@sandesh-sp
sandesh-sp deleted the refactor/panel-plugin-command-cutover branch August 27, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants