Adminix Documentation Help

List

Description

The "List" module purpose - render list of some records from your database to work with them.

When the resolved list has no rows, Adminix renders a single No records found. table row. The empty state uses the same table width and responsive scroll container as regular records, including relation manager child lists.

  1. Configuration

  2. Lenses

  3. Quick filters and chips

  4. Fields

  5. Row details

  6. Row reorder

  7. Column visibility

  8. Table summaries

  9. Grouped rows

  10. Density and sticky table controls

  11. Saved table views

  12. Searching

  13. Filters

  14. Reset controls

  15. Mobile behavior

  16. Soft-delete row actions

  17. Clone actions

  18. Bulk actions

  19. Actions

  20. New item link

  21. Appearance examples

Configuration

Basic example:

use App\Models\User; use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListModule; ... $page = new AdminixPage(); ... $page->addModule( ListModule::name('users') ->title('USERS') ->dataSource(User::class) ->pagination(20) ->addFields(...) ) ...

To add to the page List module you need paste AlexKudrya\Adminix\Modules\List\ListModule class instance as an argument to addModule or addModules method of AdminixPage object.

ListModule configuration

Full settings list:

Method

Description

name

Name of module, must be unique in current page.

name('users')

Required

title

Title on the top of the module

title('USERS')

Optional

pagination

Number of items per page. To disable pagination must be equal to 0 or not exist.

pagination(20)

Optional

dataSource

Source of data for List, can be an Eloquent Model - \App\Models\User::class or name of table in database users or public.users

dataSource(\App\Models\User::class)

Required

withDeleted

Determines render or not deleted records (working with models with included SoftDeletes trait)

By default, is not active

withDeleted()

Optional

restoreAction


forceDeleteAction


softDeleteActions

Render built-in restore and/or permanent delete controls for trashed Eloquent SoftDeletes rows. Requires withDeleted() on the module or a lens that can expose deleted rows.

softDeleteActions()

Detailed settings described below. #Soft-delete row actions

Optional

cloneAction


bulkCloneAction


cloneActions

Render built-in row clone and/or bulk clone controls for SQL/Eloquent list records.

cloneActions()

Detailed settings described below. #Clone actions

Optional

hiddenOnIndex / hideOnIndex

Removes the field from the server-rendered index table and from inline-edit writable field resolution. Use this for server-side list visibility, not for browser-only column preferences.

hiddenOnIndex() hideOnIndex()

Optional

editable

Determines possibility to edit records directly in the List. Editable possibility for every field determines in his personal config. By default, is not active

editable(),

Optional

primaryKey

Primary key in source table. Used for editable mode. Usually it is "id"

primaryKey('id')

Required if editable mode is enabled

criteria

Database Builder criteria to render records. For example to not show administrator you need paste this:

criteria([ ['is_admin', '=', false] ])

Or you can paste parameter from route:

criteria([ ['user_id', '=', 'param:1'] ])

For example, for https://site.com/adminix/comments/21 it will be mean that you need all comments where user_id ==21.

Module/query criteria are normalized through Adminix's shared criteria contract. Supported operators are =, ==, !=, <>, >, <, >=, <=, like, and not like; == is normalized to =. Array values support only equality operators and become whereIn/whereNotIn. Null values support only equality operators and become whereNull/whereNotNull.

Optional

resource

Laravel JsonResource to provide data. Example: \App\Http\Resources\UserAdminResource::class

resource(\App\Http\Resources\UserAdminResource::class)

Optional

addLens


addLenses

Add server-defined saved views for the current list. Each lens can apply extra criteria, lens-specific default sorting, and explicit withDeleted().

addLenses( ListLens::name('active') ->title('Active') ->criteria(['status' => 'active']), ListLens::name('trash') ->title('Trash') ->withDeleted() )

Detailed settings described below. #Lenses

Optional

addQuickFilter


addQuickFilters

Add server-defined quick presets that reuse configured list filters, search, and lenses. The active quick filter is selected by quick-{moduleName} and resolved only against PHP configuration.

addQuickFilter( ListQuickFilter::make('active') ->title('Active') ->filter('status', 'active') )

Detailed settings described below. #Quick filters and chips

Optional

addField


addFields

Methods by which you can add fields to List for displaying.

Arguments can be only AlexKudrya\Adminix\Modules\List\ListField class instances.

Detailed here #Fields

addField( ListField::name('Name') ->field('name') ->type(ListFieldTypeEnum::STRING) ->sortable() )

Required

rowDetails

Attach a server-declared expandable detail panel to each row. The panel reuses DetailField renderers and reads only fields configured in PHP.

rowDetails( ListRowDetails::make('Details') ->columns(2) ->addFields( DetailField::make('Status', 'status')->badge('#754195'), DetailField::make('Payload', 'metadata')->json() ) )

Detailed settings described below. #Row details

Optional

search

Render "Search" input on the top of module.

Argument - AlexKudrya\Adminix\Modules\List\ListSearch class instance.

search( ListSearch::searchFields(['name', 'email']) ->placeholder('Search users') )

Detailed settings described below. #Searching

Optional

addFilter


addFilters

Methods by which you can add filters for current List.

Arguments can be only several classes instances:

  • AlexKudrya\Adminix\Modules\List\ListFilter

  • AlexKudrya\Adminix\Modules\List\ListDateFilter

  • AlexKudrya\Adminix\Modules\List\ListDateRangeFilter

->addFilters( ListFilter::field('role_id') ->name('Role') ->src( InputSelectSrc::nameField('name') ->valueField('id') ->dataSource(UserRole::class) ), ListDateRangeFilter::field('created_at') ->name('Registration time') ->withTime() )

Detailed settings described below. #Filters

Optional

columnVisibility

Add a Columns section to the list settings menu for hideable list fields. Column state is UI-only and stored in browser local storage by list name.

columnVisibility()

Detailed settings described below. #Column visibility

Optional

densitySwitcher

Add table density controls to the list settings menu. The switcher is browser-only and stores compact/comfortable state by list name.

densitySwitcher()

Detailed settings described below. #Density and sticky table controls

Optional

stickyHeader

Keep the table header visible while scrolling inside the table container.

stickyHeader()

Detailed settings described below. #Density and sticky table controls

Optional

stickyActions

Keep the row action column visible while scrolling wide tables horizontally. It uses the existing row action buttons/forms/modal togglers.

stickyActions()

Detailed settings described below. #Density and sticky table controls

Optional

rowActionsDropdown

Render row actions behind one vertical three-dots dropdown button instead of inline buttons. This is only a presentation option; row action definitions, criteria, methods, params, CSRF forms, tooltips, and modal togglers stay server-defined.

rowActionsDropdown()

Detailed settings described below. #Actions

Optional

savedTableViews


savedViews

Add a Saved views section to the list settings menu. Users can save current list URL state and supported browser UI state through a table view provider.

savedTableViews()

Detailed settings described below. #Saved table views

Optional

resetControls

Add an opt-in "Reset view" action to the list settings menu for the current list state. The control removes current list search, quick filter, filters, sorting, active lens, and pagination from the URL.

resetControls()

Detailed settings described below. #Reset controls

Optional

addAction


addActions

Methods by which you can add action buttons (Links) for every item in current List module.

Arguments can be only several classes instances:

  • AlexKudrya\Adminix\Modules\Link\AdminixLinkModule

  • AlexKudrya\Adminix\Modules\Link\LinkModule

addAction( AdminixLinkModule::name('user_details') ->title('DETAILS') ->icon('bi bi-pencil') ->uri(UserPage::URI) ->params(['record:id']), )

Detailed settings described below. #Actions

Optional

addBulkAction


addBulkActions

Methods by which you can add server-side batch actions for selected rows in current List module.

Arguments can be only AlexKudrya\Adminix\Modules\List\BulkAction class instances.

addBulkAction( BulkAction::make('mark_paid') ->title('Mark paid') ->icon('bi bi-check2') ->confirm('Mark selected orders as paid?') ->handler(MarkOrdersPaidAction::class) )

