The per-field "ask, don't silently overwrite" divergence-resolution
modal (owner Q5) — one row per PhoenixKitCRM.Mirror.diff/2 entry, a
radio choosing which side wins that field.
Pure presentation function component. The consumer LiveView owns
@conflicts (from Mirror.diff/2), @master, and @choices (the
in-progress selection — see below), and implements:
mirror_choice_changed — form phx-change; receives
%{"choices" => %{"<field>" => "keep_crm" | "keep_user"}}
for whichever radios are currently
rendered — merge into the server-tracked
`@choices` (whitelisted against the
current `@conflicts` field set; never
`String.to_atom/1` a submitted key).
mirror_resolve — form submit; same payload shape as above.
Re-fetch the CRM record and the User
fresh (records may have changed while the
modal was open) and call `Mirror.diff/2`
again before `Mirror.resolve/4` — the
whole reason `resolve/4` re-checks
divergence internally is so a field that
stopped diverging while the modal was
open is dropped, not blindly applied.
mirror_cancel_conflict — Cancel clicked / modal dismissedRenders nothing when @conflicts is empty — there is nothing to ask.
Why @choices exists — controlled, not derived from @master alone
Earlier this modal derived every radio's checked from @master alone
(checked={@master == :crm}) with no way for a consumer to represent an
in-progress selection. That made the radios UNCONTROLLED: the user's
click lived only in the DOM, so ANY parent re-render while the modal
was open (a flash, a PubSub event, a phx-change on the underlying
form) made LiveView's DOM patch re-apply the server-rendered checked
and silently revert the selection back to @master — Apply could then
resolve with choices the user never made. @choices (%{field_atom => :crm | :user}, default %{}) makes the modal a normal controlled
LiveView form: a field with no entry in @choices still defaults to
@master (so a consumer that never wires mirror_choice_changed sees
identical behavior to before), but once a choice is recorded server-
side it survives any re-render.
Usage
<.mirror_conflict_modal
show={@show_conflict}
conflicts={@mirror_conflicts}
master={:crm}
choices={@mirror_choices}
/>