Skip to content

feat(admin): Phoenix reference admin (Live use, actions, export, history, i18n) - #131

Draft
karim-semmoud wants to merge 85 commits into
mainfrom
feature/corex-admin
Draft

feat(admin): Phoenix reference admin (Live use, actions, export, history, i18n)#131
karim-semmoud wants to merge 85 commits into
mainfrom
feature/corex-admin

Conversation

@karim-semmoud

Copy link
Copy Markdown
Member

Corex Admin is the Phoenix staff admin: host Resource modules, generic LiveViews you rarely copy, and Corex UI exclusively.

This revision flattens the index command bar and applies the same layout chrome on every admin page (index, new, show, edit, home):

  • Search is only the input (no .typo form card). Filters, selection count, bulk delete, and bulk export stay on one row.
  • Padding lives on main.admin-main (slightly less than docs px-space-xl py-size), not a nested content wrapper.
  • Forms, data lists, and tables are full width of main. Index tables use a larger viewport max-height.

Tests

  • cd admin && mix test
  • cd admin && mix credo --strict
  • e2e LiveView coverage for command bar, admin-main / admin-content, and Arabic chrome
Open in Web Open in Cursor 

karim-semmoud and others added 30 commits August 15, 2026 12:57
1.2.8 is CVE-2026-64941; LiveView 1.2.x omits internal liveSocket.main from types, so canPushEvent now duck-types getSocket and optional main.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stop reading internal liveSocket.main so hooks follow the documented LiveSocket API.

Co-authored-by: Cursor <cursoragent@cursor.com>
mix assets.build so npm consumers get getSocket().isConnected() instead of internal liveSocket.main.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep root, e2e, and installer on the same Elixir/Erlang/pnpm/Postgres versions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Ship a governed LiveView backoffice package and an e2e /admin demo, and ignore admin Mix artifacts so _build, deps, and doc stay out of git.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Use the full shell for filters, forms, and pagination, surface table sort and
selection, and add a Corex nested_fields widget with an embeds_many DSL on Tickets.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reuse the site header on Admin, replace the sidebar with a resource tree,
drop teal index breadcrumbs, and lay out filters as a collapsible toolbar
with the stock range date picker. Keep the table footer on one row, make
selection checkboxes visible, fill show/edit to the content pane, and
surface field errors as tooltips. Seed a session-scoped Posts resource
under Content.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Admin chrome utilities (flex-nowrap toolbar, max-w-none field hosts,
sticky sidebar offset) live in the admin package, which e2e's Tailwind
@source list did not include, so those classes never shipped in app.css.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Admin LiveViews no longer depend on host Tailwind scanning. Layout,
filters, footer, field hosts, crumbs, and home cards use semantic
classes from the new admin.css component in the Corex CSS bundle.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
The shared site header still uses max-w-none, so a full-page refute
was matching chrome outside Corex Admin LiveViews.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Keep the page-size select as an inline label+dropdown instead of a
full-width stacked native-input, and drop the full control-size
override so table checkboxes follow ui-size-sm.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Cap the filter search field and center its icon. Keep range date
pickers open until both dates are chosen so the first click does not
patch the table.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
The footer page-size native-input was still stretched by `.admin .native-input { width: 100% }`. Override that after the stretch rule, reset form chrome, and lay the label beside the select.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Pin selection controls to the checkbox 0.6 scale of the table --ctl-size so they no longer fill the full control height in compact rows.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
`.admin .native-input` was stretching the filter search to full width. Restore a container-sm max-width after that rule, and center the magnifying-glass heroicon in the compact control.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Selecting the first range date was also firing the search form phx-change, which patched the index and remounted the picker. Ignore incomplete date ranges on both filter and search so the calendar stays open until From and To are set.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Move non-search filters out of the search form so picking a From date no longer fires phx-change and remounts the date picker. Compare parsed list opts before patching so empty form fields cannot navigate either.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Bring in Slider and the latest Zag/Phoenix/LiveView/Credo dependency updates from main.

# Conflicts:
#	installer/priv/static/corex/GENERATED

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Raise the e2e and integration_test floors to ~> 0.22.4 so Postgrex.stream/4 rejects :comment injection (GHSA-3gww-3f36-2388).

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Controlled checkboxes ignore data-state after mount, so Zag only
updates when set_checked is pushed. Select-all already did that;
row clicks now push checkbox_set_checked for the clicked row too.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Keep a stable command bar so selection does not jump the table.
Move search and the Filters trigger there, use a square count badge,
and drive per-page with Select events instead of a form.