Detailed settings described below. #Bulk actions

Optional

sorting

Basic sorting for records in current List module.

Argument can be only AlexKudrya\Adminix\Modules\Sorting class instance.

Example:

sorting( Sorting::field('id') ->direction('asc') )

Optional

groupBy


ungrouped

Renders server-owned group header rows for contiguous records with the same configured field value. Use normal sorting() when the list should keep groups together.

groupBy('status', 'Status') ungrouped()

Optional

newItemLink

Setting for "New Item" button rendered between Title and List, near to Search input. When a list has both create and search controls, Adminix keeps them grouped on the left side of the list toolbar.

Format is similar to Adminix Link module.

newItemLink( AdminixLinkModule::name('new_user_btn') ->title('NEW USER') ->icon('bi bi-person-fill-add') ->uri(NewUserPage::URI) )

Detailed here #New item link

Optional

Lenses

Lenses are server-defined saved views for ListModule. They are useful for common list presets such as Active, Drafts, Deleted, VIP, or Needs review.

The active lens is selected by the query key lens-{moduleName}. Adminix resolves this value only against lenses configured in PHP. Unknown lens names are ignored and the default list is rendered.

use AlexKudrya\Adminix\Modules\List\ListLens; use AlexKudrya\Adminix\Modules\List\ListModule; use AlexKudrya\Adminix\Modules\Sorting; ListModule::name('users') ->dataSource(User::class) ->addLenses( ListLens::name('active') ->title('Active') ->criteria(['status' => 'active']) ->filters(['verified' => '1']) ->sorting( Sorting::field('created_at') ->direction('desc') ), ListLens::name('deleted') ->title('Deleted') ->withDeleted() ->criteria([ ['deleted_at', '!=', null], ]) ) ->addFields(...);

Lens criteria are applied after base ListModule::criteria() and before user filters, search, and sorting. Lens filters are defaults for configured list filters. Use the filter field as the key:

ListLens::name('needs-review') ->title('Needs review') ->filters([ 'status' => 'pending', 'created_at' => [ 'from' => '2026-01-01', 'to' => '2026-01-31', ], ]);

Result: when the URL contains lens-users=needs-review and does not contain filter-users-status, Adminix applies status = pending and shows the filter as selected. If the URL already contains filter-users-status, the request value wins and the lens default is not used. Date range defaults can use a nested from/to map or flat keys such as created_at-from. Lens sorting is a default sorting only: if the request contains a valid sort-{module}-{field} parameter, the user-selected sorting wins. Pagination links keep the active lens query parameter.

Use base criteria() for mandatory tenant/user scoping. Use lenses only for alternate server-owned views inside that scope.

Quick filters and chips

Quick filters are small server-defined shortcuts for frequent list states. They do not introduce a separate query language. Each quick filter can set defaults for configured list filters, the list search query, and an existing lens.

The active quick filter is selected by the query key quick-{moduleName}. Adminix resolves this value only against ListQuickFilter instances configured in PHP. Unknown quick filter names are ignored.

use AlexKudrya\Adminix\Modules\List\ListFilter; use AlexKudrya\Adminix\Modules\List\ListFilterRecord; use AlexKudrya\Adminix\Modules\List\ListQuickFilter; use AlexKudrya\Adminix\Modules\List\ListSearch; ListModule::name('orders') ->dataSource(Order::class) ->addQuickFilters( ListQuickFilter::make('needs-review') ->title('Needs review') ->filter('status', 'review') ->search('priority'), ListQuickFilter::make('active-this-month') ->title('Active this month') ->filter('status', 'active') ->range('created_at', now()->startOfMonth()->toDateString(), now()->endOfMonth()->toDateString()) ) ->search(ListSearch::searchFields(['number', 'customer_name'])) ->addFilter( ListFilter::name('Status') ->field('status') ->addFilterRecords( ListFilterRecord::name('Active')->value('active'), ListFilterRecord::name('Review')->value('review'), ) ) ->addFields(...);

Filter defaults use the same keys as ListLens::filters(). For date ranges, call range($field, $from, $to) or pass ['created_at' => ['from' => '2026-01-01', 'to' => '2026-01-31']] to filters().

Precedence is:

  • explicit request search/filter/lens values;

  • active quick filter defaults;

  • active lens default filters.

Result: clicking a quick filter renders the preset button as active, applies only server-configured filters/search/lens, and shows removable chips for the active quick filter, lens, search, and filter values. When a chip represents an explicit URL parameter, its remove link deletes that parameter. When a value came from a quick filter or lens default, the remove link clears the preset source instead.

Saved table views can persist quick-{moduleName} only when it references a configured quick filter. resetControls() clears active quick filters along with search, filters, lens, sorting, and pagination. Reorder handles remain disabled while a quick filter is active, because the visible rows are filtered.

Use quick filters for operator convenience, not tenant or authorization boundaries. Mandatory scoping still belongs in criteria(), policies, guards, relation context, or app-side query criteria. adminix:doctor warns when a quick filter references missing filter fields, missing lenses, invalid filter values, or has no effect.

Fields

Fields is an array, of fields to be rendered in the List.

To add Field to the List module you need paste AlexKudrya\Adminix\Modules\List\ListField class instance as an argument to addField or addFields method of ListModule object.

Field Presentation

ListField supports the same presentation helpers as resource fields for table columns and inline-edit controls. These helpers only change server-rendered UI; they do not change query criteria, sorting, writable fields, or export behavior.

ListField::name('Customer') ->field('customer_name') ->editable() ->placeholder('Customer name') ->help('Shown below the column title') ->wideWidth() ->alignStart();

Available helpers:

  • placeholder($text) sets inline-edit input placeholders and async select search placeholders;

  • help($text) renders escaped helper text in the table header;

  • width('narrow'|'medium'|'wide'|'full') adds safe width classes to the table header and cells;

  • narrowWidth(), mediumWidth(), wideWidth(), and fullWidth() are named width shortcuts;

  • alignment('start'|'center'|'end') controls table header/cell alignment; left, middle, and right are normalized aliases;

  • alignStart(), alignCenter(), and alignEnd() are named alignment shortcuts.

Invalid width/alignment values are ignored. Use hiddenOnIndex(), hideable(), defaultHidden(), editable(), and exportable() for behavior; presentation helpers are not access-control tools.

Row details

Use ListRowDetails when a list needs a compact primary row plus expandable read-only metadata. Adminix renders one toggle column and a hidden detail row for every visible record. The detail panel is prepared from the same server-side list datasource, base criteria, active lens, filters, search, sorting, pagination, and optional relation scope used for the rendered table.

use AlexKudrya\Adminix\Modules\Detail\DetailField; use AlexKudrya\Adminix\Modules\List\ListRowDetails; ListModule::name('orders') ->dataSource(Order::class) ->rowDetails( ListRowDetails::make('Order details') ->columns(2) ->addFields( DetailField::make('Status', 'status')->badge('#754195'), DetailField::make('Total', 'total')->money('USD', 2), DetailField::make('Payload', 'metadata')->json(), DetailField::make('Receipt', 'receipt_url')->link() ) ) ->addFields(...);

ListRowDetails supports:

  • make($title = null)/title() for an optional detail heading;

  • columns(1..4) for the read-only field grid;

  • addField()/addFields() with DetailField or another ResourceField subclass;

  • expanded() to render all detail rows initially open, otherwise they are collapsed and toggled with JavaScript.

Row details do not add a write endpoint and do not trust browser-owned datasource, criteria, field names, or primary keys. Password fields are rendered without stored values, matching resource/detail password safety. Keep sensitive tenant, owner, credential, and internal audit columns out of row details unless they are intentionally visible to the admin user.

Badge / Status Field

Use badge() or status() when a list column should show a compact visual state instead of raw text. Badge fields are display-only: even on editable lists, ListFieldTypeEnum::BADGE is excluded from inline edit writes.

