Admin LiveView for the Shopify → shop one-way sync.
Fetches products from the Shopify Admin API connection registered in
PhoenixKitEcommerce.Shopify.Provider, diffs them against the local
catalog (PhoenixKitEcommerce.Shopify.Sync.check/2), and lets an
operator apply changes — nothing is written without an explicit click.
When the Admin API token is rejected, Sync.check/2 falls back to a
price-only storefront read instead of failing outright; this LiveView
surfaces that with a banner so an operator never mistakes a price-only
report for a complete one, and only ever renders the Prices section in
that mode (see visible_sections/2).
Changes are grouped into field sections (Prices, Titles, Descriptions,
HTML texts, Tags, Statuses, Vendors — in that order, price first). One
product's change can appear in more than one section if more than one
of its fields differs. Sections are collapsed by default and show a
count; expanding one reveals its rows, 25 at a time (see the module
attribute doc on @per_page for why pagination here is a correctness
requirement, not polish). An operator can apply a single field on a
single product, a whole section, or every pending change at once —
always through PhoenixKitEcommerce.Shopify.Sync's existing
apply_change/2 / apply_changes/2, never by writing to a product
directly.
Only updates products that already exist locally (matched by Shopify's
handle against the product's slug). A Shopify product with no local
match is skipped — creating new products is the CSV import's job, not
this sync's.
Applying changes: request → confirm, never a direct write
Every apply affordance (one field on one product, a whole section, the
checked rows in a section, or everything) is two-phase: a request_*
event validates the click and stashes what it would do in @pending,
then renders <.confirm_modal> describing it; only "confirm_apply"
(the modal's own confirm button) calls into Sync.apply_change/2 /
apply_changes/2. "cancel_apply" — and re-deriving what to write from
live @changes at confirm time rather than trusting whatever @pending
captured at request time — both exist so a stale or cancelled
confirmation can never turn into a write; see clear_pending/1 and the
do_confirm_* functions. This replaces the page's previous
data-confirm (a bare browser confirm(), which cannot show the
extreme-price exclusion notice below) with PhoenixKit's own modal.
Selection is a bulk scope, scoped to one section, one page
A checkbox column (<.bulk_select_scope> / bulk_select_cell) lets an
operator pick specific rows within one expanded section and apply just
that field to just those products via "Apply selection" — distinct from
"Apply section" (every row) and a single row's own "Apply" button.
Selection is client-side (see BulkSelectScope's JS-hook moduledoc) and
scoped to the section's CURRENT PAGE of @per_page; paging re-renders
the row set, which prunes any selection that isn't on the new page — so
selection deliberately does not persist across pages. Like "Apply
section", it excludes extreme price changes (price_extreme?) from the
bulk write; see split_bulk_eligible/2.