Skip to main content

Core Table Extensions

This document covers the custom table extensions built on top of Inertia Table. These components extend the base functionality with application-specific features like stats, row styling, domain object integration, and advanced column types.
Base Documentation: See Inertia Table API Reference for core functionality.

CoreTable Class

The CoreTable class extends the base Inertia Table with additional functionality through traits:
use Inly\Core\Tables\CoreTable;

class UserTable extends CoreTable
{
    use WithStats;

    // Inherits WithRowStyles traits automatically
    // Automatically includes CoreTableExport in exports()
    // Automatically named based on class name (e.g., 'UserTable' -> 'user')

    public function resource(): Builder|string
    {
        return User::query();
    }

    // Override stats if needed
    protected function stats(): array
    {
        return [
            'total_users' => [
                'label' => __('Total Users'),
                'value' => $this->queryWithRequestApplied()->count()
            ],
            'active_users' => [
                'label' => __('Active Users'),
                'value' => $this->queryWithRequestApplied()->where('is_active', true)->count()
            ],
        ];
    }
}

Automatic Table Naming

CoreTable automatically names tables based on their class name to prevent query string conflicts when multiple tables are on the same page. The naming convention:
  1. Removes the Table suffix from the class name (e.g., TransactionTableTransaction)
  2. Converts to kebab-case (e.g., AccountTransactionRequestaccount-transaction-request)
Examples:
  • TransactionTable → automatically named transaction
  • AccountTransactionRequestTable → automatically named account-transaction-request
  • UserTable → automatically named user

Overriding the Default Name

You can override the automatic name in several ways: 1. Override getDefaultName() method:
class CustomUserTable extends CoreTable
{
    protected function getDefaultName(): string
    {
        return 'custom-users';
    }
}
2. Use as() method after instantiation:
// In your controller
$userTable = UserTable::make()->as('custom-users');
3. Override in onMake() hook:
class UserTable extends CoreTable
{
    public function onMake(): void
    {
        $this->as('custom-users');
    }
}

Why Table Names Matter

When you have multiple tables on the same page, each table maintains its own state (pagination, sorting, filtering) via query string parameters. Without unique names, tables would share the same query parameters, causing conflicts:
❌ Without naming: ?sort=name&page=2
   (Both tables would try to use the same sort/page parameters)

✅ With automatic naming: ?user[sort]=name&user[page]=1&order[sort]=created_at&order[page]=2
   (Each table maintains its own independent state)
The automatic naming ensures tables are always uniquely identified, preventing conflicts even when multiple tables are displayed on the same page.

Core Actions

CoreTable supports adding CoreAction instances directly to tables via the coreActions() method. These actions are automatically rendered in the table’s row actions and/or bulk actions bar based on their configuration.

Basic Usage

use Inly\Core\Tables\CoreTable;
use App\Actions\DeactivateUsersAction;
use App\Actions\SendNotificationAction;

class UserTable extends CoreTable
{
    public function coreActions(): array
    {
        return [
            // Row action only
            DeactivateUsersAction::make(),

            // Bulk action only
            SendNotificationAction::make()->asBulkAction(),

            // Both row and bulk action
            ArchiveUsersAction::make()->asBulkAction(),
        ];
    }
}

With WithTableAction Trait

For actions that operate on table rows, use the WithTableAction trait to get automatic authorization, validation, and query building:
use Inly\Core\Actions\CoreAction;
use Inly\Core\Actions\Traits\WithTableAction;

class DeactivateUsersAction extends CoreAction
{
    use WithTableAction;

    // Enable '*' (select all) support with table filters and model inference
    protected function getTable(): string
    {
        return UserTable::class;
    }

    protected function defaults(): void
    {
        $this->label(__('Deactivate'));
        $this->icon('UserX');
        $this->asBulkAction(true);
    }

    protected function authorizeModel(User $model): bool
    {
        return user()?->can('update', $model);
    }

    protected function handle(): mixed
    {
        $this->processModels(function (User $user) {
            $user->update(['active' => false]);
        });

        return $this->backWithSuccess();
    }
}

