Fix combobox inside the health-check editor sheet#1110
Conversation
62e30c7 to
1287945
Compare
fdcf797 to
f54e82a
Compare
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | 816cfdd | Commit Preview URL Branch Preview URL |
Jun 25 2026, 08:51 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | 816cfdd | Jun 25 2026, 08:52 PM |
Cloudflare preview
Sign-in is Cloudflare Access (one-time PIN to an allowed email). The preview has its own database and encryption key; it is destroyed when this PR closes. |
Greptile SummaryThis PR fixes portaled combobox popups (operation and identity pickers) becoming unclickable and unscrollable inside the health-check editor sheet by applying three complementary changes.
Confidence Score: 5/5The three-part fix (non-modal sheet, pointer-events-auto on the positioner, onInteractOutside guard) is internally consistent and each change is independently correct. The e2e tests exercise both the click and scroll failure modes directly. All changes are narrowly scoped to the health-check editor sheet and the shared combobox component. The onInteractOutside guard uses a precise data-slot selector and guards only against the known portal pattern. The pointer-events-auto addition is a safe override that is harmless when no modal is involved. No data-path changes, no new APIs exposed. No files require special attention beyond the open threads already on this PR. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant ComboboxTrigger
participant ComboboxPopup as ComboboxPopup (portaled to body)
participant Sheet as Sheet (modal=false)
participant Radix as Radix Dialog
User->>ComboboxTrigger: click
ComboboxTrigger->>ComboboxPopup: open popup (portaled outside sheet subtree)
User->>ComboboxPopup: click option
ComboboxPopup->>Radix: pointerdown fires outside sheet content
Radix->>Sheet: onInteractOutside(event)
Sheet->>Sheet: target.closest(PORTALED_POPUP_SELECTOR)?
alt target is inside combobox popup
Sheet->>Radix: event.preventDefault() -- sheet stays open
ComboboxPopup->>ComboboxTrigger: selection applied
else target is truly outside
Radix->>Sheet: dismiss sheet
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant ComboboxTrigger
participant ComboboxPopup as ComboboxPopup (portaled to body)
participant Sheet as Sheet (modal=false)
participant Radix as Radix Dialog
User->>ComboboxTrigger: click
ComboboxTrigger->>ComboboxPopup: open popup (portaled outside sheet subtree)
User->>ComboboxPopup: click option
ComboboxPopup->>Radix: pointerdown fires outside sheet content
Radix->>Sheet: onInteractOutside(event)
Sheet->>Sheet: target.closest(PORTALED_POPUP_SELECTOR)?
alt target is inside combobox popup
Sheet->>Radix: event.preventDefault() -- sheet stays open
ComboboxPopup->>ComboboxTrigger: selection applied
else target is truly outside
Radix->>Sheet: dismiss sheet
end
Reviews (4): Last reviewed commit: "fix: combobox works inside the health-ch..." | Re-trigger Greptile |
| // (portaled to <body>, outside the dialog subtree) - it can't be scrolled or | ||
| // clicked. The overlay still renders and the dismissal guard still keeps the | ||
| // sheet open while interacting with the popup. | ||
| <Sheet open={open} onOpenChange={onOpenChange} modal={false}> |
There was a problem hiding this comment.
modal={false} disables focus trapping
Setting modal={false} drops react-remove-scroll and the body pointer-events lock (the stated goal), but it also disables Radix's focus trap. Keyboard and screen-reader users can Tab past the sheet's close button and land on focusable elements under the overlay, even though the overlay visually blocks the page. For a right-side editor panel this is likely to surprise users who expect Tab to cycle within the sheet. Consider whether focus should be manually managed (e.g. initialFocus / finalFocus props) or whether a FocusScope wrapper inside SheetContent should be added to restore the trapping behaviour without re-enabling the scroll lock.
| onInteractOutside={(event) => { | ||
| const target = event.detail.originalEvent.target; | ||
| if (target instanceof Element && target.closest(PORTALED_POPUP_SELECTOR)) { | ||
| event.preventDefault(); | ||
| } | ||
| onInteractOutside?.(event); |
There was a problem hiding this comment.
onInteractOutside consumer receives an already-prevented event
The custom handler calls event.preventDefault() before forwarding to onInteractOutside?.(event). Any consumer that inspects event.defaultPrevented to decide whether to suppress its own side-effects will see the event as already prevented for every combobox/select click. The forward call is also unconditional: even when the guard fires, the consumer still gets invoked. If future callers expect the callback to fire only when a real outside interaction happened, they will see spurious calls. Guarding the forward call with an early return when the event is suppressed, or documenting this behaviour explicitly, would make the contract clearer.
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
1287945 to
761118f
Compare
f54e82a to
b4d3626
Compare
761118f to
69d2ed8
Compare
b4d3626 to
674550b
Compare
A connection can declare ONE authenticated operation that answers "is this
credential still alive?". The probe runs the operation, maps the HTTP status to
a health state (2xx healthy, 401/403 expired, other degraded, unset unknown),
and reports it. Built for short-lived OAuth tokens (Google's 7-day dev-token
revocation): a credential that authenticated yesterday and now 401s reads as
expired.
Core owns the shared vocabulary (HealthStatus, HealthCheckSpec, HealthCheckResult,
HealthCheckCandidate, classifyHttpStatus, candidate ranking); plugins own which
operation runs, stored in their opaque integration config and picked the same way
auth methods are. Dispatch adds integrations.healthCheck.{get,candidates,set} and
connections.{checkHealth,validate}.
OpenAPI backing: list ranked candidates (non-destructive GET first), persist the
chosen spec (merging over the raw config so provider supersets keep their keys),
and run the probe against a resolved credential.
React surfaces:
- a per-connection status dot + "Check now",
- a self-hiding operation editor (a searchable freeform combobox over the whole
spec),
- and "Check the key works" in the Add Connection modal: probe the pasted key
before saving. When no health check is configured yet, the user picks a
read-only operation inline; a healthy probe saves it as the integration's
health check.
Covered by e2e: a saved connection flips healthy -> expired when its key stops
working; an unconfigured integration reports unknown; the operation picker
filters a hundreds-long candidate list on both the edit sheet and the add screen;
and Add Connection checks the key against an inline-picked operation and persists
it.
Layer account identity on top of the liveness probe. A health check can now name a response field whose value identifies the connected account, so the same probe that answers "is this alive?" also answers "whose account is this?". Core: HealthCheckSpec gains an optional identityField (a dot-path into the response body); HealthCheckResult carries the extracted identity plus a bounded responseSample of the actual returned fields; HealthCheckCandidate carries the operation's projected responseFields. New pure helpers: extractIdentity (resolve a dot-path, numeric segments index arrays), projectResponseFields (enumerate a response schema's scalar leaves breadth-first, merging every allOf/oneOf/anyOf branch so discriminated-union fields aren't dropped), and extractResponseFields (walk a real body for the live preview). OpenAPI backing extracts the identity on a healthy probe and projects each candidate's response fields. React: the editor gains a typed identity-field picker fed by those fields and a live preview (probe a pasted test key, see the real response plus what the identity resolves to); the account row labels itself with the probed identity; and the Add Connection "check the key works" flow gains the identity field (in the inline picker) and auto-fills the connection name from the probed identity. Covered by e2e: validating a key derives the identity; a saved connection's probe surfaces the account then drops it once expired; the identity picker surfaces a shallow scalar and a second-union-branch-only field across a discriminated union; the editor live preview and the Add Connection name-derivation drive the identity flow in the browser.
Give MCP connections a liveness probe. `checkHealth` dials the server and lists its tools (the same connect path tool discovery uses): a credential that authenticates and gets a tool list reads healthy; a 401/403 (or an OAuth re-authorization signal) reads expired; any other connection/discovery failure reads degraded. The connect-modal "Validate key" path runs the same probe on an unsaved credential. MCP gets liveness ONLY: there is no usable identity source (no id_token, no userinfo, no whoami convention across servers), so no identity is derived and no operation/identity editor is shown - the connection's name stays the user's label. The plugin implements only `checkHealth`, so the editor self-hides while the generic status dot + "Check now" still render. Factors the HTTP-status extraction out of invoke into a shared http-status helper and surfaces the upstream status in connect errors so the probe can classify a 401/403 as expired rather than a generic degraded. Covered by e2e: a saved MCP connection reads healthy, then expired once the upstream revokes the token; validate reports healthy for a live key and expired for a rejected one; no identity is ever derived.
Wire the OpenAPI health-check backing into the Microsoft Graph and Google provider plugins (both compile their spec through the OpenAPI machinery and store config in the same shape), so their connections report alive/expired + identity instead of always "unknown". Both plugins now implement the four health-check hooks by delegating to the shared OpenAPI backing, and persist a `healthCheck` field on their own config schema (so it survives the plugin's own decode). Each auto-configures a sensible default at add time: - Microsoft Graph: `GET /me` with identity `userPrincipalName` (always present on /me, unlike `mail`), when the default profile preset is selected. - Google: People API `people.get` with the required args pinned (`resourceName=people/me`, `personFields=names,emailAddresses`) and identity `emailAddresses.0.value`, when the bundle includes the People API. The user can switch the operation/identity from the now-available editor. Covered by e2e: adding a Google People bundle (against a local discovery doc) auto-writes the default identity check and projects the email field as a typed candidate. Graph's spec source is a fixed upstream URL, so its hooks + default are covered through the shared backing path and verified against a live tenant.
The operation/identity pickers are base-ui comboboxes whose popup portals to document.body, which collides with the Radix sheet the editor opens in: - The sheet runs modal, so react-dismissable-layer sets the body's pointer-events to none and react-remove-scroll locks the wheel to the sheet subtree. The portaled popup, living outside that subtree, could neither be clicked nor scrolled. Fix: pointer-events:auto on the popup, and open the editor sheet non-modal. - Clicking a portaled option is a pointer-down outside the sheet and dismissed it before the selection landed. Fix: the sheet's onInteractOutside ignores interactions originating in a combobox/select popup. Covered by e2e: opening the editor over a large spec, the operation popup scrolls and an option is clickable without the sheet closing.
69d2ed8 to
cc3fa8e
Compare
674550b to
816cfdd
Compare
|
Folded into the base PR (#1108). The combobox-in-modal fixes (popup pointer-events, dialog + sheet outside-interaction guards, non-modal editor sheet) are prerequisites for the base PR's operation picker in both the editor sheet and the Add Connection modal, so they belong in #1108 rather than as a trailing PR. The scroll/click e2e moved to #1108 (sheet) and the identity-combobox click into #1111. |
cc3fa8e to
2cbcd9c
Compare
The operation/identity comboboxes portal their popup to
<body>, outside the sheet's dialog subtree, so a modal Radix sheet froze them: bodypointer-eventswere disabled (couldn't click/select) andreact-remove-scrolllocked wheel scroll.pointer-events: auto(defensive for any modal-dialog combobox).Adds e2e guards that click and wheel-scroll the popup inside the sheet.
Stacked on #1109.
Stack