Skip to content

Core: table_default sort_bar + DnD + bulk_actions_bar, Reorder/Values/Format utils, blank_to_nil sweep - #548

Merged
ddon merged 10 commits into
BeamLabEU:devfrom
mdon:dev
May 18, 2026
Merged

Core: table_default sort_bar + DnD + bulk_actions_bar, Reorder/Values/Format utils, blank_to_nil sweep#548
ddon merged 10 commits into
BeamLabEU:devfrom
mdon:dev

Conversation

@mdon

@mdonmdon commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Workspace-wide quality lift surfaced by this AI module sweep — most of it is reusable core surface that other modules can adopt incrementally.

<.table_default> extensions

  • New :sort_bar slot rendered above the toolbar (always visible in both card + table views; replaces the card-only :above_cards pattern for sort UI).
  • <.table_default_body> gains :rest, :global so consumers can wire phx-hook=\"SortableGrid\" + data-sortable-* attrs directly when manual ordering is active.
  • <.table_default_header_cell>: :inner_block is now optional so drag-handle / select columns can render as empty <th> without a placeholder.

<.sort_selector> manual mode

  • New manual_field attr — when sort_by matches, the direction toggle is replaced by a static drag-handle hint icon (asc/desc has no meaning for user-specified order).

<.form_section>

  • Now accepts :rest, :global so consumers can attach id, phx-mounted, data-* without a wrapper div.

New <.bulk_actions_bar> core component

  • Lifts the entities Data Navigator pattern: counter + action-button slot + Clear button. wrapper_class knob lets the consumer pick the inline-card look (default) or the sticky/blurred bar shape catalogue uses on its Categories tab.

New utility modules

  • PhoenixKit.Utils.Reorder.reorder/4 — two-phase index-rewrite primitive for drag-to-reorder list views. Schema-agnostic, configurable position field, payload cap (default 500), UUID-filtered against poisoned drops via PhoenixKit.Utils.UUID.valid?/1. Returns {:ok, count} so consumers can keep audit-log fidelity.
  • PhoenixKit.Utils.Values.blank_to_nil/1 + .presence/1blank_to_nil is the workspace's canonical nil | \"\" → nil helper (was duplicated across 7+ modules); presence/1 is the trim-then-blank-check sibling for HTML form values / URL query params that may carry stray whitespace.
  • PhoenixKit.Utils.Format.bytes/2format_bytes / format_file_size had grown 8 private copies across the media stack + upload controller. Single helper with :decimals / :unknown / :base (1024 vs 1000) opts so each consumer keeps its existing visual behavior without forking the algorithm.

In-tree migrations (clearing the duplicates)

  • Activity.Index LV → Values.blank_to_nil/1.
  • 8 media-stack / upload-controller call sites → Format.bytes/2 via one-line delegates so HEEx call sites are unchanged.
  • shared/components/video.exValues.presence/1; phoenix_kit_publishing/db_storage/mapper.exValues.blank_to_nil/1 (consumer-side, separate PR).

Test plan

  • mix precommit clean
  • All admin pages render without console errors (media browser, file_display, upload controller, activity)
  • <.table_default> consumers (AI endpoints, catalogue) work with both card and table views; sort_bar visible in both
  • <.bulk_actions_bar> default style matches entities Data Navigator inline-card look
  • Reorder.reorder/4 rewrite is transactional (negative-then-positive write order)