ListField::name('Status') ->field('status') ->status([ 'draft' => ['label' => 'Draft', 'color' => '#64748b'], 'published' => ['label' => 'Published', 'color' => '#16a34a'], 'failed' => ['label' => 'Failed', 'color' => '#dc2626'], ]);

badge('#754195') applies one color to all values. badgeMap() accepts the same map as status(). When a value is not mapped, Adminix renders the scalar value with the field default badge color.

Money / Currency Field

Use money($currency = 'USD', $decimals = 2) for amount columns that should render as tabular currency values. On editable lists, money fields remain writable number inputs and use a decimal step derived from $decimals.

ListField::name('Total') ->field('total') ->money('EUR', 2);

Result: read-only cells render values such as EUR 1,234.50; editable cells render a number input with step="0.01".

Color Field

Use color() for color values stored as CSS color strings, commonly hex values such as #754195. Read-only list cells render a swatch and the stored value; editable list cells render a native color picker.

ListField::name('Brand color') ->field('brand_color') ->color();

Slug Field

Use slug() for columns that store URL-friendly slugs. List slug fields render like scalar text and can still be sortable when the underlying database column supports it.

ListField::name('Slug') ->field('slug') ->slug() ->sortable();

If the list is editable and the field is marked editable(), Adminix renders a normal text input. Keep slug validation and uniqueness server-side in the resource or inline-edit validation rules.

Markdown Field

Use markdown() for columns that store markdown text. Read-only list cells render a compact safe preview: raw HTML is escaped, while simple markdown links, **strong**, *emphasis*, inline code, and line breaks are rendered. Editable list cells render a textarea and submit the raw markdown value.

ListField::name('Content') ->field('content_md') ->markdown() ->defaultHidden();

Markdown columns are usually long, so generated smart resources mark them defaultHidden() and do not make them sortable. Keep validation and content policy server-side.

JSON / Code Field

Use json() for JSON payload columns. Read-only list cells render a compact pretty-printed preview, and editable list cells render a monospace textarea with progressive JSON syntax feedback. The helper adds the Laravel json validation rule; if JavaScript is unavailable, inline edit still submits a normal textarea value to server validation.

ListField::name('Payload') ->field('payload') ->json() ->defaultHidden();

Generated smart resources mark JSON list columns defaultHidden() and do not make them sortable. Keep schema-specific validation in the consuming application.

Key-Value Field

Use keyValue() for object-like JSON columns such as metadata, settings, options, or attributes. Read-only list cells render compact key/value rows. Editable list cells render the same progressive key/value editor as resource forms and submit a single JSON object field.

ListField::name('Metadata') ->field('metadata') ->keyValue() ->defaultHidden();

The helper adds the Laravel json validation rule. Generated smart resources mark key-value list columns defaultHidden() and do not make them sortable.

Date Range Field

Use dateRange() for JSON values shaped like {"from":"2026-01-01","to":"2026-01-31"}. Read-only list cells render a compact date-range label, and editable list cells render the same progressive two-date editor as resource forms.

ListField::name('Booking window') ->field('booking_window') ->dateRange() ->defaultHidden();

The helper adds the Laravel json validation rule. Keep ordering and filtering on dedicated date columns when the database needs efficient range queries; this field is for displaying or editing a persisted range value.

Timezone Field

Use timezone() for columns that store IANA timezone identifiers. Read-only list cells render the stored identifier, and editable list cells render the same server-declared timezone select as resource forms.

ListField::name('Timezone') ->field('timezone') ->timezone() ->sortable();

The helper adds Laravel's timezone validation rule for editable list submissions. Use application validation when timezone changes must be restricted by tenant, user, or business context.

Tags Field

Use tags($separator = ',') for tag-list columns. Read-only list cells render chips. Editable list cells render a normal text input with a progressive chip preview and submit the separator-delimited string.

ListField::name('Tags') ->field('tags') ->tags() ->defaultHidden();

The formatter accepts separator-delimited strings, JSON array strings, and array values. Generated smart resources mark tag columns defaultHidden() and do not make them sortable.

Example:

use App\Models\User; use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListModule; ... $page = new AdminixPage(); ... $page->addModule( ListModule::name('users') ->title('USERS') ->dataSource(User::class) ->pagination(20) ->addFields( ListField::name('Avatar') ->field('avatar') ->type(ListFieldTypeEnum::IMAGE), ListField::name('Name') ->field('name') ->type(ListFieldTypeEnum::STRING) ->sortable() ->editable(), ListField::name('Role') ->field('role_id') ->type(ListFieldTypeEnum::SELECT) ->editable() ->src( InputSelectSrc::nameField('name') ->valueField('id') ->dataSource(Role::class) ) ->addValidationRules( 'integer', 'required', 'exists:'. Role::class.',id' ), ListField::name('Rating') ->field('rating') ->type(ListFieldTypeEnum::INTEGER) ->editable() ->sortable() ->addValidationRules( 'integer', 'required', 'min:0', ), ListField::name('Registration') ->field('created_at') ->type(ListFieldTypeEnum::DATETIME) ->sortable(), ListField::name('Administrator') ->field('is_admin') ->type(ListFieldTypeEnum::BOOLEAN), ListField::field('deleted_at') ->type(ListFieldTypeEnum::HIDDEN), ListField::field('id') ->type(ListFieldTypeEnum::HIDDEN), ) ) ...

ListField configurations

Method

Description

name

Title of the field displayed on the top of table (inside <th/> tag)

name('Role')

Required

type

Type of rendered field, can be provided only by AlexKudrya\Adminix\Modules\List\ListFieldTypeEnum Enum class

Available types for rendered field:

  • STRING

  • EMAIL

  • INTEGER

  • FLOAT

  • BOOLEAN

  • SELECT

  • DATE

  • DATETIME

  • HIDDEN

  • MARKDOWN

  • JSON

  • TAGS

  • KEY_VALUE

  • DATE_RANGE

  • TIMEZONE

  • TEXTAREA

It determines data format to render, and type of input for editable mode.

Example:

type(ListFieldTypeEnum::SELECT)

Required

field

Name of field in database or resource (if resource was provided).

field('role_id')

Required

sortable

Determines the sorting possibility for current field.

Can not be used to fields which provided by resource, and not exist in database table, or his name was modified.

By default, is not enabled.

sortable()

Optional

hideable

Allows the field to appear in the columnVisibility() menu. The column is visible by default until the user hides it in the browser.

hideable()

Optional

defaultHidden

Allows the field to appear in the columnVisibility() menu and hides it by default. The user can show it again from the list settings menu.

defaultHidden()

Optional

pinned


pinnedLeft


pinnedRight


unpinned

Sets the default sticky side for a rendered index column. Pinned columns are still browser-side presentation state; they do not change datasource, criteria, sorting, export, or authorization. Hidden/internal and hidden-on-index fields cannot be pinned.

pinnedLeft() pinnedRight() unpinned()

Optional

sum


avg


average


min


max


countSummary


summarize


summaryLabel

Adds a server-computed footer summary for a rendered list column. The aggregate uses the current datasource, criteria, active lens, filters, search, and sorting before pagination. Hidden/internal and hidden-on-index fields cannot render footer totals.

sum('Total') avg('Average') countSummary('Rows') summaryLabel('Totals')

Optional

editable

Determines the editable possibility for current field.

Can not be used to fields which provided by resource, and not exist in database table, or his name was modified.

This will not be working if in List module editable mode is not enabled.

To working correctly requires primary key (usually it is "id" field) in this fields list (can be hidden type).

By default, is not enabled.

editable()

Optional

src

Required for fields with type SELECT. Need to display correct value from related table.

For example, users table has related table roles, and relation made by role_id field in users table. To display normal role name from roles table, not role_id from users table, you need to configure src correctly.

Argument can be only AlexKudrya\Adminix\Modules\InputSelectSrc class instance.

Example:

ListField::name('Role') ->field('role_id') ->type(ListFieldTypeEnum::SELECT) ->editable() ->src( InputSelectSrc::nameField('name') ->valueField('id') ->dataSource(Role::class) )

InputSelectSrc configuration:

Methods

Description

dataSource