Add filters_open and default_filters on the resource spec, Reset /
Reset all in the filter panel, a CSS grid for filter controls, and
toggle-group / combobox / row kebab menus where they fit.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Make toggle/select filters follow server value after Reset, and wrap
the row kebab icon in a square button so the menu is easier to open.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Navigating from a menu item disconnects LiveView. Pushing the select
event afterwards threw "LiveView not connected" and aborted Show/Edit.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Wallaby clicks the real Zag controls so command-bar count, filter reset
vs chip X, per-page Select, and row Show navigation stay green.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Count selected rows via data-checked hosts, and click the visible open
menu item so Show uses the item's data-to path.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Zag may render menu content outside the hook host. Look up Show/Edit
items on the content node so js().navigate still runs.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Assert the row checkbox from input/ARIA state, and dispatch pointer
events on the open kebab item so navigation uses data-to.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
cursoragent and others added 30 commits August 27, 2026 18:33
Closed collapsible content stayed in flow/hit-testing on the command row.
Hide it when closed, and trigger the Status reset from the filters host in
Wallaby so the open select overlay cannot swallow the click.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Isolate the admin shell from body.typo article styles with data-scope=admin
so home, index, new, show, and edit are not painted as docs cards. Put
px-space-lg py-space-lg on main (slightly less than docs) and force hosts
to width 100%.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Match the e2e docs content shell: horizontal padding px-space-xl,
padding-block py-size, and a centered max-width. Use max-w-7xl
(--container-7xl) instead of the docs max-w-6xl so the staff
console is slightly wider. Keep layout-heading as the page title.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Replace the buried Filters panel with a Shopify/Linear-style bar: saved
views, pinned pills, and Add filter. Expand the resource DSL with text
operators, is/is not, number comparisons, id lookup, relative dates, and
tags/presence while keeping every control allowlisted from filters do.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Pinned filters each have a collapsible trigger, so the Wallaby helper
targets #tickets-filter-pill-status instead of the first match.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Drop the Is / Is not toggle group from enumerated filters unless a
resource opts into operators: [:in, :not_in]. Status stays a select.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Replace the All / Open / Done toggle group with a compact view select in
the command bar. canned_filters/0 still drives the options.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Drop collapsible filter pills for compact Select/Combobox/DatePicker/inputs
that dismiss on outside click. Stabilize the command bar so search never
shrinks, move Export and bulk Delete to icon-only actions, and use Corex
Checkbox in the export dialog. Compound filters and operators live in a
More filters dialog.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Unlayered Corex width:100% rules were expanding the views select and
hiding the icon-only Export/Delete cluster. Override them for the
command bar and filter row.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
…peline

The 1,432-line Live.Components module owned nav, every filter renderer, and
all dialogs at once, and filter state was parsed, merged, and encoded in five
different places. Neither could be extended without forking.

- CorexAdmin.State.Filters is now the only owner of filter parse/encode/merge
- CorexAdmin.Params replaces stringify_keys copies in five modules
- CorexAdmin.Filter is a behaviour; each filter type owns its own parse
- CorexAdmin.UI.* are public, slotted blocks (Index/Form/Show/Home/Nav/
  Filters/Dialogs/Fields) that a host LiveView can compose
- CorexAdmin.Resource.Spec is public API, since ejected views read it
- Field dispatch is one path: show now renders through display/1, so a custom
  field module no longer silently misses the detail page
- Fields gain render:/render_form: overrides, paths, relations, and columns
- Action gains icon/form_fields/confirm/destructive? so custom actions render
- Range filters move into their own dialogs; More filters holds the rest
- Create/Save semantics match other admins (create and add another, save and
  close)

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Query.apply/2 dispatched on value shape against the root binding only, so an
association could never be filtered, sorted, or searched. Worse, an
unrecognized shape fell through and returned the query unchanged, meaning a
malformed filter listed every row instead of failing.

- filters, sortable fields, and searchable fields accept path: [:author, :email]
  and are left-joined once as named bindings
- CorexAdmin.Query.Ref names the binding and column a filter targets
- a CorexAdmin.Filter module may implement apply/3 to build its own Ecto
  expression, which is the supported way to express joins or fragments
- unknown filter shapes and operators now raise

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
…ed booleans

Relations are the credibility gap: there was no way to pick an associated
record, and no panel for the records that point back.

- belongs_to and has_many render Corex select/combobox fed by a context
  function named in the resource, so the admin still never queries an
  association itself
- searchable relations ask for options as the user types
- has_many gets a related list panel on the show page from rows the context
  preloaded
- column/3 declares a read-only computed column backed by a field module
- exclusive: true on a nested boolean keeps at most one row set, enforced in
  params rather than trusted from the UI
- resources may implement filter_options/2, filter_bounds/2, and metrics/2 to
  drive choices, slider bounds, and metric cards from scoped data
