Merging dev broke this branch: SH-126/SH-184 made Add-On a backend-authoritative
indicator (isAddOn) and removed it from the WOType union, but
read-detail-closability-fields still accepted "Add-On" and mapped "AddOn" onto
it, so tsc failed on both the mapper and the closability test.
Drop Add-On from the detail type set so a legacy payload normalizes to "", and
cover the normalization directly instead of asserting it through
detailToClosabilityInput, which only forwards the already-typed value.
Three review findings on the additive add path:
- Create-mode reload refreshed selectedRoster (including rowVersion) but left
the form company fields on their pre-conflict values, so a retry diffed stale
fields against the reloaded roster and could overwrite the concurrent company
update that caused the 409. Reload now resets the form from the fresh roster,
matching the select path.
- mapVendorRosterAdditivePatchToBackend copied companyFields verbatim while the
reconcile write mapper canonicalizes companyPhone, so PATCH and PUT could send
different phone shapes for the same input.
- emptyRosterTechnician seeded preferredContact "Phone" even though that
control was removed from the form, so filling the first blank row submitted a
fabricated value. append already omitted it; both paths now agree.
An empty tradeOptions array meant two different things: "facets query still in
flight" and "vocabulary genuinely unavailable". Both enabled fail-open free
text and showed the outage warning, so a normal cold fetch looked like an
outage. Anything committed in that window was added to knownTradesRef and stayed
selectable after the canonical list arrived, permanently undermining the gate.
Expose the facets query isLoading as tradesLoading, thread it to the field as
tradeOptionsLoading, and keep the canonical gate active while loading. The
outage warning now shows only once the query has settled empty.
The pinned Unassigned section folded the queue query's pending state into
isFetching only, which the section never read. On first mount, once GET /board
resolved while the queue was still paging GET /board/search, the pin announced
"No unassigned work orders." instead of a loading state.
Expose the queue query's own isLoading as unassignedLoading, thread it through
the table data hook, and render an aria-busy loading row while it is true so the
loading and empty states are separately observable.
Adversarial review found a last-writer-wins window: after a stale 409 the
roster is refetched while the form keeps its pre-conflict values, so the
form-vs-roster diff resent our stale value for any company field another
user changed in that window, silently reverting their edit.
Transmit only fields the user actually edited, taken from react-hook-form
dirty state. Selecting a company calls reset(), so loaded values are never
dirty and only genuine edits are sent.
dirtyFields is resolved during render: formState is a Proxy that only tracks
properties read at render time, so reading it inside the submit callback
returned empty. The vendors E2E caught that; unit tests could not, since
they pass the flags in directly.