Source of data for filed items, can be an Eloquent Model - \App\Models\Role::class or name of table in database rolesor public.roles

dataSource(\App\Models\Role::class)

nameField

Name of field in related table to be displayed

nameField('name')

valueField

Name of primary key field related to main table (usually it is "id")

valueField('id')

asyncSrc

Use asyncSrc() for editable SELECT columns with large option tables. Adminix renders a search input above the native <select>, fetches options with debounce, shows loading/empty states, and submits the selected value through the existing inline edit form.

Example:

use AlexKudrya\Adminix\Modules\AsyncInputSelectSrc; ListField::name('Author') ->field('author_id') ->type(ListFieldTypeEnum::SELECT) ->editable() ->asyncSrc( AsyncInputSelectSrc::dataSource(User::class) ->nameField('name') ->valueField('id') ->searchFields(['name', 'email']) ->criteria(['active' => true]) ->minLength(2) ->limit(20) ->placeholder('Search authors') );

Result: editable rows search remote options with GET /{adminix-prefix}/api/async-select/{page}/{module}/{field}?q=..., and selected row values are resolved with ?value=.... The endpoint accepts only server-configured datasource, criteria, search fields, and limits.

For read-only list cells, async fields render the stored value because Adminix intentionally does not eager-load labels for all rows. Use synchronous src() or a resource/data transform when a read-only list must display labels.

addSelectRecords

Required for fields with type SELECT as an alternative for src(). Required if src is empty. It is a list of options, where name() is a title of <option/>, and value()is a value of <option/>

Arguments can be only AlexKudrya\Adminix\Modules\SelectRecord class instances.

Example:

ListField::name('Role') ->field('role_id') ->type(ListFieldTypeEnum::SELECT) ->editable() ->addSelectRecords( SelectRecord::name('Admin')->value(1), SelectRecord::name('Manager')->value(2), SelectRecord::name('Seller')->value(3), SelectRecord::name('Guest')->value(4), ) ...

Row reorder

reorderable($orderField) enables flat drag-and-drop ordering for a ListModule. It renders a left-side grip handle, disables user-driven column sorting for that list, and uses the configured base Sorting as the canonical row order. If the module does not already have sorting, reorderable('sort_order') configures Sorting::field('sort_order')->direction('asc') automatically.

use AlexKudrya\Adminix\Modules\List\ListField; use AlexKudrya\Adminix\Modules\List\ListFieldTypeEnum; use AlexKudrya\Adminix\Modules\List\ListModule; ListModule::name('products') ->dataSource(Product::class) ->primaryKey('id') ->reorderable('sort_order') ->addFields( ListField::name('ID')->field('id')->type(ListFieldTypeEnum::HIDDEN), ListField::name('Name')->field('name'), ListField::name('Order')->field('sort_order')->type(ListFieldTypeEnum::INTEGER), );

Result: Adminix shows a grip column for reorderable rows only when the whole current reorder scope is visible. Drag the grip with a pointer or focus it and use ArrowUp/ArrowDown. The browser sends only the ordered primary keys and a signed context; Adminix resolves page, module, datasource, criteria, primary key, order field, and relation scope from server-side configuration.

Reorder handles are intentionally hidden when the visible scope cannot be written predictably:

  • the list has pagination enabled;

  • the URL contains current-list search, quick filter, filters, lens, sort, or page state;

  • the primary key is not rendered in the list data;

  • fewer than two rows are visible;

  • the module does not have one ascending base sort by the reorder field.

  • in tree mode, a visible row points to a parent outside the current visible row set.

For standard Eloquent and query-builder data sources, the built-in writer uses a transaction and keeps the existing order-value set. It moves one row to a temporary sentinel value outside the current datasource range, rotates the affected cycle, and then assigns the existing values to the new row order. This avoids immediate unique-index collisions on databases that do not support deferrable unique constraints, while also avoiding permanent growth of the order values after repeated reorders. The current scope must have unique numeric order values.

Adminix does not automatically normalize order values to 1..N. Normalization can collide with unknown global or composite unique constraints, tenant scopes, category scopes, or non-SQL storage rules. If an application wants periodic normalization, implement it in application code where the full uniqueness scope and storage semantics are known.

Use treeReorderable($orderField, $parentField = 'parent_id', $rootParentValue = null) for adjacency-list trees. Adminix renders the current scoped rows in server-side tree preorder, signs both the rendered primary keys and their parent map, and sends adminix_reorder_parent_ids only for tree mode. Pointer drag reorders rows inside the same parent; keyboard ArrowRight indents a row under the previous sibling, ArrowLeft outdents it one level, and ArrowUp/ArrowDown move it among siblings. Tree reorder controls are hidden when the current scope is an incomplete tree and a visible row's parent is outside the rendered row set, so saving cannot detach hidden parents.

ListModule::name('categories') ->dataSource(Category::class) ->primaryKey('id') ->treeReorderable('sort_order', 'parent_id') ->addFields( ListField::name('ID')->field('id')->type(ListFieldTypeEnum::HIDDEN), ListField::name('Name')->field('name'), ListField::name('Order')->field('sort_order')->type(ListFieldTypeEnum::INTEGER), );

For the built-in SQL/Eloquent writer, tree reorder updates the configured parent field and normalizes sort_order inside each sibling group. Use the third argument when root rows use a sentinel such as 0 instead of NULL: treeReorderable('sort_order', 'parent_id', 0). Use reorderUsing() when your tree uses materialized paths, nested sets, fractional ranks, custom locks, or storage-specific constraints.

Use authorizeReorderUsing() to add a module-level authorization guard:

use Illuminate\Http\Request; ListModule::name('products') ->dataSource(Product::class) ->reorderable('sort_order') ->authorizeReorderUsing(fn (ListModule $module, array $orderedIds, Request $request): bool => $request->user()?->can('reorderProducts') === true) ->addFields(...);

Use reorderUsing() for MongoDB, external services, custom rank algorithms, application-side locks, version checks, or any storage where the built-in SQL/Eloquent writer should not decide persistence. Adminix still validates the signed rendered row set before calling the handler.

use AlexKudrya\Adminix\Modules\List\ListReorderRequest; ListModule::name('products') ->dataSource(Product::class) ->reorderable('sort_order') ->reorderUsing(function (ListReorderRequest $request): string { ProductOrderService::forTenant($request->params()[0] ?? null) ->replaceOrder( previousIds: $request->previousIds(), orderedIds: $request->orderedIds(), ); return 'Product order saved.'; }) ->addFields(...);

ListReorderRequest exposes:

  • module() for the server-side ListModule;

  • previousIds() for the signed row order that was rendered;

  • orderedIds() for the submitted permutation;

  • parentIds() for the submitted tree parent map, empty for flat reorder;

  • params() for signed page parameters after Adminix param-index compatibility resolution;

  • relationContext() for signed relation child-list context, when present;

  • httpRequest() for the original Laravel request.

adminix:doctor warns when reorderable lists miss a primary key field, base order field, valid ascending sorting, or use pagination. For the built-in writer it also checks that the reorder field exists in the SQL datasource, and tree reorder checks the configured parent field.

Column visibility

columnVisibility() adds a Columns section to the compact list settings dropdown for the current ListModule. Only fields marked with hideable() or defaultHidden() are included in that section. When the list uses columnVisibility() or savedTableViews(), the same section also exposes server-declared visible fields for browser-side column order, width, and pinned-side controls. Hidden fields, row reorder handles, row detail toggles, bulk checkboxes, inline edit buttons, and row action service columns are not part of the configurable column layout. The list settings dropdown grows to its content height and only uses vertical scrolling when the menu would exceed the viewport. On narrower desktop viewports, the list settings button shifts left only when its screen position vertically overlaps the fixed shell notification bell. Column toggles, density options, reset, and CSV export actions share the same menu item sizing, spacing, rounded corners, and hover treatment. Column toggles are rendered as switch controls, matching boolean field controls elsewhere in Adminix.

ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->columnVisibility() ->addFields( ListField::name('Number') ->field('number') ->sortable(), ListField::name('Customer') ->field('customer_email') ->hideable() ->pinnedLeft(), ListField::name('Internal note') ->field('internal_note') ->defaultHidden(), ListField::field('id') ->type(ListFieldTypeEnum::HIDDEN) );

