Skip to content

[6.x] Entry index page#19212

Open
brianjhanson wants to merge 113 commits into
6.xfrom
feature/element-index-fable
Open

[6.x] Entry index page#19212
brianjhanson wants to merge 113 commits into
6.xfrom
feature/element-index-fable

Conversation

@brianjhanson

@brianjhanson brianjhanson commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Ports the entry index page over to inertia and Vue. Not 100% yet, but it's a big enough chunk of work and works well enough that I think it's worth a merge.

In Craft 5, when an element index was rendered, an XHR request with data plucked from local storage was immediately sent back to the server with information about how we wanted to render that view. With Vue and inertia, the initial Vue is now rendered with the data up front.

Element indexes are made up of a hand full of composables that will cover off on the functionality. All of these composables are route agnostic, so hopefully we'll be able to re-use them to provide their functionality to any table or route in the future and not have everything coupled as tightly as it is today.

Misc. updates

  • When you pass a name attribute to a <craft-button-group/> it will essentially function as a radio button.
  • craft-action-menu now extends craft-popover which is a generic popover web component. Much like the corresponding element in Web Awesome
  • craft-button will now only apply icon styles when icon is empty which will now allow you to pass in an icon name to create a button with an icon prefix or suffix
  • <craft-button/> now supports an icon-position attribute which you can use to render the icon in either the prefix or suffix slot
  • <craft-checkbox/> now supports the label-sr-only property
  • <craft-nav-item/>s can now disable the subnav toggle or render it into a prefix slot
  • URL query params are the source of truth throughout so CP index urls can be shared amongst users.
  • All six element-index XHR controllers now method-inject ElementIndexRequest; Laravel caches controller instances on the Route object, so constructor-injected FormRequests served stale data across in-process requests.
  • Legacy notifications (Craft.cp.displayNotification, the "Entry copied" notice) were silently no-oping on Inertia pages — the #notifications container now gets created outside the Vue root at bootstrap.

Not yet implemented

  • Structure view mode with drag-reorder
  • inline editing
  • export
  • the interactive Move-to-Section picker (serialized but disabled),
  • per-user column prefs beyond localStorage,
  • migrating the other element types.