How It Works

  1. Actions are serialized with the table via toArray() and passed to the frontend
  2. Row actions receive id param automatically when triggered from a row
  3. Bulk actions receive ids array (or ['*'] for select all) when triggered from bulk actions bar
  4. Actions with asBulkAction(true) appear in the bulk actions bar when rows are selected
See: Core Action Documentation for complete WithTableAction usage.

Exports

CoreTable automatically includes a CSV export via CoreTableExport. The export:
  • Exports all visible table columns
  • Generates filename based on the model’s collection title (e.g., users_2024-01-15.csv)
  • Always queues the export to prevent timeouts
  • Sends an email notification when the export is ready
You can customize or override the export by implementing the exports() method:
public function exports(): array
{
    $model = $this->resourceBuilder()->getModel();

    return [
        CoreTableExport::make(
            fileNamePrefix: $model instanceof ObjectContract
                ? __($model->getObjectCollectionTitle())
                : null
        ),
    ];
}

Helper Methods

getFilterValue()

Retrieves the value of an active filter by its attribute name. Returns null if the filter is not active or not enabled. Throws a 400 error if the filter exists but is disabled.
$campaignIds = $this->getFilterValue('accounts.campaign_id');

if ($campaignIds !== null) {
    $query->whereIn('campaign_id', $campaignIds);
}

Traits

WithStats Trait

Adds statistical information to your tables that can be displayed in the frontend.

Basic Usage

// Stats are automatically included when using CoreTable
class ProductTable extends CoreTable
{
    protected function stats(): array
    {
        return [
            'total_products' => [
                'label' => __('Total Products'),
                'value' => $this->queryWithRequestApplied()->count()
            ],
            'avg_price' => [
                'label' => __('Average Price'),
                'value' => number_format($this->queryWithRequestApplied()->avg('price'), 2)
            ],
            'out_of_stock' => [
                'label' => __('Out of Stock'),
                'value' => $this->queryWithRequestApplied()->where('stock', 0)->count()
            ],
        ];
    }
}

Frontend Usage

Stats are automatically included in the table resource and can be accessed in your frontend:
import { InertiaTable } from '@/core/components/inertia-table';

export default function Products({ productTable }: Props) {
  return (
    <div>
      {/* Display stats */}
      {productTable.stats && (
        <div className="grid grid-cols-3 gap-4 mb-6">
          {productTable.stats.map(stat => (
            <div key={stat.key} className="bg-white p-4 rounded-lg border">
              <div className="text-sm text-gray-600">{stat.label}</div>
              <div className="text-2xl font-bold">{stat.value}</div>
            </div>
          ))}
        </div>
      )}

      <InertiaTable resource={productTable} />
    </div>
  );
}

WithRowStyles Trait

Provides default row styling based on model state, particularly for soft-deleted models.

Default Behavior

// Automatically applied in CoreTable
public function dataAttributesForModel(Model $model, array $data): array
{
    return [
        'trashed' => $model->trashed(), // Adds data-trashed attribute for CSS styling
    ];
}

CSS Styling

The trait works with CSS classes defined in app.css:
/* Example row styles for trashed items */
tr[data-trashed='true'] {
  @apply opacity-60 bg-gray-50;
}

Custom Row Styling

Override the method to add custom row attributes:
class OrderTable extends CoreTable
{
    public function dataAttributesForModel(Model $model, array $data): array
    {
        return [
            'trashed' => $model->trashed(),
            'status' => $model->status,
            'overdue' => $model->due_date?->isPast(),
        ];
    }
}

SearchUsingScout Trait

Enables Laravel Scout search functionality instead of the default database LIKE queries.

Basic Usage

use Inly\Core\Tables\Traits\SearchUsingScout;

class ProductTable extends CoreTable
{
    use SearchUsingScout;

    protected ?string $resource = Product::class;

    // Scout search will automatically be used instead of database search
}

Custom Scout Configuration

Override the search behavior if needed:
class ProductTable extends CoreTable
{
    use SearchUsingScout;