Result: Adminix shows a gear-icon settings button as a root-level absolute control aligned to the list title area. The control does not reserve toolbar or filter space; when a list has no title, it stays inside the list root without adding an empty row. The Customer column is visible by default and can be hidden by the user. The Internal note column is hidden by default and can be shown from the same settings menu. Visible list columns can be moved left/right, assigned an auto, narrow, medium, wide, or full width preference, and pinned left or right. pinnedLeft() and pinnedRight() set the default sticky side; clicking the same pin button in the settings menu unpins the column for that browser or saved view. The hidden id field and service columns for inline edit or row actions are not controlled by column visibility.

Column visibility, column order, column width, and column pinning are browser-side presentation settings. They are stored in local storage by list name and do not change request keys, datasource, criteria, filters, sorting, row actions, authorization, or export scope. When savedTableViews() is enabled, named table view presets can also persist normalized column order, width, and pin state through the configured table view provider. For server-side list visibility, use ListField::hiddenOnIndex()/hideOnIndex(). Hidden-on-index fields are not rendered in the table and are not writable through inline edit. adminix:doctor warns when a hidden/internal or hidden-on-index list field is marked pinned, because it cannot render as a sticky table column.

Table summaries

Use field-level summary helpers when a list needs footer totals aligned with its rendered columns. Adminix computes summary values server-side from the same list query scope used for the visible records: datasource, base criteria, active lens, filters, search, and sorting are applied, but pagination is not.

ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->columnVisibility() ->addFields( ListField::name('Customer') ->field('customer_email') ->summaryLabel('Totals') ->pinnedLeft(), ListField::name('Total') ->field('total') ->money('EUR', 2) ->sum('Total'), ListField::name('ID') ->field('id') ->countSummary('Rows') );

Result: Adminix renders a <tfoot> row below the list body. The footer uses the same data-adminix-list-column contract as header and body cells, so browser-side column visibility, order, width, reset, saved table views, and pinned columns keep footer cells aligned.

Available helpers:

  • sum(?string $label = null);

  • avg(?string $label = null)/average(?string $label = null);

  • min(?string $label = null);

  • max(?string $label = null);

  • countSummary(?string $label = null);

  • summarize(ListSummaryAggregateEnum|string $aggregate, ?string $label = null);

  • summaryLabel(?string $label).

Money fields format sum, avg, min, and max with the configured currency and decimals. countSummary() renders as a plain number even when used on a money field.

Summaries are display-only and do not change list datasource, filters, sorting, export, row actions, authorization, or writable fields. adminix:doctor warns when a summary is configured for a hidden/internal or hidden-on-index field, or when the summary field does not exist in a known SQL/Eloquent datasource table.

Grouped rows

Use groupBy($field, $label = null) when a list needs visual group headers between rows. Adminix groups the already prepared rows for the current rendered page, so datasource, criteria, active lens, filters, search, sorting, pagination, row actions, exports, and writes keep their normal server-side behavior.

ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->sorting(Sorting::field('status')->direction('asc')) ->groupBy('status', 'Status') ->addFields( ListField::name('Number')->field('number'), ListField::name('Customer')->field('customer_email'), ListField::name('Status')->field('status')->hiddenOnIndex() );

Result: Adminix inserts full-width group header rows inside the table body before each contiguous group. The group field must be present in the ListField collection so Adminix can prepare its row value; it may be hidden from the index table when the value should appear only in the group header. groupBy() does not reorder records by itself. Configure sorting() by the same field, or use a lens/default query order, when groups should be contiguous.

Use ungrouped() to clear a previously configured grouping helper in composed module factories. adminix:doctor warns when groupBy() references a field that is not configured in the list or is missing from a known SQL/Eloquent datasource table.

Density and sticky table controls

densitySwitcher(), stickyHeader(), and stickyActions() are opt-in presentation helpers for wide or frequently used tables.

ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->densitySwitcher() ->stickyHeader() ->stickyActions() ->addFields(...) ->addActions( LinkModule::name('details') ->title('Details') ->uri('orders.show') ->params(['record:id']), ModalTogglerModule::name('edit_order') ->modalName('order_modal') ->params(['record:id']) );

Result: Adminix renders Comfortable and Compact density choices inside the gear-icon list settings menu. The selected density is stored in browser local storage by list name. Compact density reduces table cell padding plus inline row controls such as editable inputs, selects, row action buttons, switches, date-range/key-value editors, async selects, and row images. Inline controls use a 32px minimum height, row images cap at 38px, cells align content vertically in the middle, and action button icons keep the normal UI-kit gap from text. stickyHeader() keeps table headers visible inside the scroll container. stickyActions() keeps the existing final service column visible while the table fits the available width. When a list table is wider than the available viewport, Adminix preserves the table's content width, moves the service column into the horizontal scroll flow, and uses horizontal scrolling instead of compressing or overlapping right-side columns into data cells.

These controls do not create a new action layer. Existing addAction()/addActions() links, non-GET forms, confirm prompts, tooltips, modal togglers, and record:*/param:* route parameters continue to render through the same row action contract. If the list also uses rowActionsDropdown(), stickyActions() keeps the dropdown trigger in the same service column.

CSV export

exportCsv() adds an Export CSV action to the list settings menu for the current ListModule. CSV export is disabled by default and must be enabled per list.

ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->criteria([ ['tenant_id', '=', 'param:0'], ]) ->exportCsv(limit: 5000) ->addFields( ListField::name('Number') ->field('number') ->sortable(), ListField::name('Status') ->field('status'), ListField::name('Internal ID') ->field('id') ->type(ListFieldTypeEnum::HIDDEN), ListField::name('Tenant') ->field('tenant_id') ->type(ListFieldTypeEnum::HIDDEN) ->exportable() );

Result: Adminix shows an Export CSV action inside the gear-icon list settings menu. The endpoint applies the same server-side datasource, criteria, active lens, filters, search, and sorting as the list. The current pagination page is ignored, and export rows are capped by the configured limit. The default limit is 10000.

The browser cannot choose a datasource or arbitrary columns for CSV export. Adminix signs the current page params when rendering the export link so param:* criteria use the same route context as the visible list. ListFieldTypeEnum::HIDDEN fields are excluded by default. Use ListField::exportable() only for hidden fields that are safe to include in the CSV. Fields hidden with hiddenOnIndex() are also excluded unless they are explicitly marked exportable(). Row actions, inline edit controls, service columns, and UI-only column visibility are not exported.

Bulk actions

Bulk actions are opt-in server-side actions for selected rows in a ListModule. They render a checkbox column, Select all current page checkbox, action dropdown, selected count, and submit button above the table.

use AlexKudrya\Adminix\Modules\List\BulkAction; use AlexKudrya\Adminix\Modules\List\BulkActionHandlerInterface; use AlexKudrya\Adminix\Modules\List\BulkActionRequest; use AlexKudrya\Adminix\Modules\List\BulkActionResult; use AlexKudrya\Adminix\Modules\List\ListField; use AlexKudrya\Adminix\Modules\List\ListFieldTypeEnum; use AlexKudrya\Adminix\Modules\List\ListModule; use AlexKudrya\Adminix\Actions\ActionField; use AlexKudrya\Adminix\Actions\ActionFieldTypeEnum; use AlexKudrya\Adminix\Modules\SelectRecord; ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->criteria([ ['tenant_id', '=', 'param:0'], ]) ->primaryKey('id') ->addFields( ListField::name('ID') ->field('id') ->type(ListFieldTypeEnum::HIDDEN), ListField::name('Number') ->field('number'), ListField::name('Status') ->field('status') ) ->addBulkAction( BulkAction::make('mark_paid') ->title('Mark paid') ->icon('bi bi-check2') ->confirm('Mark selected orders as paid?') ->batchLimit(500) ->handler(MarkOrdersPaidAction::class) ); final class MarkOrdersPaidAction implements BulkActionHandlerInterface { public function handle(BulkActionRequest $request): BulkActionResult { $request->query()->update([ 'status' => 'paid', ]); return BulkActionResult::success(count($request->selectedIds()) . ' orders updated.'); } }