mdon added 10 commits May 18, 2026 22:13
- New <.empty_state> with three variants (compact / card / featured):
the workspace had inline copies plus two module-local versions; lifted
to core so projects / catalogue / entities / AI share one component.
- New <.sort_selector> — field <.select> + bars-arrow direction toggle
in a daisyUI join. Race-free contract: form's phx-change sends only
sort_by; arrow's phx-click sends only sort_dir. LV derives the
missing field from assigns instead of stale DOM.
- <.table_default>: new :card_body slot (fully-custom card content,
replaces prescribed card_header + card_fields), card_class attr
(string or 1-arity fn per-item), and :above_cards slot that renders
inside [data-card-view] (auto-hidden in table mode by the JS hook).
- <.sort_header_cell>: cursor-pointer, hover:opacity-80, select-none,
loading spinner + opacity dim + pointer-events block, faint
chevron-up-down hint on inactive columns. active_direction/2 and
sort_header_aria_sort catch-all tolerate atom-or-string sort.dir.
- <.admin_page_header>: strip the back-arrow rendering workspace-wide.
back and back_click attrs kept as inert no-ops so existing call
sites compile without churn.
- phoenix_kit_web.ex: auto-import EmptyState + SortSelector for every
LV using `use PhoenixKitWeb, :live_view`.
Card grid was capped at 3 columns from lg+ (1024px). On 1536+ viewports
the cards became uncomfortably wide (~750px each at 2560 with 3-col).
Adding a fourth column at 2xl breakpoint keeps cards ~280-320px wide
across every desktop size — readable, never cramped, packs denser on
ultrawides.
Two small wrapper components that lift the boilerplate every admin form
has been repeating inline:
- <.form_section title icon class body_class> — replaces the standard
card + card-body + card-title h2 wrapper. Optional Heroicon prefix.
- <.form_actions cancel_to submit_label submit_icon submit_class
submitting_label> — replaces the right-aligned Cancel link + Submit
button row. `submitting_label` defaults to gettext'd "Saving…".
Optional :inner_block slot for extra buttons before Cancel/Submit.
Both are auto-imported via phoenix_kit_web.ex for every LV using
`use PhoenixKitWeb, :live_view`. Consumers in the AI module migrated
in the companion phoenix_kit_ai commit; future module forms can adopt
them with a one-line swap.
Plain string attr couldn't carry rich content (links, badges). The
:subtitle slot lets callers drop a <.pk_link> inline — same rendering
shape (small muted text under the title), no extra wrapper.
Consumers in phoenix_kit_ai update in the companion commit.
Add `attr :rest, :global` and splat it onto the outer `<section>` so
callers can attach `id`, `phx-mounted`, `data-*`, etc. without wrapping
the component in an extra div. Needed for the playground response card,
which scrolls itself into view via a `phx-mounted` JS dispatch.
table_default gains a `:sort_bar` slot that always renders above the
table/cards (both views), replacing the card-only `:above_cards`
pattern for sort UI. `<.table_default_body>` now takes `:rest, :global`
so consumers can wire `phx-hook="SortableGrid"` and the matching
`data-sortable-*` attrs directly onto the `<tbody>` when manual
ordering is active. `<.table_default_header_cell>` makes `:inner_block`
optional so a drag-handle / select column can render as an empty `<th>`
without crashing.
sort_selector picks up a `manual_field` attr. When `sort_by` matches,
the direction toggle is swapped for a static drag-handle hint — sorting
by user-specified order has no asc/desc, and the visual cue tells the
operator drag-to-reorder is now the interaction.
New `<.bulk_actions_bar>` core component lifts the entities Data
Navigator pattern (`PhoenixKitEntities.Web.DataNavigator`) into a
reusable primitive: a counter + action-button slot + Clear button. The
consumer owns the `MapSet.t()` of selected uuids and wires per-row
checkboxes themselves; this component just renders the bar.
Drop the hard-coded card wrapper in favor of a configurable
`wrapper_class` attr that REPLACES (not appends to) the default. The
default still renders the inline-card look used by the entities Data
Navigator, but consumers can pass a sticky / blurred variant to match
the catalogue Items and Categories bars without forking the component.
Collapse the inner card-body div into the flex container so the
wrapper class controls padding too — catalogue's bar is flatter than a
card and shouldn't carry hard-coded `p-3` from inside the component.
Switch the count line to `gettext("%{count} selected", count: @count)`
so translators control word order, and drop the daisyUI horizontal
divider (catalogue doesn't use one; the gap-3 spacing is enough).
Module doc now spells out the two plug-in shapes explicitly:
entities-style inline card and catalogue-style sticky bar.
Two small workspace-shared helpers that were duplicated across modules:
- `PhoenixKit.Utils.Reorder.reorder/4` lifts the two-phase index-rewrite
primitive used for drag-to-reorder list views. Takes a schema, a list
of UUIDs in display order, and the position field name; runs the
rewrite inside a transaction (negative-then-positive indices so a
unique index on the position column wouldn't trip mid-update), with
a configurable max-payload cap. Non-UUID entries dedup-filtered via
PhoenixKit.Utils.UUID.valid?/1. PhoenixKitProjects.reorder_projects/2
is the reference consumer doing scope checks + audit logging around
this primitive; phoenix_kit_ai's reorder_endpoints/1 now delegates
here instead of inlining the same pattern.
- `PhoenixKit.Utils.Values.blank_to_nil/1` consolidates the
`nil | "" -> nil, other -> other` helper that had grown private
copies across 7+ modules (activity index, openrouter client,
catalogue, project form, etc.). Migrations of the existing copies
can land incrementally — this commit only adds the canonical home.
… Reorder
- Activity.Index drops its private blank_to_nil/1 clone and uses
Values.blank_to_nil/1 directly. Same call sites, same behavior.
- Reorder.reorder/4 now returns {:ok, count} (number of rows actually
updated in the positive-write phase) instead of a bare :ok. Lets
consumers like projects/audit-logging preserve the row-count
information they used to compute themselves. AI's reorder_endpoints/1
discards the count and keeps its :ok contract.
- New `PhoenixKit.Utils.Format.bytes/2` consolidates eight private
format_bytes/format_file_size copies that had grown across the
media stack and upload controller. Accepts integer / Decimal / nil
with `:decimals`, `:unknown`, and `:base` (1024 vs 1000) options so
consumers can preserve their existing visual behavior without
forking the algorithm. Six in-tree files now delegate via a
one-line `defp` to keep their template call sites unchanged.
- `Values.presence/1` is the trim-then-blank-check sibling of
`blank_to_nil/1`. The Video component used a private copy with
trim; mapper.ex's similarly-named helper was actually `blank_to_nil`
(no trim) and migrates to that instead. Real semantic split between
the two names is preserved.
@ddon
ddon merged commit 26a461f into BeamLabEU:devMay 18, 2026
ddon pushed a commit that referenced this pull request May 18, 2026
- admin_page_header: drop now-unused Icon import left after the
back-button removal; restores `mix compile --warnings-as-errors`.
- Utils.Values: add catch-all clauses to blank_to_nil/1 and presence/1
so non-string input (e.g. a list from a `key[]=` query param) passes
through / yields nil instead of raising FunctionClauseError. The
pre-PR helpers in Activity.Index and video.ex were permissive; this
restores that contract for the consolidated versions.
- Utils.Reorder: alias PhoenixKit.Utils.UUID instead of the inline
fully-qualified call, clearing a credo --strict suggestion.
mix precommit clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon pushed a commit that referenced this pull request May 18, 2026
mix format on Elixir 1.19 indents `<%= if %>`-block content and
separates multi-line defp clauses with blank lines. Reformat the two
PR #548 files so `mix precommit` is clean. No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon pushed a commit that referenced this pull request May 18, 2026
Complete the 1.7.112 entry to cover all unreleased work since v1.7.111:
PR #548 (table_default sort/DnD, bulk_actions_bar, empty_state,
sort_selector, form_section/form_actions, Reorder/Values/Format utils),
PR #544 (MediaBrowser folder management overhaul), PR #545 (V120
document-creator taxonomy), PR #547/#542 (i18n manifests), and this
session's post-merge cleanup. Re-dated to 2026-05-18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@mdon@ddon