    protected function applyScoutSearch(QueryBuilder $queryBuilder): void
    {
        $queryBuilder->searchUsing(function (Builder $query, string $search) {
            if ($search) {
                // Custom Scout search logic
                $ids = Product::search($search)
                    ->where('published', true)
                    ->get()
                    ->pluck('id');

                $query->whereIn('id', $ids);
            }
        });
    }
}

Custom Columns

CoreColumn Base Class

Base class for all custom columns that provides enhanced constructor parameters:
use Inly\Core\Tables\Columns\CoreColumn;

abstract class MyCustomColumn extends CoreColumn
{
    public function onMake(): void
    {
        // Hook method for post-construction setup
        parent::onMake();

        // Custom initialization logic
    }
}

DetailedLinkColumn

Displays rich content with title, subtitle, icon, and optional user avatar. Perfect for entity references with navigation.

Basic Usage

use Inly\Core\Tables\Columns\DetailedLinkColumn;

DetailedLinkColumn::make('company', __('Company'))
    ->title(fn($model) => $model->company->name)
    ->subtitle(fn($model) => $model->company->industry)
    ->icon(fn($model) => 'Building2')
    ->url(fn($model) => route('companies.show', $model->company));

With Avatar

DetailedLinkColumn::make('assigned_user', __('Assigned To'))
    ->title(fn($model) => $model->assignedUser->name)
    ->subtitle(fn($model) => $model->assignedUser->email)
    ->avatar(fn($model) => $model->assignedUser->getFirstMediaUrl('profile_picture', 'preview'))
    ->avatarFallback(fn($model) => $model->assignedUser->name)
    ->url(fn($model) => route('users.show', $model->assignedUser));

Frontend Rendering

The column renders as a clickable component with:
  • Title: Primary text (bold)
  • Subtitle: Secondary text (smaller, muted)
  • Avatar/Icon: Displays in order of priority:
    1. Avatar image (if provided)
    2. Avatar with initials from fallback name (if provided)
    3. Icon (if provided)
  • Navigation: Automatic routing via URL

ObjectColumn

Automatically extracts display information from models implementing the domain object pattern. Extends DetailedLinkColumn with automatic data population.

Model Requirements

Models must implement the HasDomainObject trait and required methods:
use Inly\Core\Models\Traits\HasDomainObject;

class Company extends Model
{
    use HasDomainObject;

    public function getObjectTitle(): ?string
    {
        return $this->name;
    }

    public function getObjectSubtitle(): ?string
    {
        return $this->industry;
    }

    public function getObjectIcon(): ?string
    {
        return 'Building2';
    }

    public function getObjectUrl(): ?string
    {
        return route('companies.show', $this);
    }

    public static function getObjectCollectionTitle(?bool $singular = false): string
    {
        return __('Companies');
    }
}

Column Usage

use Inly\Core\Tables\Columns\ObjectColumn;

// Direct model access
ObjectColumn::make('company', __('Company'))
    ->sortable();

// Nested relationship
ObjectColumn::make('user.company', __('User Company'))
    ->sortable();

// Custom model accessor
ObjectColumn::make('primary_contact', __('Contact'))
    ->asModel(fn($account) => $account->primaryContact);

CoreBadgeColumn

Displays badge components with automatic enum support or manual configuration.

Automatic Enum Usage

Works automatically with enums implementing the SerializeAsBadge trait:
use Inly\Core\Tables\Columns\CoreBadgeColumn;

// Automatically extracts label, variant, and icon from enum
CoreBadgeColumn::make('status', __('Status'))
    ->sortable();

CoreBadgeColumn::make('deal.stage', __('Stage'));

Manual Configuration

CoreBadgeColumn::make('priority', __('Priority'))
    ->label(fn($priority) => match($priority) {
        1 => 'Low',
        2 => 'Medium',
        3 => 'High',
    })
    ->variant(['low' => 'secondary', 'medium' => 'warning', 'high' => 'destructive'])
    ->icon(['high' => 'AlertCircle']);

Export Behavior

Exports only the label text, making it suitable for CSV/Excel exports.

InlineEditColumn

Enables inline editing of column values directly in the table.