Result: Adminix shows bulk controls above the list. The button stays disabled until the user selects at least one current-page row and one configured action. When confirm() is configured, the browser shows that confirmation before submitting. The web endpoint redirects back with a success or error toast, or returns a direct Response when the handler returns one.

BulkAction settings:

  • name() is the stable server-side action key submitted by the browser.

  • title() is the dropdown label.

  • icon() stores an optional Bootstrap icon class for package consumers and future renderers.

  • tooltip() stores optional helper text for package consumers and future renderers.

  • color() stores an optional ColorsEnum value or CSS color string.

  • destructive() marks dangerous actions for custom renderers and future UI states.

  • confirm() sets an optional submit confirmation message.

  • criteria() stores optional action visibility criteria metadata; execution still uses the selected row scope.

  • addField()/addFields() declare input fields shown before the selected bulk action is submitted.

  • handler() accepts a callable, object, or class string. Implement BulkActionHandlerInterface for reusable actions.

  • authorizeUsing()/authorization() accepts a boolean, callable, object with authorize(), or class string.

  • batchLimit() caps selected scalar IDs before the handler runs. Default is 1000.

  • queued('Message') dispatches the handler through Laravel queues after the normal server-side scope and authorization checks.

  • progressTrigger() starts a matching ProgressBarModule trigger when the selected action is submitted.

  • onConnection() and onQueue() optionally select the Laravel queue transport for queued bulk actions.

  • actionMetadata() returns the shared Actions metadata DTO.

BulkActionRequest gives handlers:

  • module() and action() from server-side configuration;

  • requestedIds() from the request after scalar normalization;

  • selectedIds() after applying the current list scope;

  • params() after signed page-param context verification and legacy index normalization;

  • query() already scoped to selected records;

  • primaryKey() from the list module;

  • httpRequest() for request/session context when needed.

  • fields() and field($name, $default = null) for server-declared action field values.

Action fields are documented in Actions. They are submitted as adminix_action_fields[...], but Adminix only passes keys declared on the selected server-side BulkAction to the handler.

Handlers can return:

  • BulkActionResult::success('Message');

  • BulkActionResult::error('Message');

  • BulkActionResult::validation(['field' => ['Message']]);

  • BulkActionResult::redirect('/adminix/orders');

  • BulkActionResult::openInNewTab('/adminix/orders/export.csv');

  • BulkActionResult::modal('Title', 'Body');

  • BulkActionResult::download($response) or a Symfony/Laravel Response for downloadable/custom responses.

Typed action responses are documented in Actions. Queued actions are documented in Actions.

The endpoint resolves page, module, relation context, and action from server-side module configuration. The browser submits only selected IDs and action name. Datasource, primary key, criteria, active lens, filters, search, sorting, relation parent scope, and page params are resolved server-side. IDs outside the current criteria/search/filter/relation scope are ignored; if no selected ID remains in scope, the request fails with a controlled validation error. After a handler returns a normalized result, Adminix dispatches BulkActionExecuted and passes the scope-filtered selected IDs in audit metadata. See Audit for recorder and listener examples.

Bulk actions do not replace row actions. Existing addAction()/addActions() links, non-GET row forms, modal togglers, confirm prompts, tooltips, and record:* params remain the row action contract.

Searching

Determines possibility to make full-text search in source table.

Example:

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListSearch; $page = new AdminixPage(); ... $page->addModule( ListModule... ->search( ListSearch::searchFields(['name', 'email']) ->placeholder('Search users') ) ) ...

Method

Description

searchFields

An array of fields which involved in the search process. The more fields, the slower but more accurate the search. Every field must be existed in main database table

searchFields(['name', 'email'])

Required

placeholder

A placeholder for Search <input/>

placeholder('Search users')

Optional

Filters

Add filters to the top of the module, near to search input. Provides the ability to filter the list by specific fields.

There are several types of filters available:

ListFilter

Basic filter - <select/> input with defined options

Example:

1 usage variant - get records for filter from DB table by Eloquent model or table name, and took nameField as displayable name and valueField as value for filtering

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListFilter; $page = new AdminixPage(); ... $page->addModule( ListModule... ->addFilters( ListFilter::field('role_id') ->name('Role') ->src( InputSelectSrc::nameField('name') ->valueField('id') ->dataSource(\App\Models\Role::class) ), ) ) ...

Or 2 usage variant - get records for filter from DB table by Eloquent model or table name by SELECT DISTINCT ... query

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListFilter; $page = new AdminixPage(); ... $page->addModule( ListModule... ->addFilters( ListFilter::field('role') ->name('Role') ->distinct('role', \App\Models\User::class), ) ) ...

Or 3 usage variant - set records for filter manually as list of ListFilterRecord objects

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListFilter; $page = new AdminixPage(); ... $page->addModule( ListModule... ->addFilters( ListFilter::field('role_id') ->name('Role') ->addFilterRecords( ListFilterRecord::name('Client')->value(1), ListFilterRecord::name('Guest')->value(2), ListFilterRecord::name('Manager')->value(3), ListFilterRecord::name('Admin')->value(4), ) ) ) ...

Method

Description

name

Title of the filter

name('Role')

Required

field

Name of the field used for filtering. Field must be existed in base database table

field('role_id')

Required

src

Required if filter_records is empty. Has a format similar to select field src directive:

src( InputSelectSrc::nameField('name') ->valueField('id') ->dataSource(\App\Models\Role::class) ),

setting

Description

dataSource

Source of data for filed items, can be an Eloquent Model - \App\Models\Role::class or name of table in database rolesor public.roles

nameField

Name of the field witch used as a title of <option/>

valueField

Name of the field witch used as a value of <option/> (usually it is "id")

Required if no filterRecords() and no distinct()

distinct

Field and table for `SELECT DISTINCT` selection

distinct('role', \App\Models\User::class),

**Required** if no **src()** and no **filterRecords()**

filterRecords

Required if src is empty. It is an array of options, where name is a title of <option/>, and valueis a value of <option/>

addFilterRecords( ListFilterRecord::name('Client')->value(1), ListFilterRecord::name('Guest')->value(2), ListFilterRecord::name('Manager')->value(3), ListFilterRecord::name('Admin')->value(4), )

Required if no src() and no distinct()

ListDateFilter

Date filter - allows you to select records within a selected date.

Example:

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListDateFilter; $page = new AdminixPage(); ... $page->addModule( ListModule... ->addFilters( ListDateFilter::field('created_at') ->name('Registration date') ) ) ...

Method

Description

field

Name of the field used for filtering. Field must have timestamp, date or datetime format.

field('created_at')

Required

name

Title of the filter

name('Registration date')

Required

ListDateRangeFilter

Date range filter - allows you to select records within a date range.

Example:

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\List\ListDateRangeFilter; $page = new AdminixPage(); ... $page->addModule( ListModule... ->addFilters( ListDateRangeFilter::field('created_at') ->name('Registration date') ->withTime() ) ) ...

Method

Description

field

Name of the field used for filtering. Field must have timestamp, date or datetime format.

field('created_at')

Required

name

Title of the filter

name('Registration date')

Required

withTime

Determines whether the filter will filter by time or only by dates.

By default, filter only by date, without time.

withTime()

Optional

filterFrom

Define "From" input. If set false, "From" datepicker will not be displayed.

By default, it is enabled.

filterFrom(false)

Optional

filterTo

Define "To" input. If set false, "To" datepicker will not be displayed.

By default, it is enabled.

filterTo(false)

Optional

Saved table views

savedTableViews() enables user-owned table view presets for a ListModule. It adds a Saved views section to the list settings menu where the current user can save, apply, and delete named presets.