- CSS for metric cards, detail rows, nested legends, scrollable dialogs, the
  compact range trigger, and a sort trigger that stays square

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
…three

The Action behaviour declared only handle/3, so the index could render nothing
but Delete, BulkDelete, and Export, matched by module identity. collection_actions
rendered nothing at all and the generic action events had no emitters.

- actions declare icon/form_fields/confirm/destructive?/chrome, and the chrome
  renders every registered action that asks to be rendered generically
- a bulk or record action with form_fields gets a Corex dialog with inputs, so
  'assign owner' is now expressible without forking
- action names from the client are matched against registered actions rather
  than converted to atoms
- Export explains that it runs through the controller instead of failing vaguely
- the test layout renders flash, so action outcomes are actually asserted

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Customization had two settings: use the DSL, or fork the package. This adds the
middle tier and makes the last one honest.

- mix corex.admin.gen.live --render writes a render/1 that composes public
  CorexAdmin.UI blocks, so reordering and wrapping cost no maintenance
- mix corex.admin.gen.admin copies block markup into the host namespace,
  rewriting only the ejected names so the shared imports and translated
  operator vocabulary stay upstream
- ejection records the version and digest it copied from in
  priv/corex_admin/ejected.exs
- mix corex.admin.doctor reports which copies have fallen behind and exits
  non-zero, which is the part Administrate and Torch never shipped

Ejection changes composition and markup, never behaviour: generated pages still
read @SPEC and still delegate events to the package controllers.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
The demo was two flat tables of synthetic rows, which showed the chrome but not
what the DSL can express.

- adds Authors, so Posts and Tickets have a real belongs_to and Authors get
  has_many related lists on show
- Posts gain a publishing state validated together with published_at, tags, and
  a featured flag; Tickets gain a due date and an assignee
- filters reach through associations (assignee name, author name)
- slider bounds, status options, and metric cards are computed by the context
  from scoped data
- a bulk action with a form moves the selected tickets to a chosen status
- seeds produce a queue that looks like one: spread of statuses, assignees, due
  dates, and priorities
- the exclusive preferred link rule is enforced in the changeset as well as in
  the submitted params

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
LiveView only serializes phx-change values from an enclosing form, so a
bare text or datetime-local input never sent its value. Wrap those
controls in a form so assignee/email and published-at filters actually
narrow the list.

has_many relations already have a related-list panel on show; drop the
duplicate detail row and keep an empty panel when there are no rows yet.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Resources now covers filter widgets and path joins, belongs_to/has_many,
exclusive nested booleans, filter_options/filter_bounds, metrics, and the
Create / Save action pairs. Customization is Resource → compose UI blocks
→ tracked ejection. Installation and security drop the deleted
Live.Components.nav_tree API and document the export token salt.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Point guides at public APIs and salt strings instead of @moduledoc false
modules so mix docs does not warn on customization and security.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
README now lists a reading order. Installation becomes Getting started
with a day-one checklist and a positive authorize example. Security,
Resources, and Customization drop repeated overlap; Field/Filter/Action
implementations live in Customization; eject + doctor move to a new
Eject guide. Resource and Page moduledocs and mix task help link the
same path.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
…ides

Keep Hex installation.md in sync with the README packages table, point
manual installation and update guides at the optional staff admin package.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Link the packages table to Hexdocs, mention the optional staff admin
after Corex wiring, and include corex_admin in the usual deps.update list.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Resolve e2e/mix.exs by keeping corex_design runtime: false from main
and the corex_admin path dependency from this branch.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Remove unreachable stringify_keys catch-alls, match Code.ensure_loaded,
and type list_items as [Corex.List.Item.t()]. Tighten the e2e bulk-delete
assertion now that index rows lead with an id cell and seeded titles share
prefixes.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Use static ~W/~S sigils, handle :ok match errors in bulk status and
author metrics, and exclude author_resource from the CSRF false positive
like the other admin resources.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
set_ticket_status returns a count directly, and author metrics unwrap
the always-ok page result without a dead {:error, _} branch that broke
warnings-as-errors.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Reformat long lines in UI, eject, list_opts, export, and query tests so
admin mix lint passes format --check-formatted.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
Document index LiveView callbacks, replace single-clause with/else with
case, filter export fields with a comprehension, split format_value
heads, parse eject manifests as map literals without eval, and avoid
Module.concat when rewriting ejected block names.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
put_resp_content_type now takes hardcoded csv/json MIME strings so
sobelow --exit no longer fails on XSS.ContentType.

Co-authored-by: Karim Semmoud <karim-semmoud@users.noreply.github.com>
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.

2 participants