Basic Usage

use Inly\Core\Tables\Columns\InlineEditColumn;

InlineEditColumn::make('name', __('Name'))
    ->placeholder(__('Enter name'))
    ->method('patch');

Configuration Options

InlineEditColumn::make('price', __('Price'))
    ->inputType('number')
    ->placeholder('0.00')
    ->method('put')
    ->inputName('product_price')
    ->hideIcon(); // Hide the edit icon

Backend Handling

The column will send requests to your resource routes:
// routes/web.php
Route::patch('/products/{product}', [ProductController::class, 'update']);

// ProductController
public function update(UpdateProductRequest $request, Product $product)
{
    $product->update($request->validated());

    return back()->withSuccess(__('Product updated successfully'));
}
// app/Http/Requests/UpdateProductRequest.php
class UpdateProductRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => __('Product name is required'),
            'name.max' => __('Product name cannot exceed 255 characters'),
        ];
    }
}

Custom Actions

CoreTableAction

Base class for all custom table actions. Extends InertiaUI’s Action class and provides a hook method (onMake()) for post-construction customization.

Purpose

CoreTableAction serves as the foundation for building custom action classes. It provides an onMake() hook that is called after the action is instantiated, allowing subclasses to perform custom initialization logic.

Basic Usage

When creating custom action classes, extend CoreTableAction instead of the base Action class:
use Inly\Core\Tables\Actions\CoreTableAction;

class CustomAction extends CoreTableAction
{
    public function onMake(): void
    {
        // Custom initialization logic after action is created
        $this->icon('custom-icon');
        $this->asRowAction();
    }
}

The onMake() Hook

The onMake() method is automatically called after an action is instantiated via make(). Override this method to perform custom setup:
class ArchiveAction extends CoreTableAction
{
    public function onMake(): void
    {
        // Set default configuration
        $this->icon('archive')
            ->confirmationRequired()
            ->confirmationMessage(__('Are you sure you want to archive this item?'));
    }
}

When to Use

  • Creating custom action classes: Extend CoreTableAction when building reusable action classes with custom behavior
  • Post-construction setup: Use onMake() when you need to configure actions after instantiation but before they’re used
  • Base for specialized actions: Use as a base class for actions with specific functionality (like CoreModalAction)

Available Methods

CoreTableAction inherits all methods from InertiaUI’s Action class, including:
  • url(Closure $url) - Set the action URL
  • handle(Closure $handle) - Set the action handler
  • asRowAction() - Show in row actions
  • asBulkAction() - Show in bulk actions dropdown
  • icon(string $icon) - Set the icon
  • hidden(Closure|bool $hidden) - Control visibility
  • authorize(Closure|bool $authorize) - Control authorization
  • confirmationRequired() - Require confirmation before execution
  • And many more…

CoreModalAction

Enables table actions that open modals (dialogs or sheets) instead of navigating to a new page. Extends CoreTableAction and supports both URL-based modals (server-rendered) and local modals (client-side).

URL-Based Modal (Server-Rendered)

Opens a modal that loads content from a route. CoreModalAction is modal by default (opens as a centered dialog). The selectedItems (for bulk actions) or row ID (for row actions) are automatically passed to the modal page via the selectedItems prop:
use Inly\Core\Tables\Actions\CoreModalAction;

CoreModalAction::make(__('Create user'))
    ->url(fn () => route('users.create'))
    ->asButton()
    ->icon('user-plus');
In your controller, the selectedItems are automatically available:
public function create()
{
    // selectedItems is automatically available from the request
    $selectedItems = request()->input('selectedItems'); // Array of IDs for bulk actions, or [rowId] for row actions

    return CoreModalPage::make('users/user-create')
        ->title(__('New user'))
        ->with([
            'roles' => role_options(true),
            // selectedItems is automatically passed as a prop to the component
        ]);
}
In your React component, access selectedItems via props:
interface Props {
  roles: ComboboxOption[];
  selectedItems?: string[]; // Automatically passed from CoreModalPage
}

export default function UserCreate({ roles, selectedItems }: Props) {
  // selectedItems contains:
  // - For row actions: [rowId] (single item array)
  // - For bulk actions: [id1, id2, ...] (array of selected IDs)

  return <CoreModalPage>{/* Modal content */}</CoreModalPage>;
}
CoreModalAction::make(__('Edit user'))
    ->url(fn ($user) => route('users.edit', $user))
    ->modalClassName('sm:max-w-2xl')
    ->asRowAction();

Sheet (Slideover) Modal

Opens a modal as a slideover panel instead of a centered dialog:
use Inly\Core\Enums\SheetPosition;

// Position can be specified directly in sheet()
CoreModalAction::make(__('Settings'))
    ->url(fn () => route('settings'))
    ->sheet(position: SheetPosition::RIGHT) // SheetPosition::RIGHT, SheetPosition::LEFT, SheetPosition::TOP, or SheetPosition::BOTTOM
    ->asButton();

// Or separately using position()
CoreModalAction::make(__('Settings'))
    ->url(fn () => route('settings'))
    ->sheet()
    ->position(SheetPosition::RIGHT)
    ->asButton();

Local Modal (Client-Side)

Opens a modal defined directly on the page (useful for forms that don’t require a full page load):
// In your table actions
CoreModalAction::make(__('Update status'))
    ->localModal('update-transaction-status')
    ->asRowAction()
    ->asBulkAction()
    ->icon('settings');
Then define the modal in your frontend component:
import { HeadlessModal } from '@inertiaui/modal-react';
import { InertiaTable, useTableAction } from '@/core/components/inertia-table';

export default function Transactions({ transactionTable }) {
  return (
    <>
      <InertiaTable
        resource={transactionTable}
        modals={({ actions }) => (
          <HeadlessModal name="update-transaction-status">
            {({ close, isOpen }) => (
              <UpdateStatusModal
                close={close}
                isOpen={isOpen}
                selectedItems={actions.selectedItems}
              />
            )}
          </HeadlessModal>
        )}
      />
    </>
  );
}

function UpdateStatusModal({ close, isOpen, selectedItems }) {
  const { rowActionId } = useTableAction();

  // Use rowActionId for single row actions
  // Use selectedItems for bulk actions
  const ids = rowActionId ? [rowActionId] : selectedItems;

  return (
    <CoreDialog open={isOpen} onOpenChange={open => !open && close()}>
      {/* Modal content */}
    </CoreDialog>
  );
}

Action Configuration

CoreModalAction supports all standard action configuration methods:
CoreModalAction::make(__('Edit'))
    ->url(fn ($item) => route('items.edit', $item))
    ->asRowAction() // Show in row actions
    ->asBulkAction() // Show in bulk actions dropdown
    ->icon('edit')
    ->hidden(fn ($item) => !$item->canEdit())
    ->authorize(fn ($item) => Gate::allows('edit', $item));