use App\Models\Order; use AlexKudrya\Adminix\Modules\List\ListField; use AlexKudrya\Adminix\Modules\List\ListFilter; use AlexKudrya\Adminix\Modules\List\ListFilterRecord; use AlexKudrya\Adminix\Modules\List\ListModule; use AlexKudrya\Adminix\Modules\List\ListSearch; ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->savedTableViews() ->columnVisibility() ->densitySwitcher() ->search(ListSearch::searchFields(['number', 'customer_email'])) ->addFilter( ListFilter::field('status') ->name('Status') ->addFilterRecords( ListFilterRecord::name('Paid')->value('paid'), ListFilterRecord::name('Refunded')->value('refunded') ) ) ->addFields( ListField::name('ID')->field('id')->hiddenOnIndex(), ListField::name('Number')->field('number')->sortable()->hideable(), ListField::name('Status')->field('status')->hideable() );

Saved table views are different from ListLens. ListLens is server-defined PHP configuration. Saved table views are user-created presets stored by a provider.

Adminix stores only normalized table state:

  • current list search key, configured quick filter, configured filters, configured lens, and one valid configured sort key;

  • browser-only column visibility state for hideable() fields when columnVisibility() is enabled;

  • browser-side column order, width, and pin preferences for rendered list fields;

  • compact density when densitySwitcher() is enabled.

Pagination is not saved. Applying a saved table view removes the current list page and applies the saved search/quick/filter/lens/sort state to the URL. Unrelated query parameters and other list modules are preserved.

Standard database storage

The built-in database provider is opt-in. Enable it in config/adminix.php and run migrations:

'table_views' => [ 'storage' => true, 'provider' => null, ],
php artisan migrate

This creates adminix_table_views. Rows are scoped by server-derived owner key, page name, and module name. When a real admin user is authenticated, owner type and ID are derived from the current request user. When no_auth_access is enabled for local/demo use, table views share the anonymous owner scope.

Custom provider

For existing preference tables, multi-tenant storage, encrypted state, or non-SQL storage, bind or pass a custom provider:

use AlexKudrya\Adminix\Contracts\TableViewProviderInterface; // AppServiceProvider $this->app->singleton(TableViewProviderInterface::class, App\Admin\AdminTableViewProvider::class);

Or configure a single module:

ListModule::name('orders') ->savedTableViews(App\Admin\AdminTableViewProvider::class);

Providers implement:

use AlexKudrya\Adminix\Contracts\TableViewProviderInterface; use AlexKudrya\Adminix\Dto\TableViewDeleteRequestDto; use AlexKudrya\Adminix\Dto\TableViewQueryDto; use AlexKudrya\Adminix\Dto\TableViewRecordDto; use AlexKudrya\Adminix\Dto\TableViewResultDto; use AlexKudrya\Adminix\Dto\TableViewSaveRequestDto; class AdminTableViewProvider implements TableViewProviderInterface { public function tableViews(TableViewQueryDto $query): TableViewResultDto|array {} public function saveTableView(TableViewSaveRequestDto $request): TableViewRecordDto|array|null {} public function deleteTableView(TableViewDeleteRequestDto $request): bool {} }

adminix:doctor warns when savedTableViews() is enabled without standard storage, a module provider, or a custom TableViewProviderInterface binding.

Security notes:

  • browser requests cannot choose owner type, owner ID, page params, provider, datasource, fields, filters, lenses, or sortable fields;

  • Adminix verifies a signed table view context before every list/save/delete request;

  • server normalization drops unknown query keys, unknown columns, invalid sort directions, invalid column order/width/pin state, and unsupported UI state before the provider sees the request.

Reset controls

resetControls() adds a Reset view action to the list settings menu for the current ListModule. It is disabled until the current URL or browser storage contains state for that list.

ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->resetControls() ->search( ListSearch::searchFields(['number', 'customer_email']) ->placeholder('Search orders') ) ->addFilters(...) ->addLenses(...) ->addFields(...);

Result: when the URL contains keys such as search-orders, quick-orders, filter-orders-status, sort-orders-created_at, lens-orders, or page, Adminix shows an enabled Reset view action inside the gear-icon list settings menu. The same action is enabled when browser-only view state exists for the list, such as hidden/shown columns, custom column order, custom column widths, custom pinned columns, or compact density. Clicking it returns to the same page without current-list URL state and clears current-list browser view state. Unrelated query parameters and another list module's view state are preserved.

This is a presentation and URL-state convenience only. It does not create a new server-side action endpoint and does not change list filters, row actions, datasource, or authorization behavior.

Mobile behavior

ListModule keeps the same PHP configuration and request contract on mobile. Search, lenses, filters, sorting, pagination, editable rows, row actions, and modal togglers still use the same module name and server-side configuration.

Example configuration:

ListModule::name('orders') ->title('Orders') ->dataSource(Order::class) ->search( ListSearch::searchFields(['number', 'customer_email']) ->placeholder('Search orders') ) ->addFilters( ListFilter::field('status') ->name('Status') ->addFilterRecords( ListFilterRecord::name('Paid')->value('paid'), ListFilterRecord::name('Refunded')->value('refunded') ), ListDateRangeFilter::field('created_at') ->name('Created') ) ->addActions( AdminixLinkModule::name('details') ->title('Details') ->uri(OrderPage::URI) ->params(['record:id']) ) ->addFields(...);

Result:

  • desktop renders filters inline above the table;

  • narrow screens show a Filters button with an active-filter count;

  • the button opens a drawer containing the same ListFilter, ListDateFilter, and ListDateRangeFilter controls;

  • changing a filter still writes the same filter-{moduleName}-{field} query key and resets pagination;

  • wide tables scroll horizontally inside the table area, not across the whole Adminix shell;

  • row actions remain the existing buttons/forms/modal togglers, including route params, CSRF methods, confirm text, icons, and tooltips.

The mobile drawer is a presentation wrapper only. Do not add browser-owned datasource, action, or filter metadata; continue to configure lists from PHP classes.

Soft-delete row actions

restoreAction() and forceDeleteAction() enable built-in row forms for Eloquent models that use Laravel SoftDeletes. Use softDeleteActions() to enable both actions on the same list.

use App\Models\User; use AlexKudrya\Adminix\Modules\List\ListField; use AlexKudrya\Adminix\Modules\List\ListFieldTypeEnum; use AlexKudrya\Adminix\Modules\List\ListModule; ListModule::name('users') ->dataSource(User::class) ->criteria([ ['tenant_id', '=', 'param:0'], ]) ->withDeleted() ->softDeleteActions() ->rowActionsDropdown() ->addFields( ListField::name('ID')->field('id')->type(ListFieldTypeEnum::HIDDEN), ListField::name('Name')->field('name'), ListField::name('Status')->field('status'), );

Result: Adminix renders Restore and Force delete controls only for rows whose source Eloquent model is currently trashed. Active rows do not receive those built-in controls. The forms submit with CSRF and a signed page/module/route-param context to the Adminix soft-delete action endpoint.

Requirements:

  • the list dataSource() must be an Eloquent model class using SoftDeletes;

  • the list must expose trashed rows through withDeleted() or through at least one ListLens::withDeleted();

  • the primary key must be present in the rendered row, usually as a hidden ListField;

  • tenant/user boundaries must still be expressed in server-side criteria(), policies, guards, or relation context.

On submit Adminix re-resolves the page, module, relation context, route params, datasource, primary key, criteria, active lens, filters, search, sorting, and current request scope from server-side configuration. It then verifies that the target record is still inside that scope and still trashed() before calling restore() or forceDelete(). Browser-provided datasource, writable fields, primary-key names, deleted state, tenant ids, and param:* values are not trusted.

forceDeleteAction() is irreversible. Use it only for records where permanent deletion is acceptable for your application.

adminix:doctor reports:

  • an error when restore/force-delete actions are configured on a non-Eloquent datasource or a model without SoftDeletes;

  • a warning when actions are enabled but neither the module nor any lens can expose soft-deleted rows.

Successful restore and force-delete operations dispatch audit events and flash typed Adminix notifications.

Clone actions

cloneAction() enables a built-in per-row clone form. bulkCloneAction($batchLimit = null) adds a built-in adminix_clone bulk action to the existing bulk action pipeline. Use cloneActions() to enable both on the same list.

use App\Models\Order; use AlexKudrya\Adminix\Modules\List\ListField; use AlexKudrya\Adminix\Modules\List\ListFieldTypeEnum; use AlexKudrya\Adminix\Modules\List\ListModule; ListModule::name('orders') ->dataSource(Order::class) ->criteria([ ['tenant_id', '=', 'param:0'], ]) ->primaryKey('id') ->cloneActions() ->rowActionsDropdown() ->addFields( ListField::name('ID')->field('id')->type(ListFieldTypeEnum::HIDDEN), ListField::name('Number')->field('number'), ListField::name('Status')->field('status'), ListField::name('Tenant')->field('tenant_id')->type(ListFieldTypeEnum::HIDDEN) );

Result: Adminix renders a Clone row action for rows with the configured primary key and a Clone option in the bulk action dropdown. The row form submits with CSRF and a signed page/module/route-param context. The bulk option uses the normal bulk endpoint, selected IDs, confirmation, batch limit, and scope filtering.

Clone data is derived server-side. By default Adminix copies rendered index ListField columns that exist in the datasource table. It skips the primary key, hidden fields, created_at, updated_at, and deleted_at. It also merges equality values from server-side criteria() after signed page params are resolved, so tenant or parent scope fields such as tenant_id = param:0 can be preserved even when the scope field is hidden.

On submit Adminix re-resolves the page, module, relation context, route params, datasource, primary key, criteria, active lens, filters, search, sorting, and current request scope from server-side configuration. The source record must still be inside the current list scope. The browser cannot choose the datasource, clone fields, writable fields, primary-key names, tenant ids, or param:* values.

Requirements and caveats:

  • the list dataSource() must resolve to an Eloquent model table or a plain SQL table;

  • at least one rendered index ListField must be backed by a datasource column after skipped fields are removed;

  • unique columns, required hidden columns, custom defaults, file paths, and sensitive fields should be modeled intentionally in the consuming app before clone is enabled;

  • bulkCloneAction($batchLimit) can lower the default selected-row cap; the default is the normal bulk limit of 1000;

  • relation child lists keep their signed relation context and parent scope.

adminix:doctor reports:

  • an error when clone actions are configured for a datasource that cannot be resolved to an existing SQL/Eloquent table;

  • a warning when clone actions are enabled but no cloneable rendered index field remains.

Successful row and bulk clone operations dispatch resource.cloned audit events and flash typed Adminix notifications. Bulk clone also records the normal bulk.executed audit event with the scope-filtered selected IDs.

Actions

"Actions" is an array of control buttons for every single record, to do some actions, such as "open", "edit", "delete", or wherever else you need.

Example:

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\Link\AdminixLinkModule; use AlexKudrya\Adminix\Modules\Link\LinkModule; use AlexKudrya\Adminix\Enums\HttpMethodsEnum; $page = new AdminixPage(); ... $page->addModule( ListModule... ->addActions( AdminixLinkModule::name('edit') ->title('EDIT') ->icon('bi bi-pencil') ->uri(UserPage::URI) ->params(['record:id']), LinkModule::name('delete') ->title('BAN') ->icon('bi bi-person-fill-slash') ->uri('ban_user') ->method(HttpMethodsEnum::POST) ->params(['record:id']) ->criteria([ ['deleted_at', '==', null] ]), LinkModule::name('restore') ->title('UNBAN') ->icon('bi bi-person-fill-up') ->uri('unban_user') ->method(HttpMethodsEnum::POST) ->params(['record:id']) ->criteria([ ['deleted_at', '!=', null] ]), ) ) ...

To add Action buttons to List module you need paste AlexKudrya\Adminix\Modules\Link\AdminixLinkModule or AlexKudrya\Adminix\Modules\Link\LinkModule class instances as an argument to addAction or addActions method of AdminixPage object.

Action buttons syntax is similar to Link module

By default, Adminix renders row actions inline in the final service column. For dense tables with many row controls, call rowActionsDropdown() on the same ListModule to render one compact vertical three-dots button per row. Clicking it opens a small contextual dropdown next to the trigger and shows the same configured actions inside the menu.

ListModule::name('users') ->dataSource(User::class) ->rowActionsDropdown() ->addFields(...) ->addActions( AdminixLinkModule::name('edit') ->title('Edit') ->icon('bi bi-pencil') ->uri(UserPage::URI) ->params(['record:id']), LinkModule::name('ban') ->title('Ban') ->icon('bi bi-person-fill-slash') ->uri('ban_user') ->method(HttpMethodsEnum::POST) ->params(['record:id']) );

Result: every row keeps a single bi-three-dots-vertical trigger in the action column. The dropdown contains the existing GET links, POST/PUT/PATCH/DELETE forms with CSRF and confirm prompts, and modal togglers. If an action has an icon but no title, the dropdown uses the action name() as a text label so the menu remains readable. Rows where criteria() hides all actions still render an empty service cell to keep the table columns aligned.

rowActionsDropdown() is presentation-only. It does not change action authorization, criteria evaluation, route parameter substitution, signed modal relation context, request payloads, datasource, or persistence behavior.

Method

Description

name

Title which will be displayed on the button

name('EDIT')

Optional

uri

In case AdminixLinkModule uri means the uri directive of page which defined in Adminix config file. In this case better to use URI constant from target page provider class.

uri(UserPage::URI)

In case LinkModule uri means the name of any route defined in router of your Laravel application. Just copy route name from router.

uri('edit_user')

Required

icon

Icon class name from Bootsrap icons

Example: to generate in the Link an icon <i class="bi bi-people-fill"></i> you need paste here bi bi-people-fill class name

icon('bi bi-people-fill')

Optional

method

Type of HTTP request, can be provided only by AlexKudrya\Adminix\Enums\HttpMethodsEnum Enum class

In case AdminixLinkModule method can be only GET so it is redundant.

In case LinkModule method must be match to the route defined in uri directive.

By default, it is equal to HttpMethodsEnum::GET

method(HttpMethodsEnum::POST)

Required if not GET

params

Technically it is optional directive, but action without params makes it pointless, so it is make it required.

It is list of parameters which you added to your action (usually it is "id"). When record:id, means that id of current record will be the parameter. By the way, for this purpose, if you not display id field i your list, you need add it to fields directive, if you do not want to display it you may add this field with HIDDEN type.

Parameters adds to your link like this: /parameter 1/parameter 2...

Example:

params(['record:id'])

For record with id = 123 action link will be https://example/uri/123

Required

criteria

Determines display or not this action button for current record. For example if you need display action button only for users without admin status, you can do it like this:

criteria([ ['is_admin', '==', false] ])

or

criteria([ ['deleted_at', '!=', null] ])

Action visibility criteria are evaluated against the rendered list row, not the database query builder. For row actions this directive intentionally supports only == and !=, including null checks.

Optional

Usually, in the admin panels, lists have the ability to add new records, and the New resource module is used for this. In order to display a link to a page with such a module in the correct place in the "list" module, there is this directive

Format is similar to Link module or Actions

Example:

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\Link\AdminixLinkModule; $page = new AdminixPage(); ... $page->addModule( ListModule... ->newItemLink( AdminixLinkModule::name('new_user_btn') ->title('NEW USER') ->icon('bi bi-person-fill-add') ->uri(NewUserPage::URI) ) ) ...

or

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\Link\AdminixLinkModule; $page = new AdminixPage(); ... $page->addModule( ListModule... ->newItemLink( ModalTogglerModule::name('new_user_btn') ->title('NEW USER') ->icon('bi bi-person-fill-add') ->modalName('new_user_modal') ) ) ...

Appearance examples

Here is example of List without editable mode:

image.png

Here is example of List with editable mode:

image.png
Last modified: 01 July 2026