Skip to content

Selector popup: language, either-or, instant feedback, catalogue-first drill, smart fees - #90

Open
mdon wants to merge 29 commits into
BeamLabEU:mainfrom
mdon:main
Open

Selector popup: language, either-or, instant feedback, catalogue-first drill, smart fees#90
mdon wants to merge 29 commits into
BeamLabEU:mainfrom
mdon:main

Conversation

@mdon

@mdon mdon commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What this does

Continues the item-selector arc after 0.24.0 — six threads from Max's reports plus a full external review sweep:

  1. Language fixes — list names now resolve "_name" (the multilang editor's key; the detail popup already did, masking the miss until a real bilingual catalogue), and ItemPicker falls back to the process gettext locale instead of a hardcoded "en" — including for an explicit locale={nil}.
  2. Either-or selection — a checkmark means no number entry (check in the leftmost column); a visible :qty column IS the amount flavour. Both only via the explicit inline_qty: true opt-in.
  3. Instant feedback — colocated QtySignal hook flips the row/card highlight the moment a keystroke changes the value (mirroring the server's accept set), phx-click-loading cues on every round-trip surface, and the :phoenix_live_view compiler so colocated hooks actually ship (they were silently dead on every deployment — the manifest never wrote). The README now documents the required host app.js import.
  4. Esc layering — with the details popup stacked, Esc closes only the top popup (server-side cancel belt here; the client-side half is core PR https://github.com/BeamLabEU/phoenix_kit/pulls — see companion PR).
  5. Catalogue-first drill — a multi-catalogue scope's root lists the catalogues as tiles ("for multiple catalogues we should first have the user choose a catalogue"); drilling into one behaves exactly like a single-catalogue root. BrowseState.{:set_catalogue, …} is membership-checked, singleton scopes refuse it, and the fetch re-ANDs the narrowed catalogue.
  6. Smart fees where the price goes — flat standalone fee = a real price (line totals included), percent renders "12%", a fee item missing its number says a localized "Computed". fee_note is part of the presented-map shape and the confirm payload.

Review sweep

A 4-seat AI panel (risk-sliced briefs: state/security, rendering/fees, core JS, UX model) + the quality-sweep playbook's four triage agents reviewed the full batch; every finding was verified before acting (~⅓ did not survive). Fixes that came out of it are in the last commit — the notable one: the auto-load sentinel now ships as a colocated hook targeting the component, because core's InfiniteScroll pushes to the root LiveView on every published core, which crashed hosts without a load_more clause. Full record in dev_docs/pull_requests/2026/ (added under this PR's folder after numbering).

Deferred, recorded in the review doc: scoped tile counts, a deliberate a11y pass over the table view, the 50-file detail cap.

Gate

mix precommit clean; 2 doctests, 2194 tests, 0 failures with a database attached. Browser-verified on max-dev (demo page: default/card/client/checkbox/cart/multi flavours).

⚠️ Companion core PR (PkDialog stacked-cancel relay + CID routing, JS-only) should merge/release around the same time — this PR degrades gracefully without it (Esc closes both popups, as before), but the popup's Esc layering needs the host's vendored phoenix_kit.js refreshed from that core.

🤖 Generated with Claude Code

https://claude.ai/code/session_018bbQqpwP4bRCYWNtsJAksH

mdon added 12 commits August 31, 2026 01:00
tim-dev error report (2026-08-31): the language does not reach the
popup or the item inspection popup. Two real causes:

- Browse.present_items/2 read the wrong translation key — plain "name"
  where the multilang editor stores "_name" — so list names NEVER
  translated and always fell back to the primary-language column. The
  detail popup resolved "_name" and came out right, which is exactly
  how the miss stayed invisible until a bilingual catalogue: list in
  one language, inspection popup in another. Affected the selector,
  its tray names, and CatalogueBrowse.

- ItemPicker defaulted :locale to a hardcoded "en" instead of the
  process gettext locale, so on a localized page its dropdown,
  breadcrumbs AND its product-card popup stayed English unless the
  host passed the attr. It now resolves the process locale per update,
  the same fallback ItemSelectorModal and CatalogueBrowse share; an
  explicit attr still wins.

The test harness gained the integration shape that exposed this: the
HOST process carries the gettext locale and passes no attr (put via
?loc=), with ?clocale= for the explicit-attr override pin. New tests
cover list + detail through the process fallback for both components.
Max, 2026-08-31: a checkmark means no number entry (and it sits in the
leftmost column); numbers mean no checkmark. The one path that still
paired them was FORCING selection_mode="click" with a visible :qty —
exactly the shape integrators pin — where a selected row showed the
check icon/card badge plus a stepper. By default that flavour now
leads with the checkbox column, quantities live in the tray, and the
qty cell shows the picked amount read-only. inline_qty: true is the
deliberate both-signals opt-in, restoring the legacy pairing (no
checkbox column, check plus stepper-on-selected) for hosts that
really want it; quantity mode ignores it. Derived defaults were
already either-or and are untouched.
Latency audit of the popup (Max, 2026-08-31: 'I add 1 and it gets
highlighted blue but only after a delay'). What must stay a server
trip stays one — every fetch, every selection decision, the view
toggle (a dual-face client switch would duplicate the qty-form ids) —
but the FEEDBACK no longer waits for it:

- The selected highlight keys off data-selected instead of a
  server-computed class, and the qty stepper carries a colocated
  QtySignal hook that flips the attribute the moment a keystroke or
  arrow changes the value. State stays server-authoritative; the next
  render reconciles the attribute, and garbage input flips nothing.
- Every round-trip control now shows LiveView's client-applied
  loading state the instant it is clicked: pulse on rows, cards,
  tiles, chips, drills, Up, the mode switchers, column toggles and
  search-category hits; a spinner on Load more (its old server-assign
  gate could never show — the fetch is synchronous) and on Confirm;
  the search input pulses while its change is in flight. All pure
  phx-*-loading Tailwind variants — hosts without the variants just
  see today's behaviour.
The colocated hooks (ItemPicker's keyboard handling, Browse's QtySignal)
extract fine, but LiveView writes the phoenix-colocated/<app>/index.js
manifest from its mix compiler task — which this library never listed.
Hosts importing "phoenix-colocated/phoenix_kit_catalogue" therefore
could not resolve it, and the console showed 'unknown hook found' for
every colocated hook: they have been silently dead on every deployment.
Prepended like phx.new does — the task registers an after-:elixir
callback, so it must run before :elixir to attach at all.
Max, 2026-08-31: 'Esc closes both popups, but only top one needs to
go.' Chromium groups the close watchers of dialogs whose showModal()
ran without user activation (ours run from LiveView patches), so one
Esc fires cancel on BOTH stacked dialogs. Core's PkDialog now swallows
a dialog's cancel while a child dialog is open inside it; this adds
the server-side belt for hosts on an older core — a cancel arriving
with the details open closes only the details, and the selector's
data-show makes the hook reopen its natively-closed dialog with
search, scroll and selection intact.

Plus the quality-of-life sweep of the popup:
- autofocus on the search box (a fresh native dialog otherwise focuses
  nothing and the first keystrokes go nowhere)
- a clear (x) button in the search box
- the confirmable count on the Confirm button — with the cart off by
  default there was no running tally anywhere
- auto-load on scroll via core's InfiniteScroll sentinel (made
  component-aware in core), Load more staying as the fallback
- the results region scrolls back to the top when the level, search,
  mode or view changes (colocated ScrollTop hook); load-more keeps the
  offset
A scope naming SEVERAL catalogues — tim-dev's wide order picker — got
no tiles, no Categories|Items switcher and no search category hits at
all: the tree only built for single-catalogue scopes (Max, 2026-08-31:
'check why the categories and the sub categories aren't showing up',
against catalogues holding up to 49 categories each). The root level
now groups every offered catalogue's top-level categories under the
catalogue's display name (card sections / table divider rows); below
the root, drilling is catalogue-agnostic — category uuids are global,
Up and the scope enforcement work unchanged, and the fetch still
re-ANDs catalogue_uuids. The Uncategorized entry sums the offered
catalogues' loose items, and search category hits now run per offered
catalogue (capped like the admin's). Harness gained c2= for
multi-catalogue scopes.
Max, 2026-08-31: 'for multiple catalogues we should first have the user
choose a catalogue' — replacing the grouped flat root from the previous
commit. The root of a multi-catalogue popup now lists the offered
CATALOGUES as tiles/rows (the switcher reads Catalogues | Items, and a
catalogue tile carries its item total and a has-contents badge);
choosing one lands on that catalogue's own root — its top categories,
its own Uncategorized bucket, its items in Items mode — and Up climbs
subcategory, category, catalogue, catalogue list. BrowseState gained
{:set_catalogue, uuid | nil}: membership-checked against the scope's
offered list like every narrowing, clearing the category on change, and
overriding the fetch's catalogue_uuids from validated state only. Search
category hits narrow to the chosen catalogue. Tests pin the drill chain,
the per-level Uncategorized, the item narrowing per level, and that a
crafted out-of-scope catalogue uuid is a no-op.
Investigating 'smart catalogues show 0 items' (2026-08-31): that claim
was WRONG — smart catalogues hold their items directly and the popup
lists them fine; the demo's smart catalogues were simply empty seed
data. The real gap, verified against tim-dev's rule-priced service
item: a smart fee item rendered a BLANK price everywhere — list rows,
cards, and the details popup.

Browse.smart_fee/1 resolves what a surface can display: a flat
standalone fee IS the price per the smart-catalogues guide (it now
feeds the price column, the card price line, the detail Price row, and
line totals); a percent fee displays as its number ('12%'); a
rule-priced item with no displayable number shows a localized
'Computed' (et 'Arvutuslik', ru 'Рассчитывается' — hand-added to pot +
po + pinned). The fee honours show_prices exactly like prices do.
present_items grows a fee_note key; hand-built presented maps without
it keep rendering (Map.get).
The boss's post-merge review of BeamLabEU#89 (card select hit area, out-of-scope
Up climbs to the popup root, category hits only on a fresh fetch) plus
the 0.24.0 release commit and lib upgrades, merged under the local
batch so all three fixes survive the multi-catalogue and latency
rework.

# Conflicts:
#	lib/phoenix_kit_catalogue/web/components/browse.ex
A 4-seat AI panel (risk-sliced briefs) plus the quality-sweep playbook's
four triage agents reviewed the batch; every finding was verified
against code before acting. The ones that survived:

- The auto-load sentinel routed load_more to the HOST LiveView on every
  published core (core's InfiniteScroll pushes with no component
  target; the CID-aware routing is unreleased) — a host without a
  load_more clause crashed and remounted, dropping the popup and every
  pick. Replaced with a colocated .AutoLoad hook pushing through the
  sentinel's own phx-target, correct on every core.
- Both client-side list-identity keys (.ScrollTop, sentinel cursor)
  omitted the new catalogue drill — drilling catalogues landed the user
  mid-list and could wedge auto-load on equal page lengths.
- BrowseState: set_catalogue now refuses singleton scopes (the code
  contradicted its own documented 'only when several' contract) and a
  whitespace-only query no longer flips :direct drill to subtree
  listing (the fetch layer trims it to no filter). First unit pins for
  set_catalogue at all.
- The component refuses a crafted browse_category naming ANOTHER
  catalogue's category while drilled — a contradictory dead-end level
  the UI never offers (tiles and search hits are narrowed to the
  drilled catalogue).
- Translated names: all six ad-hoc "_name" || "name" chains routed
  through the presence-guarded Translations helpers — a stored blank
  override is truthy and blanked the display. ItemPicker's locale
  fallback also fires for an explicit locale={nil}, the common host
  shape put_new_lazy skipped.
- QtySignal mirrors the server's accept set (select_floor /
  zero_deselects) instead of flipping on > 0 — a rejected value
  produces no diff to undo a premature highlight, so the row stayed
  wrongly lit until the next real diff.
- Smart fees: the detail card formats a flat fee through
  Browse.format_price like the listing (49.00, not 49.505 vs 49.51);
  fee_note joined the documented presented-map shape AND the confirm
  payload — a percent-fee pick was indistinguishable from a free item.
- README documents the phoenix-colocated host import (it lived only in
  AGENTS.md, which the Hex package excludes).
- The multi-catalogue tree build logs its degrade instead of silently
  rescuing to "no categories", and the two tree clauses share one
  category_tree_base/4.
- Polish: smart_fee doc narrowed (rule-priced items with no fee fields
  are indistinguishable from plain price-less at presentation time),
  competing tooltips on the title button, @search_hit_cap named,
  confirmable_selection? deduped, stale grouped-roots comment, hook
  destroyed() guard, inline_qty x quantity and DB-scale decimal pins.

Plus dev_docs/pull_requests/2026/89-…/FOLLOW_UP.md for the boss's BeamLabEU#89
review (all three findings fixed by his own d8e545d; deliberate skips
recorded).

Gate: 2 doctests, 2194 tests, 0 failures; mix precommit clean.
mdon added 4 commits August 31, 2026 03:42
The Categories|Items (Catalogues|Items at a multi-catalogue root)
either-or read as a search-mode control — it sat beside the search
input, matched the index's search-for switcher visually, even carried
its 'Search for' aria-label — while doing something else entirely:
flipping what the LEVEL lists. Max and the external UX review tripped
over it independently, and its flat no-query list is served better by
search (which already answers with category hits + item results).

Default now: the root lists its tiles alone — catalogues at a
multi-catalogue root, categories otherwise, the Uncategorized bucket
included — and items come from entering a folder or searching. The
tiles-only root also skips its page-1 item fetch, which the BeamLabEU#89 review
had recorded as deliberately thrown away (the price of the instant
Items switch — no switch, no price). A category-less root still lists
the plain flat items.

root_switcher: true restores the old either-or for hosts that want the
flat browse mode (aria-label fixed to Browse — et/ru translated); a
crafted set_root_mode without it is refused, since it would reveal an
item block whose fetch was skipped. Harness grows rs=true; the flat
root-list tests opt in, and the default (tiles only, no switcher, no
item markup, crafted flip refused, search still answers) is pinned.

Also drops the 'Switch to Items' translation pin ahead of that
string's retirement in the detail-page commit.
⚠️ Reverses a fresh call: the index's three-state search mode (auto
item results, ?mode=items/catalogues) is the boss's 2026-08-31 design —
this commit is deliberately separate so it can be dropped alone if he
prefers the modes.

Max's ruling tonight: 'what if we did the search like we do in the
sub-categories — if anything matches we show the categories and the
items.' So the index now has ONE surface: a query filters the
catalogue listing (which stays, as navigation) AND renders matching
items below it — the popup search's two-list idiom. The attribute
filter is always offered and engages the item-results section by
itself.

Gone with the modes: the Catalogues|Items switcher (which looked
identical to the popup's browse switcher while meaning something
else — the collision that started this), the ?mode= URL state, the
items-mode-only toolbar hiding, and the auto-mode's whitespace-query
wart (BeamLabEU#89's deliberate skip — the engagement check now trims).
Legacy ?mode= URLs are ignored, not errors, and pinned as such.
⚠️ Reverses fresh design like the previous commit — the root either-or
was the 2026-08-29/30 detail-page ruling the popup then copied. With
the popup and the index both switcher-less, keeping it here would make
the detail page the odd one out; separate commit so the boss can drop
it alone.

The root is a pure outline (categories + the Uncategorized bucket);
its items live one drill down or behind search, which keeps the typed
All|Categories|Items REFINEMENT chips — those describe what a search
returns, which is exactly what a control next to a search box should
do. A category-less root simply lists its loose items now (the popup's
rule) instead of an empty outline pointing at the retired Items mode.

Gone: the search_mode URL state and its prior_* change tracking, the
set_search_mode handlers (including the return-to-outline expansion
choreography), items_mode?/show_categories_section? and every gate
they drove, and the 'Switch to Items' empty-state. The attribute
filter is always_visible — with the modes gone it is a primary control
on every level. Legacy ?mode=items URLs land on the outline, pinned.
@mdon

mdon commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Three commits added after Max's testing round tonight (9efbf63, b2c0614, 42e8dec): the root Categories|Items switcher is retired everywhere — opt-in root_switcher param in the popup, and one search surface on the index (query → matching catalogues + matching items, no modes) and the detail page (outline root + the typed refinement chips).

⚠️ Two of these deliberately reverse fresh design calls of yours — the index's auto/three-state search mode and the detail root either-or, both from this week. Rationale: the switcher read as a search-mode control while flipping the browse listing (Max and the external UX review both tripped over it within minutes), and the two lookalike controls next to search boxes meant opposite things on adjacent surfaces. They're separate commits so either can be dropped alone if you prefer the modes. Net −250 lines; legacy ?mode= URLs are ignored-not-errors and pinned; suite 2195 green, precommit clean.

…Back

Two fixes straight from Max's tim-dev testing round (2026-08-31):

Pointer cursors ('the mouse doesn't change on hovering on clickable
things'): the popup's tiles are <button>s, and browsers give bare
buttons an ARROW cursor — the admin's identical tiles point because
they are patch <a> links. cursor-pointer added to the shared card-tile
trigger's button variant (figure + title), the level table's name
buttons and the Uncategorized row (daisyUI's .link styles the text,
not the cursor, on a button), and the detail popup's thumbnail strip +
more-files tile. The item rows/cards already carried it. Pinned in
both views.

Live header ('use the header to show where you are… and the up button
should be back instead'): the modal header now names the level being
browsed — the drilled category, the Uncategorized bucket, or the
chosen catalogue, image/name/description from the tree already in
memory, so navigating costs no queries — falling back to the
host-scoped record (2026-08-30 behaviour) and then the plain title at
the root, where an explicit title attr still wins. The way back moved
there too: Back (left arrow) beside the context, climbing exactly as
Up did — category chain, then the catalogue level, then the catalogue
list — absent at the root. The old Up row above the tiles is gone;
context_header={false} keeps the whole header static as before.
@mdon

mdon commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Final round from Max's live testing (c9eeaf8): pointer cursors on every clickable popup surface (bare <button> tiles had the arrow cursor — the admin's identical tiles point because they're <a> links), and the modal header is now LIVE — it names the drilled category/bucket/catalogue (records from the in-memory tree, no per-navigation queries) with Back moved into the header replacing the old Up row. Gates re-run: 2197 tests, precommit clean. This PR is complete and testing has signed off; both dev boxes run this exact SHA.

mdon added 5 commits August 31, 2026 12:47
tim-dev error report (2026-08-31): WASTE SORTERS' subcategories were
missing from the popup while the data was perfectly healthy — two
active children in the same catalogue. Their per-category narrow
pickers pass the popup category_uuids: [cat] with catalogue_uuids:
nil, and build_category_tree keyed every clause off catalogue_uuids —
that shape fell through to the EMPTY tree, so the level rendered no
navigation at all (a flat subtree item list). Broken for
category-only scopes since the tiles landed in BeamLabEU#89/0.24.0.

The catalogue is implied by the scoped categories themselves, so the
tree now derives it — for the TREE ONLY: the browse scope, and every
fetch derived from it, stays exactly what the host passed (pinned:
out-of-scope items stay invisible). Only a single unambiguous
catalogue is injected; a scope whose categories span several keeps
the flat behaviour rather than inventing a catalogue-first root the
host never asked for. The Uncategorized gate is unaffected — the
category restriction still blocks the bucket.

Pinned with the report's exact shape: cat_scope with no catalogue,
children tiles offered, drill + Back + live header work, the fetch
stays scoped.
Three boss/Max calls from the morning testing round (2026-08-31):

- ITEMS: only the thumbnail is the look-closer gesture — supersedes
  the earlier title-joins-the-photo ruling. The table's name cell
  follows the row's select behaviour; the card title moves INSIDE the
  select button, which means that button can never render empty and
  the BeamLabEU#89 review's min-height patch for that case retires with it.
- CATEGORY/CATALOGUE tiles: the image enters the level exactly like
  the name — the level table's thumb cell (and the uncategorized
  row's folder icon) were inert; card tiles were already wired
  through the shared trigger.
- ADMIN index search: an item result links straight to the item's
  EDIT page — whoever searched an item by name wants that item, not
  its category page with the query re-applied and every sibling
  around it.
The catalogue and category editors already badge the tab with the
attached-file count; the item editor — the one with the most media —
was the one missing it (Max's report, 2026-08-31). Same shape, same
count as the section header below.
Two boss calls (2026-08-31):

- Images (and files) are reorderable after being added: the attached-
  files grid rides core's <.draggable_list> (SortableGrid), drag
  restricted to a handle so the file links and remove buttons keep
  working. Files had NO order anywhere — they sorted by inserted_at —
  so the editor's order persists as an ordered uuid list in the
  record's data["media_order"] (same save lifecycle as the featured
  pointer, via inject_attachment_data), applied by the new
  Attachments.apply_media_order/2 wherever the media renders: the
  editor grid itself and the detail popup's carousel + file list
  (ProductCard.resolve_images/resolve_files). Unknown files (new
  uploads, legacy records) keep their inserted_at tail order; crafted
  reorder payloads can neither lose nor invent a file. The item and
  catalogue editors get the grid; the category editor has no files
  grid (featured image only), so nothing to reorder there.

- The find-item-in-PDFs block moves from the top of the item edit form
  to the bottom, under the Save row.
Boss's trashcans follow-up (2026-08-31): after the subcategory-tiles
fix the scoped popup showed WASTE SORTERS' children, but not the items
filed directly on the category itself — with the root switcher retired
there was no Items mode left to reveal them, so they were unreachable
outside search.

The model Max confirmed: like the admin — (sub)categories at the top,
the level's own not-in-a-subcategory items at the bottom. So a scope
naming exactly ONE category now initializes the browse ALREADY DRILLED
into it: the first paint is the drilled level (child tiles + own
items + the live header naming it), not a synthetic outline above it.
Back stays hidden at that floor — the host said "this category", and
a Back to an outline that repeats the same tiles would loop; a root
tile's Back (level_up's "") remaps to the floor instead of the
retired synthetic root. Search hits now cover the subtree BELOW the
floor, which dissolves BeamLabEU#89's drill-to-the-standing-root dead-Up
scenario entirely — its pin is reworked to guard the new contract
(descendant-only hits, Back names the floor, never the out-of-scope
ancestor).
@mdon

mdon commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Post-signoff rounds from the boss's morning feedback (all deployed to both dev boxes):

  • ccfabcc only the thumbnail opens details (supersedes the title-joins-the-photo ruling — the title joins the SELECT surface instead, which retires the Item selector rework: admin parity, new defaults, per-user memory #89 empty-select-button min-height patch); category/catalogue tiles enter via image AND title; admin index search results link straight to the item's edit page.
  • 7aafd90 item editor's Photos and Files tab gets the count badge its siblings had.
  • 0c97937 WASTE SORTERS bug: category-only scopes (category_uuids with catalogue_uuids: nil — tim-dev's narrow pickers) got the EMPTY category tree since Item selector rework: admin parity, new defaults, per-user memory #89; the tree now derives the catalogue from the scoped category (tree-only, fetch scope untouched).
  • b8298d5 drag-reorder for attached files/images in the item + catalogue editors via core's <.draggable_list>, persisted as data[\"media_order\"] and driving the detail popup's carousel + file list; the PDF-search block moves below the Save row.
  • 803f3ee a category-scoped popup opens standing IN its category — the admin shape (subcategory tiles on top, the level's own items below); Back hidden at that floor; search hits cover the subtree below it. This dissolves Item selector rework: admin parity, new defaults, per-user memory #89's drill-to-the-standing-root dead-Up scenario; its pin is reworked to the new contract.

Suite 2203 green + precommit clean after each round. Companion: BeamLabEU/phoenix_kit_crm#31 (the manufacturers backfill the supplier import shipped without).

Max, 2026-08-31: the popup and the admin listed the same category's
items in DIFFERENT orders — the admin's default is document order
(position, name, uuid; apply_item_order/2), the fetch layer's was
name. The name default existed because position is per-(catalogue,
category) scope, so interleaving raw positions across catalogues is
meaningless — but the admin itself orders whole-catalogue lists by
position, so for any single-catalogue listing the caution doesn't
apply.

search_items/2 grows an explicit `order: :position` opt carrying the
admin's exact chain; BrowseState.query_opts/1 asks for it on every
BROWSE fetch scoped to one catalogue (drilled categories, the scoped
floor, the uncategorized bucket, a chosen catalogue in the multi
drill, category-less roots). A multi-catalogue listing keeps name
order — the one place position genuinely means nothing — and a live
SEARCH stays name-ordered everywhere, like the admin's results. No
global ordering system; the default look simply matches wherever
"the same" is definable.

Pinned at the reducer (which shapes get :order and which don't) and
at the DB (positions inverted against names distinguish the chains).
mdon added 2 commits August 31, 2026 14:20
The error report of the day (2026-08-31): column toggles silently
never persisted on tim-dev — their embeds passed no current_user, and
'no user, no persistence' gives no signal. The popup had no README
section at all (the moduledoc was its only doc surface), so the
most-missed attr in real integrations now leads the README's usage
snippet with the one-liner every admin page has at hand
(@phoenix_kit_current_scope.user).
The boss's report: subcategory images in the admin tables were inert
while the name beside them was a link. The thumb cell now wraps in the
same link as the title everywhere a title links: the detail page's flat
and tree category tables (patch to the category browser) and the
catalogues index's tree row and both simple-table sites (navigate to
the catalogue). Deleted and trashed rows keep the bare thumb, exactly
like their names stay unlinked. Cards were already covered by the
shared category_card_trigger.
mdon added 4 commits August 31, 2026 15:22
The 'items reorder is missing' report was not a regression: every
reorder affordance is position-sort-only by design, and the global
catalogue_sort_detail_items setting on the reporting box was name:asc,
which hides them all silently. The items section toolbar and the
items-only page header now say where the Manual sort lives instead of
just showing nothing.
Core's set_routing_info assigns url_path as the bare path, and the
language switcher rebuilds locale URLs from it - so switching language
inside a drilled category rebuilt the URL without ?category= and threw
the user back to the root. Both browse LiveViews now publish the full
path+query into :url_path at the end of handle_url_state (UrlState
keeps its own bare-path base assign, so state building is unaffected).
Pairs with the core fix teaching Tab.normalize_path to strip query and
fragment before tab matching.
The boss's report: 'New subcategory' rendered in English in the Russian
admin. Root cause: Gettext.gettext(Backend, ...) is the runtime API - a
plain function call the extractor never sees - so a string used only in
that form never enters the pot, and the completeness sweep (which counts
untranslated entries after extraction) has nothing to count. Most such
strings survive because the same msgid also appears at a macro call site
somewhere; three did not: 'New subcategory' (the category row menu, three
sites), 'Search for' (the search aria-label), and 'That category belongs
to another catalogue.' (an item-form flash - that module never had
use Gettext at all).

All six call sites now use the extractable macro form, the pot and po
files are regenerated (the merge's fuzzy guesses corrected - it matched
'New subcategory' to 'New category' and gave 'Search for' a msgstr with
a binding the msgid does not have), et/ru filled in, and the three
strings pinned in the gettext guard test alongside the drag-reorder
hint. A workspace-wide sweep found the same defect class in core
(fixed there) and queued gaps in staff, locations, db, ecommerce and
billing.
The boss's report: photos invisible in the item selector popup while the
same items show them everywhere else. Every server-side factor verified
healthy on the reporting box - the items carry featured_image_uuid, the
popup's own fetch+present pipeline produces signed thumb URLs for the
exact items in the report, those URLs serve 200 with image/jpeg through
the public proxy, the deployed source and compiled beams are current,
and the served CSS carries the thumb classes. The rendered rows reserve
the thumb column's width but paint nothing, and the no-image letter-box
fallback is absent - so the img elements are there with a src and never
load. The one browser-only ingredient the popup has that the healthy
admin tables lack: loading=lazy images patched into an open top-layer
dialog, where Chromium's lazy-load intersection handling is unreliable.

Inside a size-capped modal lazy loading buys nothing - a page of 24
thumbnails is a handful of kilobytes - so the modal-context images
(browse card + table row, the detail popup's gallery, the tray, the
attribute-set modal) load eagerly now. Admin page images keep lazy.
@mdon

mdon commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Five more commits from today's error reports (95887ecbdb8419):

  • Admin thumbnails enter their record (95887ec) — subcategory/catalogue images in the detail + index tables now carry the same link as the title beside them; deleted/trashed rows keep the bare thumb.
  • Manual-sort hint (fa0872b) — the "items reorder is missing" report was the global catalogue_sort_detail_items=name:asc hiding the position-only affordances by design; the items toolbar and items-only header now say where the Manual sort lives.
  • Language switch keeps the drilled category (e9d23b4) — both browse LVs publish path+query into :url_path; pairs with core's normalize_path fix on #777.
  • Untranslated popup strings (9c2549a) — "New subcategory" (the row-menu item you saw in English), "Search for", and the cross-catalogue flash were Gettext.gettext(Backend, …) runtime calls — invisible to the extractor, so they existed in no catalogue. Converted to the macro form, extracted, et/ru filled, pinned in the guard test.
  • Popup photos (bdb8419) — every server-side factor on the reporting box verified healthy (data, signed URLs, CSS, deployed source); the one browser-only difference from the healthy admin tables was loading="lazy" images patched into an open top-layer <dialog>, where Chromium's lazy-load handling is unreliable. Modal-context images now load eagerly (a capped modal page is a handful of KB). Please re-check the popup on tim-dev — if photos are still blank there, a devtools look at one row's <img> will tell us the rest.

Suite 2205 green + precommit per commit; deployed to max-dev and the tim-dev preview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant