agentleFS
OrchardCMSOrchardCMS/OrchardCore/.agents/skills/orchardcore-admin-index-views/SKILL.md

orchardcore-admin-index-views

Skill8.2k starsChanged 15 days agoBSD-3-ClauseView on GitHub

Builds OrchardCore admin index (list) pages with the AdminList shape and the admin-list tag helpers — view-declared columns, row display drivers placing shapes for both the SummaryAdmin (List) and SummaryAdminGrid (Grid) display types, column providers, row actions, toolbar, search, pager and per-list alternates. Use for every new admin page that lists items, when converting a hand-written admin listing, adding or reordering columns of an admin list, extending another module's list, or changing how its rows and actions render.

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 3 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every admin page that lists items (an "index" page: content items, users, queries, workflows, ...) renders its items through the shared …

    line 8
  2. Distinctive:

    Do not add column or cell drivers, column metadata classes, or build both representations of a row.

    line 26
  3. Distinctive:

    Every visible piece of a row (checkbox, title, badges, dates, buttons, menu items) is its own shape in a zone.

    line 81
  4. Very specific:

    Do not use editor |Column groupings for cells.

    line 90
  5. Very specific:

    Import the tag helpers once per module, in Views/_ViewImports.cshtml:

    line 155
  6. Very specific:

    Use the shared bulk-select-list and list-search-filter components, wired by one small module per page (Assets/ts/*.ts), and keep the …

    line 196
  7. Very specific:

    Add the page's URL to _lists in test/OrchardCore.Tests.Functional/Tests/Cms/AdminListLayoutTests.cs when the Blog recipe enables its …

    line 253
  8. Distinctive:

    Check the page in both layouts and both actions layouts (Buttons and Menu), with rows and without, and in a narrow window, where the grid …

    line 256
  9. Very specific:

    Add the list to the table of lists in src/docs/reference/modules/Admin/README.md.

    line 258

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

OrchardCore Admin Index Views

Every admin page that lists items (an "index" page: content items, users, queries, workflows, ...) renders its items through the shared AdminList shape. Never hand-write a <ul class="list-group">, a <table>, a toolbar, a select-all checkbox, an item count, an empty state or a pager for an admin listing: the list renders all of them, in the List or Grid layout the site and the user choose.

Ownership

LayerResponsibility
ControllerQuery the raw items and supply an AdminListContext<TModel> with the typed Source, pager, search, toolbar and other region shapes. Never build the list shape or the row shapes.
ViewDeclare the list name, the grid columns, the CSS classes, the empty message and other presentation properties with <admin-list>, inside the page's form.
Row display driverReturn ordinary IDisplayResult shapes, each with a SummaryAdmin location (List) and a SummaryAdminGrid location (Grid). Set optional row classes/attributes on context.Shape.
Column providerOptional. Extend the columns a view declares, e.g. from another module.
AdminListResolve the layout at display time, build each row once with the display type of that layout, and render the whole listing.

AdminListLayout has exactly two values: List renders each SummaryAdmin row shape whole (its row template decides the look); Grid renders the SummaryAdminGrid row zones directly, one cell per column, each cell rendering the zone named after its column. Layouts are not discovered or registered through templates. Do not add column or cell drivers, column metadata classes, or build both representations of a row.

Decide

SituationDo this
New admin page listing itemsFollow the workflow below.
Rows already built with BuildDisplayAsync(..., "SummaryAdmin") and a hand-written listConvert: move the row building out of the controller into AdminListContext<TModel>.Source, add the SummaryAdminGrid locations, then follow steps 3 to 6.
Rows are hard-coded <li> markup in RazorWrite a display driver for the row model first, so a row is a zone-holding shape, then follow the workflow.
Adding a column to another module's listStep 5, plus a SummaryAdminGrid location in your driver.
The page edits a single entityNot this skill: use orchardcore-admin-edit-views.
The page arranges items rather than listing them (a tree, a canvas, a media library, widgets by zone)Not this skill: such pages keep their own presentation.

Workflow

1. Write the row display driver

A row is the shape IDisplayManager<TModel> builds for the item. Its type is typeof(TModel).Name plus the display type, e.g. IndexProfile_SummaryAdmin, so register the driver against a domain model named exactly what the shape should be called, never a *ViewModel. Register it in the Startup of the feature that owns the list: a driver registered by a neighbouring feature leaves every row empty on a site where that feature is off.

Give every shape a location for each display type. The shape types and templates are the same; only the zone differs, a row zone for List and the zone of a column for Grid:

public override Task<IDisplayResult> DisplayAsync(IndexProfile profile, BuildDisplayContext context)
{
    // The row's classes and attributes are rendered on its row element, in both layouts.
    if (context.DisplayType is OrchardCoreConstants.DisplayType.SummaryAdmin or OrchardCoreConstants.DisplayType.SummaryAdminGrid
        && context.Shape is Shape rowShape)
    {
        rowShape.Classes.Add("item");
        rowShape.Attributes["data-filter-value"] = profile.Name?.ToLowerInvariant() ?? string.Empty;
    }

    return CombineAsync(
        View("IndexProfile_Checkbox_SummaryAdmin", profile)
            .Location(OrchardCoreConstants.DisplayType.SummaryAdmin, "Checkbox:1")
            .Location(OrchardCoreConstants.DisplayType.SummaryAdminGrid, "Select:1"),
        View("IndexProfile_Fields_SummaryAdmin", profile)
            .Location(OrchardCoreConstants.DisplayType.SummaryAdmin, "Content:1")
            .Location(OrchardCoreConstants.DisplayType.SummaryAdminGrid, "Name:1"),
        View("IndexProfile_DefaultTags_SummaryAdmin", profile)
            .Location(OrchardCoreConstants.DisplayType.SummaryAdmin, "Tags:5")
            .Location(OrchardCoreConstants.DisplayType.SummaryAdminGrid, "Source:5"),
        View("IndexProfile_Buttons_SummaryAdmin", profile)
            .Location(OrchardCoreConstants.DisplayType.SummaryAdmin, "Actions:5")
            .Location(OrchardCoreConstants.DisplayType.SummaryAdminGrid, "Actions:5"),
        View("IndexProfile_ActionsMenuItems_SummaryAdmin", profile)
            .Location(OrchardCoreConstants.DisplayType.SummaryAdmin, "ActionsMenu:5")
            .Location(OrchardCoreConstants.DisplayType.SummaryAdminGrid, "ActionsMenu:5"));
}
  • Every visible piece of a row (checkbox, title, badges, dates, buttons, menu items) is its own shape in a zone. Anything hard-coded in the row template is missing from the grid.
  • A location without a display type, .Location("Tags:5"), applies to both display types, so in the grid the shape lands in a Tags zone no column renders. Always pair a SummaryAdmin location with a SummaryAdminGrid one, unless the zone name is also a column name (e.g. Actions).
  • Display types do not inherit placement: a shape placed only for SummaryAdmin, by a driver or by placement.json, does not appear in the grid. A grid-only shape declares only a SummaryAdminGrid location.
  • placement.json rules filtered by "displayType": "SummaryAdminGrid" move a shape to another column zone (Source:10), hide it (-), change its template or add wrappers and alternates.
  • Do not use editor |Column groupings for cells.

2. Write the row template for the List layout

{Model}.SummaryAdmin.cshtml renders the zones of the row; the grid does not use it. Every shipped row has the same structure, so the lists look alike:

<div class="row g-0 align-items-center">
    <div class="col">
        <div class="title d-flex align-items-center">
            @* Checkbox, then <div class="summary d-flex flex-column flex-md-row list-item-search-text"> with Content, Tags, Meta *@
        </div>
        @if (Model.Description != null)
        {
            @await DisplayAsync(Model.Description)
        }
    </div>
    <div class="col-auto d-flex justify-content-end ps-2">
        @await DisplayAsync(await Factory.CreateAdminListActionsAsync((IShape)Model))
    </div>
</div>
ZoneRenders asNever
ContentThe title, a plain link to the edit pageA heading (<h5>)
Tags, Meta<span class="badge ta-badge fw-normal"> per fact, leading icon, title tooltipPlain text or a <span class="hint">
Description<div class="admin-list-secondary">, its own line under the titleA second full-width row running under the actions
Actions, ActionsMenuThrough AdminListActions (CreateAdminListActionsAsync), so the Buttons/Menu setting appliesA hand-written button group, or ActionsMenu items wrapped in <li>

3. Supply the items from the controller

model.List = new AdminListContext<IndexProfile>
{
    Source = items,
    Pager = pagerShape,
};

The view model exposes public AdminListContext List { get; set; }. AdminListContext<TModel> sets ItemType and exposes a typed IEnumerable<TModel> Source; its generic argument can select a registered base/interface display manager, even for an empty collection of derived items. Items must be non-null instances of that type. The list invokes the display manager after resolving its layout, so the controller has no row-rendering dependencies and never builds rows, row callbacks or prebuilt shapes.

The default IAdminListItemDisplayManager<TModel> adapter calls IDisplayManager<TModel>. Register a closed adapter for a specialized pipeline, e.g. content items use IContentItemDisplayManager.

Other context properties, all optional:

  • Header: an options editor shape (Summary and Actions zones), as the content items and users pass.
  • Toolbar: a toolbar shape of the page's own. Otherwise the list builds AdminListToolbar from the rows, the pager, BulkActions, ShowSelectAll and ToolbarActions (a shape at its end, e.g. filters). ShowToolbar = false omits it.
  • Search, Actions: the search bar (e.g. the AdminListSearch shape) and the page buttons, in the action bar.
  • PageSize: a separate page size selector; set ShowPageSizeSelector = false on the pager when passing it.
  • Data: what a column provider may need, e.g. the content types the listing is filtered by.
  • Layout: forces a layout, only when the page can be rendered one way, e.g. while its rows are dragged.
  • ShowLayoutSelector = false: for a page that renders several lists or places one selector itself, by creating an AdminListLayoutSelector shape with the shared ListName.

4. Render the list in the view

Import the tag helpers once per module, in Views/_ViewImports.cshtml:

@using OrchardCore.Admin
@addTagHelper *, OrchardCore.Admin.Abstractions

The view keeps the form, its hidden inputs and the action bar, and renders the list with <admin-list>:

<form asp-action="Index" method="post" class="no-multisubmit bulk-select-list"
      data-checkbox-name="itemIds" data-bulk-action-name="Options.BulkAction" data-selected-text="@T["selected"]">
    <input type="submit" name="submit.Filter" id="submitFilter" class="visually-hidden" />
    <input asp-for="Options.BulkAction" type="hidden" />
    <input type="submit" name="submit.BulkAction" class="visually-hidden" />

    <admin-list name="Indexes" context="Model.List"
                bulk-actions="Model.Options.BulkActions"
                empty-message="@T["<strong>Nothing here!</strong> There are no indexes at the moment."]">
        <admin-list-list item-css-class="list-group-item" />
        <admin-list-grid>
            <admin-list-column type="Select" />
            <admin-list-column name="Name" title="@T["Name"]" position="20" />
            <admin-list-column name="Source" title="@T["Source"]" position="30" width="auto" />
            <admin-list-column type="Actions" title="@T["Actions"]" />
        </admin-list-grid>
    </admin-list>
</form>
  • name and context are both required. The name identifies the list for column providers, the user's layout choice and the alternates. Use a string literal when only the view uses it; keep a module constant when it is reused, e.g. ContentsAdminList.Name, and do not create a holder class for a single use.
  • The tag also takes show-toolbar, show-select-all, show-layout-selector, layout, item-css-class, empty-message, bulk-actions, and rows-* attributes (or rows-attributes) for the rows container, e.g. rows-id for a sortable list.
  • admin-list-list and admin-list-grid configure their layout's rows-class and item-css-class without forcing it. Each can be declared once. List item classes replace the default; grid item classes are added to the required grid row class. Children emit no HTML.
  • The hidden submit.Filter button stays first in the form: Enter in the search box triggers it. The search is an input group ending with a magnifying-glass button, never an icon inside the input.
  • Use the shared bulk-select-list and list-search-filter components, wired by one small module per page (Assets/ts/*.ts), and keep the select-all, items, selected-items and actions ids the toolbar renders.

5. Declare the grid columns

Each <admin-list-column> renders the row zone named after it; zones="A B" renders several named zones instead. A zone no column renders is omitted from the grid.

AttributeMeaning
typeSelect (the Select zone, at position 10) or Actions (the Actions and ActionsMenu zones, at position end); omit for a regular column. A preset keeps its defaults unless overridden.
nameRequired for a regular column, unique ignoring case; also the default zone and the admin-list-column-{name} CSS class.
titleLocalized header text, e.g. @T["Name"]; it also labels the cells when a narrow list stacks them.
positionPlacement syntax (10, 25, end); columns are sorted by it.
widthAny CSS width, or auto to hug the content. No width shares the remaining space.
alignment, no-wrap, css-classCell presentation.

To change the columns of another module's list, implement IAdminListColumnsProvider:

public sealed class CultureColumnsProvider : IAdminListColumnsProvider
{
    private readonly IStringLocalizer S;

    public CultureColumnsProvider(IStringLocalizer<CultureColumnsProvider> stringLocalizer)
    {
        S = stringLocalizer;
    }

    public Task PopulateAsync(AdminListColumnsContext context)
    {
        // Between "Title" (20) and "Type" (30), whatever order the providers run in.
        context.Columns.Add(new AdminListColumn { Name = "Culture", Position = "25", Title = S["Culture"], Width = AdminListColumn.AutoWidth });

        return Task.CompletedTask;
    }
}
services.AddAdminListColumnsProvider<CultureColumnsProvider>(ContentsAdminList.Name);
  • Registered with a list name, a provider is only created and run for that list (keyed DI with an AdminListColumnsProviderKey; names are case-sensitive). Several providers can target a list, and a provider can target several lists. Duplicate registrations are ignored.
  • Without a name, AddAdminListColumnsProvider<TProvider>() registers a global provider for every list. Global providers run first, then the providers of the list, each group in feature dependency order. A global provider must tolerate different column sets: check context.Find(name) before changing an optional column.
  • context.Columns.Add(...), context.Find(name) and context.Remove(name) change the columns. context.Data, TryGetData<T>() and GetData<T>() read what the page passed, e.g. ContentsAdminList.ContentTypesKey and ContentsAdminList.StereotypesKey.
  • A provider never builds cells: the content of its column comes from a display driver placing a shape in the zone of that column for SummaryAdminGrid.

6. Test and document it

  • Add the page's URL to _lists in test/OrchardCore.Tests.Functional/Tests/Cms/AdminListLayoutTests.cs when the Blog recipe enables its feature (add an optional-list test otherwise). The test renders every list in the grid and fails when a column renders nothing on any row, which catches a shape missing its SummaryAdminGrid location.
  • Check the page in both layouts and both actions layouts (Buttons and Menu), with rows and without, and in a narrow window, where the grid stacks its cells.
  • Add the list to the table of lists in src/docs/reference/modules/Admin/README.md.

Regions and properties of the shape

Rows and regions inherit the list name and layout automatically, so their alternates work without a page passing them. Rows is output built during display, never an input; DisplayType is the resolved row display type. Custom AdminListCell templates receive Row and Column. The Actions cell renders the row's Actions and ActionsMenu zones through AdminListActions; moving an action to another zone removes it from that cell.

Alternates

ShapeAlternates, least to most specific
AdminListAdminList__{Layout}, AdminList__{Name}, AdminList__{Name}__{Layout}
AdminListCellAdminListCell__{Column}, AdminListCell__{Name}__{Column}
AdminListActionsAdminListActions__{Layout}, AdminListActions__{Name}, AdminListActions__{Name}__{Layout}, where {Layout} is Buttons or Menu
AdminListToolbar, AdminListSearch__{Layout}, __{Name}, __{Name}__{Layout}

AdminListActionsLayout has exactly two values, Buttons and Menu; configuration and the nullable site settings use the enum. Keep templates and alternates overridable, but do not add option shapes or custom modes.

Gotchas

  1. Never name the rows Items: Shape already exposes Items, its child shapes.
  2. A location without a display type does not reach the grid column (step 1). An empty grid column is the symptom.
  3. Per-type row templates only apply to the List layout: Content-BlogPost.SummaryAdmin.cshtml does not affect the grid; customise it with SummaryAdminGrid placement or AdminListCell-{List}-{Column}.cshtml.
  4. Never wrap ActionsMenu items in <li>: the dropdown sits inside the <li> of a List row, and the parser hoists a nested <li> out of it, so the menu renders empty and stray links appear under the list.
  5. No viewport-based classes such as d-md-none in list Razor: the grid stacks with a container query on the list itself (_admin-list.scss), because the sidebar makes the viewport a poor proxy for the list width.
  6. A sortable list drags the rows, never their container: in the grid the rows container is display: contents. Mark it with rows-* attributes, and send SortableJS oldDraggableIndex/newDraggableIndex, which count only the rows.
  7. Rows are built while the view renders, from the AdminList displaying event: a driver's queries and errors happen during rendering, not in the controller.

References

  • references/worked-example.md: the Indexes page, driver, controller and view.
  • src/docs/reference/modules/Admin/README.md: the user and developer documentation of admin lists.
  • src/OrchardCore.Modules/OrchardCore.Admin/Views/AdminList*.cshtml: the layout, cell, actions and toolbar templates.
  • src/OrchardCore.Themes/TheAdmin/Assets/scss/components/_admin-list.scss: the layout CSS and its container queries.
  • test/OrchardCore.Tests.Functional/Tests/Cms/AdminListLayoutTests.cs: renders every list in the grid.

Download now (ZIP)

Trending

Steady talk.about the whole repository

Dec 2025: 1 mentionJan 2026: 0 mentionsFeb 2026: 1 mentionMar 2026: 1 mentionApr 2026: 2 mentionsMay 2026: 1 mentionJun 2026: 1 mentionJul 2026: 0 mentionsAug 2026: 0 mentionsSep 2026: 0 mentionsOct 2026: 1 mentionNov 2026: 0 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the OrchardCMS/OrchardCore repository that our weekly crawl found. There is no adoption data for this file yet.

Who's talking about OrchardCMS/OrchardCore

The biggest 5 of 67 mentions linking to the repository that our crawl found. Each line is their own headline.

See all 67

Talked about on GitHub, Stack Overflow and Bluesky (67 times).

On GitHub · 50

45 more on GitHub

On Stack Overflow · 15

10 more on Stack Overflow

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.