Conversation
The `call` row action awaited `serveQueueEntry` with no rejection handler, and both call sites (the inline button and the overflow menu item) discard the promise the handler returns. Any failure — a dropped network, a server error, a missing ticket number — therefore became an unhandled rejection: a full-screen "Uncaught runtime errors" overlay in development builds, and nothing at all in production builds. The action now validates the queue name, ticket number and calling status before posting, reports every failure through `showSnackbar` the way the sibling queue entry mutations do, and leaves the row usable so the user can retry without reloading. `runAction` wraps both invocation sites so a rejection an action does not handle itself is logged rather than escaping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwrMcxXwSx8rPmMZu2VigK
Contributor
|
Size Change: 0 B Total Size: 7.32 MB ℹ️ View Unchanged
|
Reviewing the first commit turned up one more copy of the defect and four guards no test was holding down. The "Serve" button inside the modal the Call action opens fires a second `assignticket` request, chained off `updateQueueEntry`'s success, so the rejection handler on that first request never covered it. Driving it in a browser with the request aborted produced exactly what the ticket reports: one full-screen overlay iframe, `pageerror: TypeError: Failed to fetch`, no snackbar. It now reports the failure the way the Call action does. The last-resort `runAction` net only wrote to the console, which leaves the production half of the defect in place — a click that appears to do nothing. It now shows a snackbar too, and two tests hold it there; before, removing the net entirely broke no test at all. Mutation testing the rest found three more survivors: nothing covered a missing queue name, a missing calling status, or the fallback used when a failure carries no message. Fourteen deliberate breakages now each fail at least one test. The action no longer runs `mapVisitQueueEntryProperties`, a twenty-five field mapper with date parsing in it, to read two fields on every click. It reads the queue name off the entry and the ticket number through a small shared `getVisitQueueNumber`, which the mapper now uses too. That also moves the work behind the guard, where a throw is reported rather than swallowed. Finally, the missing-details message told the user to check the service queues configuration. The value that is actually missing in ordinary use is the ticket number, which comes from a visit attribute, so a clerk looking at a visit created outside the queue flow was being sent to a configuration page they cannot change. That case now names the visit instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwrMcxXwSx8rPmMZu2VigK
Fuzzing the failure path with the range of values `openmrsFetch` can reject with turned up two ways the handling added for this ticket could itself fail, which is the original defect wearing the error handler's clothes. `getErrorMessage` promises a `string`, but reads a shape the REST API documents rather than one anything enforces, so a rejection carrying a non-string `message` was handed straight to a snackbar subtitle — a React render crash. It now keeps its word and lets the caller's fallback speak. `runAction` is the last thing between a failed action and an unhandled rejection, and it reported failures without protecting the reporting. A throw from the snackbar store, or from reading the rejected value, escaped as the very rejection the net exists to catch. The reporting is now guarded, so `runAction` cannot reject. Also covers the generic-message fallback on the Serve request, the last branch this ticket added that no test reached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwrMcxXwSx8rPmMZu2VigK
…ess path Re-running the mutation set found one survivor: deleting the `await mutateQueueEntries()` that precedes the modal broke no test, because the hook's mock was an anonymous `vi.fn()` nobody could see. The entry's status has changed on the server by that point, so without the re-fetch the "Serve patient" modal opens on top of a table still showing "Waiting". The mock is now a visible spy and the success-path test asserts it, which takes the set to eighteen mutants and no survivors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwrMcxXwSx8rPmMZu2VigK
…light `disabled` takes a button out of the tab order, so the moment a keyboard user presses Call the browser drops focus onto `document.body` — measured on the standalone, before the request has even been sent. Getting back to the button to read the outcome or retry then means tabbing in from the top of a queue table that is one row per waiting patient. That is the failure this PR's visible error message exists to prevent, reintroduced by the debounce. `aria-disabled` plus `aria-busy` says the same thing to a screen reader without removing the button from the tab order, and `handleClick`'s `isPendingRef` guard — not the attribute — is already what discards the second click. Carbon's own `--btn--disabled` class was not used to keep the greyed-out look, because it pairs the disabled colour with `outline: none` on `:focus` and would hide the focus ring on the button we are deliberately keeping focused; the trade is that a sighted user no longer sees the button dim for the length of a request. Also rewrites `callPatientMissingConfiguration`. "Calling status" is the name of a configuration property rather than anything a triage clerk has a word for, and neither that nor the queue's name is something they can go and change, so the message now says whose job it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwrMcxXwSx8rPmMZu2VigK
The overflow menu item had no in-flight guard, so two clicks 85ms apart on its Call item sent two POST /queueutil/assignticket requests for one intended call — measured on a running queue table, not inferred. Neither control is `disabled` while its request runs, because that drops a keyboard user's focus mid-request, so nothing in the DOM discards the second click. The inline button's guard is extracted into a `useSingleFlight()` hook that both call sites now share, and the button's guard was re-measured on the same page because `aria-disabled` leaves a button clickable. Both send one request for a rapid double-click and release afterwards, so a deliberate retry works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwrMcxXwSx8rPmMZu2VigK
The row action guards its own error reporting, because a throw while reporting a failure escapes a rejection handler as exactly the unhandled rejection the report replaces. The three rejection handlers in the Serve patient modal — the other path this change touches — did not, so the two behaved differently under the same failure. They now report through one guarded helper. Two of them also read `error?.message`, which shows the wrapper's "Server responded with 500 ..." instead of the wording the server sent, and hands a non-string straight to a snackbar subtitle where React cannot render it. Both now use `getErrorMessage`, like every other queue entry mutation. When only the ticket-display request fails, `updateQueueEntry` has already succeeded: the entry has ended and been replaced by one reading "In Service", confirmed against the API on a running standalone. Reporting that as "Error calling patient" told the user nothing had happened, and left the table behind the modal showing a status that is no longer true with nothing to revalidate it. It now names what happened and re-reads the table. Repairing the half-done transition still needs a compensating request and is not attempted here; a second Serve press cannot make it worse, because the backend refuses to end an entry that has already ended. `mutateQueueEntries()` was also called bare from three promise handlers, each a floating rejection of its own; they now go through a helper that logs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwrMcxXwSx8rPmMZu2VigK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requirements
Summary
Fixes O3-5666.
What was wrong
The
callrow action in the service queues table (queue-table-action-cell.component.tsx) awaitedserveQueueEntrywith no rejection handler:Both call sites throw the returned promise away —
ActionButtonawaits it inside a baretry/finally, andActionOverflowMenuItempasses it straight to a DOMonClick. So any failure fromPOST /ws/rest/v1/queueutil/assignticketbecame an unhandled promise rejection:Because
openmrsFetchrejects on a non-2xx response, thecallingQueueResponse.okbranch never actually ran for server errors — it just skipped opening the modal for the (unreachable) non-throwing case.Separately, the request was posted without checking that the queue name, ticket number and calling status were present. The ticket number comes from a visit attribute, so it is missing whenever a visit was created outside the queue flow.
JSON.stringifydrops the undefined key, so what actually went over the wire was{"servicePointName":"Outpatient Triage","status":"calling"}— the queue module accepts it, and the "Serve patient" modal opens as though the patient had been called with a number they do not have.What changed
queue-table/cells/queue-table-action-cell.component.tsxcallaction validates the queue name, ticket number and calling status before posting, and shows a red snackbar naming what is missing instead of sending an incomplete request. The message distinguishes the two situations: the ticket number lives on the visit, so a clerk who hits that case cannot fix it by "checking the configuration"; the other case does point at the configuration, and says so in a way that names whose job it is rather than naming the configuration properties.try/catch; every failure is surfaced withshowSnackbar({ kind: 'error' })usinggetErrorMessage()frommodals/queue-entry-error.utils, matching how the sibling queue entry mutations (queue-entry-actions-modal.component.tsx,call-queue-entry.modal.tsx) report errors. A response that resolves without a result falls back to the existingunexpectedServerResponsemessage.runAction()helper wraps both invocation sites (inline button and overflow menu item) so that a rejection an action does not handle itself is reported rather than escaping as an unhandled rejection.ActionProps.onClickis now typedvoid | Promise<void>to reflect what it already was.useSingleFlight(), so a click that arrives while the previous one's request is still running is discarded. Neither control isdisabledwhile it runs — see the accessibility section — so nothing in the DOM does this, and it has to be a ref rather than state because the second click can arrive before React has re-rendered from the first. On a live queue table the inline button already held (the guard was added with the rest of this change); the overflow menu item did not, and two clicks 85ms apart on it sent twoPOST /queueutil/assignticketrequests for one intended call.mapVisitQueueEntryProperties, a twenty-five field mapper with date parsing in it, to read two fields on every click. It reads the queue name off the entry and the ticket number through a small sharedgetVisitQueueNumber, which the mapper itself now uses.call-queue-entry-modal("Serve patient") opens as before.modals/call-modal/call-queue-entry.modal.tsx— the "Serve" button inside that modal fires a secondassignticketrequest, chained offupdateQueueEntry's success. A rejection handler passed to.then()does not cover a promise created inside its own fulfilment callback, so this request had no handler at all: the same defect, one click further on. It now has one. Three further things in the same file:reportFailure()that cannot itself throw. The row-action path already guarded its reporting, because a throw while reporting — a snackbar store that is itself the broken thing, say — escapes a rejection handler as exactly the unhandled rejection the report was added to replace. This path did not, so the two behaved differently under the same failure.error?.messagerather thangetErrorMessage(error), which showed the wrapper's "Server responded with 500 …" instead of the wording the server itself sent inresponseBody, and handed a non-string straight to a snackbar subtitle, where React cannot render it. Both now go throughgetErrorMessage, like every other queue entry mutation in the app.updateQueueEntryhas already succeeded: the queue entry has ended and been replaced by one carrying the transition status. Verified on the running standalone — the newly created entry comes back from the API as"In Service". Reporting that as "Error calling patient" told the user nothing had happened when the patient had in fact moved on, and left the table behind the modal showing a status that is no longer true, with nothing to revalidate it (the queue entries have no refresh interval). It now says "The patient has been moved on in the queue, but the ticket display was not updated" and re-reads the table. Repairing the half-done transition is deliberately still not attempted; see the deferrals below.mutateQueueEntries()was called bare from three promise handlers. It rejects when a re-read fails, so each of those was a floating rejection of its own; they now go through arefetchQueueEntries()that logs instead.service-queues.resource.ts—getVisitQueueNumber, extracted from the mapper so both callers read the attribute the same way.modals/queue-entry-error.utils.ts—getErrorMessagedeclares that it returns astring, but it reads a shape the REST API documents rather than one anything enforces. It now keeps that promise, so a rejection carrying a non-stringmessageproduces the caller's generic fallback rather than being handed to a snackbar subtitle, where a non-string is a React render crash.Five new translation keys:
errorCallingPatient,callPatientNoTicketNumber,callPatientMissingConfiguration,queueEntryActionFailedandpatientMovedButNotCalled.Tests
queue-table-action-cell.test.tsxgains fifteen cases andcall-queue-entry.test.tsxgains eleven: the success path including the queue re-fetch that precedes the modal; a rejected request; a rejection carrying the server's own message inresponseBody; a rejection carrying no message; a rejection whose message is not text; a response that resolves with no result; each of the three missing-detail guards; the same failure launched from the overflow menu; three for therunActionnet, one of which fails the reporting itself; the failed Serve request with and without a message; the pending Call button keeping focus and its place in the tab order; a rapid double-click on the overflow menu item, which must send one request and must let a deliberate later click through; the ticket-display failure saying the patient has moved on and re-reading the table; each of the modal's three rejection handlers surviving a snackbar that throws while it reports; the requeue and update handlers showing the server's own message and falling back when it is not text; and a queue re-read that fails.Twenty-five of those twenty-six fail when the two source files are reverted to
main, and Vitest reports eleven unhandled rejections in that run — which is the same defect the user sees as the crash overlay:Every guard, catch and fallback this PR adds was then broken deliberately, one at a time — twenty-eight mutants, twenty-eight killed, no survivors. The ten added in this revision: the overflow item losing its in-flight guard, that guard never blocking, that guard never releasing, the inline button losing it, the modal's reporting losing its own guard, the ticket-display failure going back to "Error calling patient", that path not re-reading the table, each of the two handlers going back to
error?.message, and the queue re-read's rejection going unhandled.Coverage over the four touched files puts a test on every line and every branch the PR adds. The uncovered remainder of
queue-table-action-cell.component.tsx(82.6% statements, 81.6% branches) is pre-existing unknown-action,showIfand layout guards plus the modal-opening bodies of the unrelated row actions; the uncovered remainder ofcall-queue-entry.modal.tsx(92.1% statements, 83.3% branches) is the pre-existing identifier list rendering.Whole package:
Test Files 28 passed (28) · Tests 166 passed (166). Repo-wideyarn verify— what CI runs — is green across all 27 tasks, andprettier --checkis clean.Screenshots
No design changes; the visible difference is the failure feedback described in the table below, and the exact snackbar text is quoted there. No images are attached — this description is maintained through the GitHub API, which cannot upload them. Say the word if a reviewer wants screenshots and I will add them through the web UI.
Related Issue
https://openmrs.atlassian.net/browse/O3-5666
Other
How this was verified in a browser
Beyond unit tests, every row below was driven end to end, twice: once with the branch's source and once with the two source files reverted to
main.http://localhost:8081/openmrs, with the queue module and a livePOST /ws/rest/v1/queueutil/assignticketendpoint.openmrs develop --port 9000 --backend http://localhost:8081 --sources packages/esm-service-queues-app— a development build, so React's error overlay is active.Visit Queue Numbervisit attributeTRI-042, queue entry inOutpatient Triagewith statusWaitingso the Call button renders.**/ws/rest/v1/queueutil/assignticket. The missing-ticket-number case was produced by settingvisitQueueNumberAttributeUuidto"", which is how the reference application ships it.callinactions.overflowMenu, which is not the default, so those runs setqueueTables.columnDefinitionsto{ buttons: ['edit'], overflowMenu: ['call', 'remove'] }through the SPA's own temporary-config store.main)pageerror: TypeError: Failed to fetch; no snackbarpageerror: Uncaught (in promise) Error: Server responded with 500 (Internal Server Error) for url /openmrs/ws/rest/v1/queueutil/assignticket; no snackbar; nothing opensvisitQueueNumberAttributeUuidblank{"servicePointName":"Outpatient Triage","status":"calling"}— no ticket number at all — and the "Serve patient" modal opened as if it had workedpageerror: TypeError: Failed to fetch; no snackbar"In Service"; table behind still shows the old statusGET /queue-entry); no page errors. Pressing Serve again sends only theendedAtPOST and the backend refuses it — "Error updating queue entry / Invalid Submission" — so a second press cannot double-transition the entryassignticketPOSTassignticketPOSTassignticketPOSTs, 85ms apart, identical bodiesassignticketPOSTIn every failing scenario the page stayed interactive and no reload was needed.
The error handler is not allowed to be the next crash
The whole change is failure handling, so the failure was fuzzed:
getErrorMessageand both catch blocks were fed the range a rejectedopenmrsFetchcan carry — a plainError, an empty one, aTypeError, a bare string, a number,null,undefined,false,{}, aResponse-like object with and withoutresponseBody,responseBodyas a string,erroras a string,rawMessage/translatedMessage/message,fieldErrorswith no message, a null-prototype object, an array, a frozen error. All of them produce a readable snackbar and nothing escapes.Two did not, and both are fixed above:
messageis not a string reachedshowSnackbaras an object, which React cannot render;showSnackbaritself failing, say — escapedrunActionas an unhandled rejection, putting back the exact crash the net exists to prevent. The reporting is now guarded, sorunActioncannot reject.The last-resort net also only wrote to the console at first. That leaves the production half of the defect — a click that appears to do nothing — in place for any failure an action does not report itself, so it shows a snackbar too.
Accessibility
This change replaces a silent failure with a visible message, so it was checked against what a
keyboard-only and a screen-reader user actually get. Driven on the same standalone with Playwright,
with
axe-core4.12.1 injected into the running page.Fixed here: the click no longer destroys focus.
disabledremoves a button from the tab order,so pressing Call dropped focus onto
document.bodybefore the request had even been sent —measured, not inferred. Getting back to the button to read the outcome or retry then meant tabbing
in from the top of a queue table that is one row per waiting patient. The button now carries
aria-disabledandaria-busyinstead; focus was re-measured on the running page and stays on theCall button through the request and after the failure. The
useSingleFlight()ref, not theattribute, is what discards the second click; because an
aria-disabledbutton is still clickable,that was re-measured on the running page after this change — a rapid double-click on the inline Call
button sends one request, and a deliberate click once the first has settled sends a second.
Carbon's
--btn--disabledclass was deliberately not used to keep the greyed-out look, because itpairs the disabled colour with
outline: noneon:focusand would hide the focus ring on thebutton we are keeping focused; the trade is that a sighted user no longer sees the button dim for
the length of a request.
Not fixed here: the snackbar is announced to nobody.
showSnackbarrenders Carbon'sActionableNotification, which defaults torole="alertdialog"— a dialog role, not a live region.On the running page the snackbar node carries
role="alertdialog", noaria-live, and no ancestorwith one anywhere up to
<body>;.omrs-snackbars-containerhas neither a role noraria-live.An
alertdialogis announced only when focus moves into it, and the Carbon effect that would dothat looks for
button.cds--actionable-notification__action-button— which OpenMRS's snackbar neverrenders, because it passes
actionButtonLabel: ''. Focus was sampled before, during and after thefailure and never entered the notification. axe reports zero violations on the snackbar, which
is exactly the point: the markup is valid ARIA and still silent. If we ship without this, a
screen-reader user on a failed Call is left with precisely the silent no-op O3-5666 set out to
remove, because the message is placed in a
role="alertdialog"that nothing ever focuses. The fixbelongs in
@openmrs/esm-styleguide(snackbars/snackbar.component.tsx): a snackbar with no actionbutton should use Carbon's
ToastNotification, whose defaultrole="status"is a live region, orpass
role="alert". That was tried here as an experiment and does reach the DOM — but only becauseSnackbarDescriptor's unknown keys happen to be spread onto the Carbon component, so it needs atype assertion, it leaves Carbon's two focusable "Focus sentinel" spans inside the announced region,
and it would patch three call sites of a defect that affects every snackbar in O3. It was reverted.
Other framework-level findings, all pre-existing and none introduced here:
role="dialog" aria-modal="true"and no accessible name — axearia-dialog-name, serious, the only violation inside the Serve modal.createModalFrameinesm-styleguide/modalsnever sets one andModalPropscarries onlysize. If we ship withoutthis, a screen-reader user is told "dialog" with no indication of which one, because the frame
has no
aria-labeloraria-labelledby.the navigation behind. If we ship without this, a keyboard user who opens "Serve patient" is
still standing on the Call button behind it, because the modal host renders
aria-modal="true"without a focus trap.
.omrs-snackbars-containersits before#omrs-apps-containerin the app shell'sindex.ejs, sothe snackbar's close button is reached by tabbing forward past the end of the document and
wrapping. If we ship without this, dismissing an error by keyboard costs a tab through the rest
of the queue table, because the notification host is at the top of the document.
tabindex="0" role="link"spans reading "Focus sentinel" inside everyActionableNotification. If we ship without this, a screen-reader user tabbing past a snackbarhears "Focus sentinel, link", because Carbon's default focus wrap uses AT-visible sentinels.
Checked and clean:
when the modal they opened closes. Carbon renders them as native
<button role="menuitem">, soboth keys fire
click— there is no one-key-only handler here.navigation and landmarks (
button-nameon the help menu, twomainlandmarks, the switcherlist, panel list items). None of them are in the queue table, the action cell or the snackbar.
snackbar's
subtitlewhile the translated wording goes into itstitle. No translated fragmentis concatenated with a server string, so nothing here depends on English word order.
Deferred, and why
Three review rounds accumulated a list of "not fixed, here is why". Each was re-judged on its merits
once the rest of the change was finished; four are now fixed and described above, two stay deferred.
Fixed in this revision.
the other of the two call sites it touches. Measured, not argued: two clicks 85ms apart sent two
assignticketPOSTs for one intended call. Fixed by sharinguseSingleFlight()between bothsites, and the inline button's guard was re-measured because R3 changed it from
disabledtoaria-disabledand anaria-disabledbutton is still clickable.showSnackbarthrowing. The earlierreason — a broken snackbar store breaks every modal equally — is true but beside the point: this
PR already guards the row-action path, so leaving this one unguarded meant the two paths it
touches behaved differently under the same failure. Now they do not.
still deferred: it needs either a compensating request or a different call order, and neither is
error handling. Both are also a worse trade than they look — a compensating "un-transition" is
itself a request that can fail, and calling the ticket display first would only move the
inconsistency to the other side. What is fixed is the part that was actively misleading: the
message no longer claims nothing happened when the entry has in fact moved, and the table behind
the modal is re-read rather than left showing a status that is no longer true. The bound on the
damage was also checked on the running standalone rather than assumed: pressing Serve again sends
only the
endedAtPOST, which the backend refuses, so a second press cannot double-transition.error?.message. Fixed with the rest of item 2. Thiswas not only an inconsistency in wording:
error?.messageis the wrapper's "Server responded with500 …" rather than the server's own text, and it is not guaranteed to be a string — which is the
React render crash this PR fixes for the other path and left in place here.
Still deferred.
getErrorMessageis not total — an error object with a throwing property getter still makes itthrow. Left alone, because there is no producer.
openmrsFetchrejects withOpenmrsFetchError,which assigns
messageandresponseBodyas plain own data properties, or with aTypeErrorfrom
fetch; neither carries an accessor. R2 fuzzed 25 shapes a rejection can actually carry andthis was not among them. Every call site the PR touches is now inside a guard that would catch it
anyway, so making it total would add a branch nothing can reach and no test can honestly kill.
useActionPropsByKey()runs three times per row — once for the cell, once for the inlinebutton, once for the menu item. Measured before deciding, in the package's own test environment:
two extra calls cost 3.7µs per row taken as a best-of-41 and 6.8µs as a median over 200 rows,
against 700–900µs per row for the action cell's own render (Carbon
ButtonplusOverflowMenu), which is itself one of seven columns. So the saving is under 1% of the one cell itlives in — around a third of a millisecond on a fifty-row table — and it sits inside the run-to-run
noise of the same measurement. Not worth restructuring three components for. Dropped.
Found while measuring the above, and not fixed here
The overflow menu never closes when an action is chosen.
ActionOverflowMenuItemis a wrapperaround Carbon's
OverflowMenuItem, andOverflowMenuinjects four props into whatever its directchild is —
closeMenu,handleOverflowMenuItemFocus,indexand aref— none of which thewrapper forwards. Carbon says so itself, on every render, in the development console: "
<OverflowMenuItem>detected missing
closeMenuprop.closeMenuis required to let<OverflowMenu>close the menu uponactions on
<OverflowMenuItem>." Measured on the running page, the trigger keepsaria-expanded="true"and the items stay mounted after one is clicked, so the menu hangs open behind whatever modal the
action opened. If we ship without this, choosing an overflow action leaves the menu open on top of
the row it belongs to, and arrow-key navigation inside the menu has nothing to drive it, because the
wrapper swallows the props Carbon uses for both.
It is one prop passthrough and it was tried: forwarding
closeMenualone also brings thedouble-click down to one request, because the item unmounts before the second click lands. It was
reverted and is not in this PR, for three reasons. It is a pre-existing defect of the wrapper,
not of the failure handling O3-5666 is about. It changes the behaviour of every overflow action in
every queue table, and the focus consequences would need re-measuring — R3's finding that focus
returns to the overflow trigger when a modal closes was measured under the current behaviour. And it
would fix the duplicate request only as a side effect of unmount timing, where the in-flight guard
fixes it directly and also covers the inline button. It wants its own ticket.
Corrections to earlier revisions of this description
visitQueueNumberAttributeUuiddefaults tonull. That is the per-column override inqueueTables.columnDefinitions; the top-level element this code reads defaults toc0c579b0-8e59-401d-8a4a-976a0b183519, a real "Visit Queue Number" visit attribute type in the demo data. The reference application does ship the top-level element as""inappdata/frontend/referenceapplication/config.json, though that standalone'sindex.htmlcarriesconfigUrls: [], so at runtime it falls back to the schema default. Either way the guard is warranted: the value is per-visit, not per-installation.visitQueueNumberAttributeUuidcase as producing "The queue name, ticket number, or calling status is missing." That message no longer exists; that case produces the ticket-number message, as the table above now shows.patientMovedButNotCalled.TRI-007. The visit attribute on the standalone readsTRI-042; that is the value in the request bodies quoted above.