Available Methods

  • sheet(bool $isSheet = true, SheetPosition|string|null $position = null) - Opens as a slideover panel instead of a centered dialog (default is dialog). Position can be specified directly as the second parameter.
  • modalClassName(string $className) - Sets the modal max-width (e.g., 'sm:max-w-sm', 'sm:max-w-2xl lg:max-w-4xl')
  • localModal(string $name) - References a local modal by name (without # prefix)
  • position(SheetPosition|string $position) - Sets sheet position (SheetPosition::RIGHT, SheetPosition::LEFT, SheetPosition::TOP, or SheetPosition::BOTTOM) - only valid when sheet() is called
  • url(Closure $url) - Sets the URL for URL-based modals
  • All standard action methods: asRowAction(), asBulkAction(), icon(), hidden(), authorize(), etc.
Note: CoreModalAction is modal by default (opens as a centered dialog). Call sheet() to open as a slideover panel instead.

Frontend Integration

Local Modals
When using local modals, the table provides a context (TableActionProvider) that allows modals to access:
  • Row Action ID: The ID of the row that triggered the action (via useTableAction() hook)
  • Selected Items: The currently selected items for bulk actions (via the modals slot prop)
The modals slot prop receives the table state, including actions.selectedItems for bulk operations.
URL-Based Modals
For URL-based modals, selectedItems are automatically passed to the modal page:
  • Row actions: selectedItems contains [rowId] (single item array)
  • Bulk actions: selectedItems contains the array of selected item IDs
The selectedItems are available:
  • In the controller via request()->input('selectedItems')
  • As a prop in your React component (automatically passed by CoreModalPage)
  • In pageData if you need to access them via useModal() hook
import { useModal } from '@inertiaui/modal-react';

export default function UserCreate({ selectedItems }: Props) {
  const modal = useModal();

  // Access via props (recommended)
  console.log('Selected items:', selectedItems);

  // Or access via modal hook
  const modalData = modal?.props?.pageData as Inly.Core.Data.CoreModalPageData;
  console.log('Selected items from pageData:', modalData?.selectedItems);

  return <CoreModalPage>{/* Content */}</CoreModalPage>;
}

Domain Object Integration

HasDomainObject Trait

Provides standardized domain object functionality for models, including search capabilities and data formatting.

Required Methods

use Inly\Core\Models\Traits\HasDomainObject;

class Product extends Model
{
    use HasDomainObject;

    // Required: Primary display name
    public function getObjectTitle(): ?string
    {
        return $this->name;
    }

    // Required: Navigation URL
    public function getObjectUrl(): ?string
    {
        return route('products.show', $this);
    }

    // Required: Collection title for UI
    public static function getObjectCollectionTitle(?bool $singular = false): string
    {
        return __('Products');
    }

    // Optional: Secondary text
    public function getObjectSubtitle(): ?string
    {
        return $this->category?->name;
    }

    // Optional: Icon identifier
    public function getObjectIcon(): ?string
    {
        return 'Package';
    }
}

Global Search Configuration

The HasDomainObject trait also provides global search functionality (separate from table search):
// Override global search behavior (not table-specific)
public static function searchQuery(string $query): \Laravel\Scout\Builder
{
    return static::search($query)
        ->where('published', true)
        ->take(50);
}

// Set global search priority (higher = appears first)
public static function getSearchPriority(): int
{
    return 10; // Higher priority than default (0)
}

// Control global search access
public static function allowGlobalSearch(): bool
{
    return Gate::allows('index', static::class);
}
Note: These methods are for application-wide global search, not table-specific search functionality.

Domain Object Data Access

// Access formatted data via attribute
$product = Product::first();
$domainData = $product->domain_object;

// Returns DomainObjectData with:
// - id, title, descriptiveTitle, subtitle, icon, url, collectionTitle

Best Practices

  1. Use CoreTable: Always extend CoreTable instead of the base Table class to get stats, row styling, and automatic exports.
  2. Domain Objects: Implement HasDomainObject trait on models that will be displayed in tables for consistent navigation and display.
  3. Stats Performance: Be mindful of expensive queries in stats. Consider caching or using database views for complex calculations.
  4. Row Styling: Use data attributes with CSS classes rather than inline styles for better maintainability.
  5. Column Organization: Group related functionality using the custom columns rather than complex cell overrides when possible.
  6. Search Strategy: Use SearchUsingScout for models with large datasets or complex search requirements.
  7. Exports: CoreTable automatically includes CSV exports. Override the exports() method only when you need custom export behavior or formats.
  8. Leverage Inertia deferred properties for complex tables: For tables with significant data or processing, return them as deferred properties in your controller using Inertia::defer(fn() => MyTable::make()). If your table extends CoreTable, you can use the convenient shorthand: MyTable::defer().
  9. Modal Actions: Use CoreModalAction for actions that should open modals instead of navigating to new pages. Prefer local modals for simple forms that don’t require server-side rendering, and URL-based modals for complex pages or when you need server-side data.
  10. Action Context: When using local modals with CoreModalAction, use the modals slot prop on InertiaTable to render modals within the table’s action context. This provides access to row action IDs and selected items via the useTableAction() hook.
This extension system provides a powerful foundation for building consistent, feature-rich tables throughout your application while maintaining the flexibility of the base Inertia Table functionality.