The legacy Twig index and element-indexes/* endpoints remain untouched for all other element types and modal/field contexts.

brianjhanson and others added 30 commits May 28, 2026 17:09
I ran into some issues going full inertia, namely that elements are able
to render their own HTML within the table, which got me scared. This
takes a step back and tries to find a different middle ground for
rendering the table.

I'm still not sure it's right though
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…element-indexes-entries-assets-users-convert-to
…o brian/cms-2148-cp-element-indexes-entries-assets-users-convert-to
…element-indexes-entries-assets-users-convert-to
…element-indexes-entries-assets-users-convert-to
…hub.com:craftcms/cms into brian/cms-2148-cp-element-indexes-entries-assets-users-convert-to
…element-indexes-entries-assets-users-convert-to
…element-indexes-entries-assets-users-convert-to
brianjhanson and others added 21 commits July 2, 2026 09:12
Any focusable light-DOM element now gets a visible focus ring on keyboard
focus. Placeholder treatment. Supersedes the per-component row/card rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Row/card :focus-visible styles are now covered by the global rule in
base.css, so remove the redundant component-scoped copies. (These two
files also carry incidental Prettier reflow from a prior format pass.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The New entry button now falls back to a section menu when the selected
source isn't a creatable section (e.g. All entries / singles). Source
links carry real hrefs so switching sources updates the URL query, and
index columns use accessors so sortable headers render sort buttons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Folds the full element index feature set from the previous branch into
the ElementIndexes-service architecture:

- Frontend modules/elements: BaseElementIndex shell + DataTable + cards
  view, selection (shift-click ranges, keyboard nav), bulk actions bar,
  view-mode switching, per-source column/sort customization persisted to
  localStorage, sources sidebar, and the publishable-sections New entry
  button; AdminTable becomes a thin wrapper over the shared shell.
- Backend: ContentIndexController now delegates query building, view
  state, pagination (with page clamping), bulk-action serialization,
  column/sort metadata, and row/card serialization to the ElementIndexes
  service; EntriesIndexController absorbs the section-handle redirect.
- CP package: craft-badge, craft-thumbnail, craft-popover, craft-empty,
  status/size/volume constants, attrs() utility, thumbnail loader, and a
  global :focus-visible keyboard-focus indicator.
- Fixes from the fold: table cells render through the element's
  getAttributeHtml() pipeline (the renderer bypass crashed on Entry's
  authors attribute under Pro); stale tests for the branch's abandoned
  ElementIndexService/Params classes removed; thumbnail markup tests
  updated for craft-thumbnail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stories for BaseElementIndex, DataTable, ElementCards, AdminTable, and
ElementStatus under an Elements group, backed by shared sample-entry
fixtures with a client-side TanStack table so selection, sorting, and
pagination are interactive in isolation. An Overview MDX documents the
module architecture, the useElementIndexSelection composable, bulk
actions, and accessibility behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Register content/{section}/new before the index route it was shadowed by
- Remove dead code: DisplayedInIndex::applyIndexSort(), the unused
  criteria/contentHtml/canHaveDrafts/baseSortOptions page props, and the
  Craft.defaultIndexCriteria default that coupled the page to the legacy
  bootstrap
- Render only the visible table columns per row (client sends its column
  selection; indexData() eager-load-preps exactly what renders) and
  refetch rows when columns are toggled, restoring persisted selections
  into the URL on load
- Serialize real sort options (element-type + field-layout, string
  attributes only) and drive the View popover's Sort select from them,
  starting a newly chosen attribute at its defaultDir
- Select the source for section-handle URLs, including the singles
  source, and redirect entries/singles accordingly
- Method-inject ElementIndexRequest in all six element-index XHR
  controllers (route-cached controller instances froze the
  constructor-injected request across in-process requests)
- Type the table prop as TanStack's Table<any> across the index
  components, fixing the undefined-row and CSS-var typing gaps it exposed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… into feature/ineria-element-indexes-filter-hud

Resolves the ContentIndexController conflict by adopting the ElementIndexes
service refactor and re-applying the filter HUD condition to the element
query via a new $condition parameter on ElementIndexes::buildQuery().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m:craftcms/cms into feature/element-index-fable
Copy mirrors Craft 5: it runs client-side, storing the selection in
localStorage via Craft.cp.copyElements() so paste targets (nested element
managers) and other tabs see it. The serializer emits it as an 'event'
action (craft:copy-elements); the bulk-actions bar merges the live
selection into the event detail and owns the listener. Non-copy actions
are untouched.

Also creates the #notifications container on Inertia pages (outside the
Vue root) and re-points the legacy CP singleton's cached reference — all
legacy notifications, including the persistent 'Entry copied' one, were
silently no-oping without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These were committed on the old element-indexes branch and carried in by
the merge; builds now output to cms-assets/resources/build (already
ignored), so nothing writes here anymore. origin/6.x has no
resources/build at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The composables and ElementSources no longer import the content/index
wayfinder helper. The page supplies a minimal ElementIndexRoute
({url(query)}) and a shared createIndexVisitor owns the visit plumbing —
current-query merging (including stripping serialized key[0][...] forms,
which the sort and view-mode composables previously lacked), and the
preserveState/preserveScroll/only/replace semantics.

Also fixes two latent bugs the coupling hid: ElementSources hardcoded
{page: 'entries'}, so source links on any other content page pointed back
to the entries page; and the page never passed viewMode to
ElementSources, so its documented view-mode carry-through never ran.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@riasvdv Feels like you should probably put some eyes on this one. I'm not sure this is the best place for it, but I wanted somewhere to get some logic out of the controller.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all very HTTP bound so I think this could work as a ViewModel, or a base ElementIndexViewModel which can be extended for entries as EntryIndexViewModel, accepting the necessary variables that change the result (like elementType) in its constructor.

Any variables from the request can be retrieved directly inside the ViewModel so wouldn't need to be passed into the methods, or an ElementRequest could be passed into the constructor to make it explicit.

Any public properties and public methods get turned into props through toArray if you extend the base ViewModel

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored in fee058f

brianjhanson and others added 7 commits July 6, 2026 14:47
One kernel, two formatters: the trait's domain logic (source resolution,
query-state building with source criteria/conditions/filters/collapsed
descendants, filter-HUD query params, exporter availability) now lives on
ElementIndexes with explicit parameters, and the request-shape knowledge
(viewState, fieldLayouts, returnUrl, criteria, filter config, collapsed
ids) moves onto ElementIndexRequest as parsing helpers.

All six consumers — ContentIndexController, the element-index XHR
controllers, and ElementIndexResource — consume the service directly, so
the Inertia page and the legacy getElements() path share query building
rather than parallel copies. The controllers' stashed $this->request
property (a workaround for route-cached controller instances) is gone;
actions read their method-injected request and pass values explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors Craft 5: each time the table-columns popover opens, the checked
columns group to the top (keeping their display order) so they're easy to
drag into new positions. The list is an open-time snapshot — toggling
checkboxes while it's open never re-sorts rows out from under the
pointer; only the next open regroups. The snapshot refreshes when the
available column set changes (source switch), keyed on option values
rather than array identity so recomputes from checkbox toggles don't
trigger a regroup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The persisted per-source `visible` array now drives everything: the
table renders columns in that order, checking a column appends it to the
end (of the table and the persisted order — the checkbox itself stays
put in the open popover), and dragging in the View popover rewrites the
order to what's displayed. Unchecked columns carry no persisted order
and always present alphabetically below the visible block.

This replaces the separate per-source `order` array, whose
natural/server ordering leaked into the popover's top block and made
items appear to shift between closes and opens. The popover snapshot now
refreshes on a set-change signature (sorted values), since the canonical
option order itself changes on every toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The HTTP-bound payload assembly moves off the ElementIndexes service into
a ContentIndexViewModel (following the existing ViewModel pattern: public
zero-arg methods are payload keys, typed for the TypeScript transformer).
Shared intermediates — resolved source, query, indexData, paginator — are
memoized privately since payload methods can run in any order, with the
indexData-before-paginate ordering encoded explicitly.

ElementIndexes slims to the kernel shared by every index surface (source
resolution, query-state building, filter-HUD params, exporters), and the
controller drops to hook wiring + the view model.

Also fixes a serialization bug the swap surfaced: the controller merged
hook context and payload with Arr::merge, whose recursive semantics
interleaved the hooks' legacy assoc-keyed tableColumns with the payload's
list into a JSON object (previously masked by the value being a
Collection). Now a plain array_merge with the view model last, pinned by
a list-shape regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collection's TValue template is invariant under PHPStan, so the
branch-narrowed member types produced inside map() could never satisfy a
declared Collection<int, array{...}> shape. Array shapes are covariant,
and both consumers immediately converted the Collection to an array
anyway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@brianjhanson brianjhanson marked this pull request as ready for review July 6, 2026 21:16
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