Skip to content

Table Columns, Filters, Grouping & Tabs ​

Table Columns ​

StateSelectColumn ​

An interactive select column that allows changing states directly from the table:

php
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateSelectColumn;

StateSelectColumn::make('state')
    ->sortable() // Enable custom state-based sorting
    ->ignoreTransitions(); // Allows direct state changes without transitions

Options:

  • sortable() — Enable sorting with custom workflow order (see Custom State Sorting)
  • ignoreTransitions() — Allow changing to any state, bypassing transition rules
  • Without ignoreTransitions() — Only allowed transitions are available

StateColumn ​

A display-only column that shows states with badges, colors, and icons, with support for custom sorting:

php
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateColumn;

StateColumn::make('state')
    ->label('Status')
    ->sortable(); // Enables custom state-based sorting

Key Features:

  • Automatic Badge Display: Shows state as a colored badge
  • Color & Icon Support: Automatically uses colors and icons from state metadata
  • Custom Sorting: Sorts by workflow order instead of alphabetically (see Custom State Sorting)
  • Display Only: Non-editable, ideal for read-only views

Basic Usage:

php
StateColumn::make('state')
    ->label('Order Status')
    ->sortable()

The column will automatically:

  • Display the state label (from HasLabel)
  • Apply the state color (from HasColor)
  • Show the state icon (from HasIcon)
  • Sort by custom order if defined (see below)

OpenRequestsColumn ​

A column that tells, at a glance, that a row awaits an answer: the request an earlier transition opened and no later one answered. It reads the same history the OpenRequestsEntry reads — the note and the term declared by the transition that asked — one record per row.

php
use RoBYCoNTe\FilamentFlow\Tables\Columns\OpenRequestsColumn;

OpenRequestsColumn::make('open_requests')
    ->noteField('review.notes')          // optional: override the declared path
    ->deadlineField('meta.deadline')     // optional: override the declared path
    ->hideDeadline()                     // omit the term
    ->toggleable();                      // already on: the reader may put it away

The column is created toggleable, so a list can offer it and each reader can hide it without the call deciding for them. A row with an open request wears a chip — “Waiting for a reply · 2 days”, or “Overdue” in red — with the note on hover; a row a decision was taken on wears the label and the colour of the state it moved to (a rejection in red, an approval in green); a row with nothing to answer shows a dash.

MethodDefaultDescription
noteField(?string $field)declaredOverrides the path of the note the transition declared.
deadlineField(?string $field)declaredOverrides the path of the term the transition declared.
hideDeadline(bool $hidden = true)falseLeaves the term out of the chip.
toggleable(bool | Closure $condition = true, ...)trueInherited from Filament: the reader shows or hides the column.

Cost. The cell is not a value of the row: the column queries the history once per record. On a list of a hundred rows that is a hundred queries — the price of a column that says something the row does not carry. It is why the column is toggleable, and why a host that keeps it should keep the page size modest.

The reading is also available outside the render:

php
$request = OpenRequestsColumn::make()->getRequest($order); // OpenRequest|null

TextColumn with Badge ​

Alternatively, use the standard Filament TextColumn to display states as badges:

php
use Filament\Tables\Columns\TextColumn;

TextColumn::make('state')
    ->badge()
    ->sortable();

StateExportColumn ​

Export model states to Excel or CSV with proper label formatting:

php
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateExportColumn;

StateExportColumn::make('state')
    ->label('Order Status');

Key Features:

  • Automatic Label Generation: Automatically uses state labels from HasLabel interface
  • Fallback to Morph Class: Uses the state's morph class name if no label is defined
  • Based on ExportColumn: All familiar ExportColumn modifiers can be used (e.g., label(), enabledByDefault())

Usage in Exporters:

php
<?php
// app/Filament/Exports/OrderExporter.php

namespace App\Filament\Exports;

use App\Models\Order;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateExportColumn;

class OrderExporter extends Exporter
{
    protected static ?string $model = Order::class;

    public static function getColumns(): array
    {
        return [
            ExportColumn::make('order_number')
                ->label('Order Number'),
            ExportColumn::make('customer_name')
                ->label('Customer'),
            ExportColumn::make('total_amount')
                ->label('Total'),
            
            // Export the state with automatic label formatting
            StateExportColumn::make('state')
                ->label('Status'),
                
            ExportColumn::make('created_at')
                ->label('Created'),
        ];
    }

    public static function getCompletedNotificationBody(Export $export): string
    {
        $body = 'Your order export has completed and ' . number_format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.';

        if ($failedRowsCount = $export->getFailedRowsCount()) {
            $body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.';
        }

        return $body;
    }
}

Customization Options:

php
// Use a custom label
StateExportColumn::make('state')
    ->label('Order Status')

// Disable by default in export selection
StateExportColumn::make('state')
    ->enabledByDefault(false)

// Use a different state attribute
StateExportColumn::make('payment_state')
    ->stateAttribute('payment_state')
    ->label('Payment Status')

Custom State Labels:

To provide custom labels for exported states, implement Filament's HasLabel interface:

php
use Filament\Support\Contracts\HasLabel;

class PendingState extends OrderState implements HasLabel
{
    public function getLabel(): string
    {
        return __("Pending");
    }
}

The StateExportColumn component will automatically use this method to format the exported value.

Complete Table Example ​

php
<?php
// app/Filament/Resources/OrderResource/Tables/OrdersTable.php

namespace App\Filament\Resources\OrderResource\Tables;

use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateSelectColumn;

class OrdersTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('order_number')
                    ->searchable()
                    ->sortable()
                    ->weight('bold'),
                    
                TextColumn::make('customer_name')
                    ->searchable()
                    ->sortable(),
                    
                TextColumn::make('total_amount')
                    ->money('EUR')
                    ->sortable()
                    ->alignEnd(),
                    
                StateSelectColumn::make('state')
                    ->ignoreTransitions(),
                    
                TextColumn::make('processed_at')
                    ->dateTime()
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
                    
                TextColumn::make('shipped_at')
                    ->dateTime()
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ]);
    }
}

Custom State Sorting ​

Filament Flow supports custom sorting for state columns, allowing you to order records by workflow logic instead of alphabetically or by database value.

Why Custom Sorting? ​

By default, sorting a state column would order states alphabetically (e.g., "Cancelled", "Delivered", "Pending", "Processing"). With custom sorting, you can define a logical workflow order like: Pending → Processing → Shipped → Delivered → Cancelled.

Implementation ​

Step 1: Add the Trait to Your Base State Class

php
<?php
// app/States/Order/OrderState.php

namespace App\States\Order;

use RoBYCoNTe\FilamentFlow\Concerns\HasStateMetadata;
use RoBYCoNTe\FilamentFlow\Concerns\HasStateSortOrder;
use RoBYCoNTe\FilamentFlow\Contracts\HasStateMetadata as HasStateMetadataContract;
use Spatie\ModelStates\State;
use Spatie\ModelStates\StateConfig;

abstract class OrderState extends State implements HasStateMetadataContract
{
    use HasStateMetadata;
    use HasStateSortOrder; // Add this trait

    public static function config(): StateConfig
    {
        // ...existing configuration...
    }
}

Step 2: Implement Sort Order in Each State

Add the HasStateSortOrder interface and getSortOrder() method to each concrete state class:

php
<?php
// app/States/Order/PendingState.php

namespace App\States\Order;

use RoBYCoNTe\FilamentFlow\Contracts\HasStateSortOrder;
// ...other imports...

final class PendingState extends OrderState implements HasStateSortOrder
{
    public static function getSortOrder(): int
    {
        return 1; // First in the workflow
    }

    // ...existing methods...
}
php
<?php
// app/States/Order/ProcessingState.php

final class ProcessingState extends OrderState implements HasStateSortOrder
{
    public static function getSortOrder(): int
    {
        return 2; // Second in the workflow
    }
    
    // ...existing methods...
}
php
<?php
// app/States/Order/ShippedState.php

final class ShippedState extends OrderState implements HasStateSortOrder
{
    public static function getSortOrder(): int
    {
        return 3; // Third in the workflow
    }
    
    // ...existing methods...
}
php
<?php
// app/States/Order/DeliveredState.php

final class DeliveredState extends OrderState implements HasStateSortOrder
{
    public static function getSortOrder(): int
    {
        return 4; // Fourth in the workflow
    }
    
    // ...existing methods...
}
php
<?php
// app/States/Order/CancelledState.php

final class CancelledState extends OrderState implements HasStateSortOrder
{
    public static function getSortOrder(): int
    {
        return 100; // Last - cancelled orders appear at the end
    }
    
    // ...existing methods...
}

Step 3: Enable Sorting in Your Table

Use StateSelectColumn or StateColumn with the sortable() method:

php
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateSelectColumn;
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateColumn;

// Option 1: Interactive select column with sorting
StateSelectColumn::make('state')
    ->label(__('Status'))
    ->sortable() // Custom sorting is automatically applied

// Option 2: Display-only column with sorting
StateColumn::make('state')
    ->label(__('Status'))
    ->sortable() // Custom sorting is automatically applied

Complete Example:

php
<?php
// app/Filament/Admin/Resources/Orders/Tables/OrdersTable.php

namespace App\Filament\Admin\Resources\Orders\Tables;

use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use RoBYCoNTe\FilamentFlow\Tables\Columns\StateSelectColumn;

class OrdersTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('order_number')
                    ->searchable()
                    ->sortable()
                    ->weight('bold'),
                    
                TextColumn::make('customer_name')
                    ->searchable()
                    ->sortable(),
                    
                TextColumn::make('total_amount')
                    ->money('EUR')
                    ->sortable()
                    ->alignEnd(),
                    
                // State column with custom sorting
                StateSelectColumn::make('state')
                    ->label(__('Status'))
                    ->sortable(), // Orders will sort by workflow order (1, 2, 3, 4, 100)
                    
                TextColumn::make('created_at')
                    ->dateTime()
                    ->sortable(),
            ])
            ->defaultSort('state', 'asc'); // Sort by state workflow order by default
    }
}

How It Works ​

When you enable sorting on a state column:

  1. Custom Query: The column generates a SQL CASE WHEN statement based on your sort order values
  2. Performance: Uses native SQL sorting for optimal performance
  3. Fallback: States without getSortOrder() default to order value 999
  4. Bidirectional: Works with both ascending and descending sort

Generated SQL Example:

sql
ORDER BY 
    CASE 
        WHEN `state` = 'App\States\Order\PendingState' THEN 1
        WHEN `state` = 'App\States\Order\ProcessingState' THEN 2
        WHEN `state` = 'App\States\Order\ShippedState' THEN 3
        WHEN `state` = 'App\States\Order\DeliveredState' THEN 4
        WHEN `state` = 'App\States\Order\CancelledState' THEN 100
        ELSE 999
END
ASC

Best Practices ​

1. Use Logical Workflow Numbers

Order states according to your business workflow:

php
PendingState::getSortOrder()      // 1 - Start of workflow
ProcessingState::getSortOrder()   // 2 - Next step
ShippedState::getSortOrder()      // 3 - Next step
DeliveredState::getSortOrder()    // 4 - Final state
CancelledState::getSortOrder()    // 100 - Terminal state (always last)

2. Leave Gaps Between Numbers

Use gaps (10, 20, 30) if you might add states later:

php
PendingState::getSortOrder()      // 10
ProcessingState::getSortOrder()   // 20
ShippedState::getSortOrder()      // 30
DeliveredState::getSortOrder()    // 40

3. Group Similar States

Put terminal or exceptional states at the end:

php
// Normal workflow
PendingState::getSortOrder()      // 1
ProcessingState::getSortOrder()   // 2
ShippedState::getSortOrder()      // 3
DeliveredState::getSortOrder()    // 4

// Terminal states
CancelledState::getSortOrder()    // 100
RefundedState::getSortOrder()     // 101

4. Combine with Default Sorting

Set the state column as default sort to show the most relevant orders first:

php
return $table
    ->columns([...])
    ->defaultSort('state', 'asc'); // Show pending orders first

Table Filters ​

Filter records by state using StateSelectFilter:

php
use RoBYCoNTe\FilamentFlow\Tables\Filters\StateSelectFilter;

StateSelectFilter::make('state')
    ->label('Filter by Status');

Add to your table:

php
return $table
    ->columns([...])
    ->filters([
        StateSelectFilter::make('state'),
    ]);

Table Grouping ​

StateGroup ​

Group table records by their state with automatic label generation and no extra columns:

php
use RoBYCoNTe\FilamentFlow\Tables\Grouping\StateGroup;

StateGroup::make('state')
    ->label('Order Status')
    ->collapsible();

Key Features:

  • Automatic Grouping: Automatically groups records by state attribute
  • Automatic Labels: Generates labels from state classes
  • Custom Labels: Uses HasLabel interface if implemented by the state
  • Standard Group Modifiers: All familiar Group modifiers work (e.g., label(), collapsible())
  • No Extra Columns: Does not add visible columns to maintain your table layout

Basic Usage:

php
return $table
    ->columns([
        TextColumn::make('order_number')
            ->searchable()
            ->sortable(),
        TextColumn::make('customer_name')
            ->searchable(),
        TextColumn::make('total_amount')
            ->money('EUR')
            ->sortable(),
    ])
    ->groups([
        StateGroup::make('state')
            ->label('Status')
            ->collapsible(),
    ])
    ->defaultGroup('state'); // Apply grouping by default

Note: The StateGroup component is designed to not add extra columns to your table. It uses getKeyFromRecordUsing() internally to access state data without displaying an additional column, keeping your table layout intact.

Customization Options:

php
// Change the group label
StateGroup::make('state')
    ->label('Order Status')

// Make the group collapsible
StateGroup::make('state')
    ->collapsible()

// Use a different state attribute
StateGroup::make('payment_state')
    ->stateAttribute('payment_state')
    ->label('Payment Status')

// Hide the label prefix
StateGroup::make('state')
    ->titlePrefixedWithLabel(false)

// Custom ordering
StateGroup::make('state')
    ->orderQueryUsing(fn ($query, $direction) => 
        $query->orderBy('state', $direction)
    )

Working with Other Components:

StateGroup works seamlessly with other filament-flow components:

php
->columns([
    StateSelectColumn::make('state'), // Interactive state column
    TextColumn::make('order_number'),
    // ...other columns
])
->filters([
    StateSelectFilter::make('state'), // State filter
])
->groups([
    StateGroup::make('state')         // Group by state
        ->label(__('Status'))
        ->collapsible(),
])

Multiple State Attributes:

If your model has multiple state attributes, you can use multiple groups:

php
->groups([
    StateGroup::make('order_state')
        ->stateAttribute('order_state')
        ->label('Order Status'),
    StateGroup::make('payment_state')
        ->stateAttribute('payment_state')
        ->label('Payment Status'),
])

Listing Tabs ​

Create tabs for each state on your listing page:

php
<?php
// app/Filament/Resources/OrderResource/Pages/ListOrders.php

namespace App\Filament\Resources\OrderResource\Pages;

use App\Models\Order;
use Filament\Resources\Pages\ListRecords;
use RoBYCoNTe\FilamentFlow\StateTabs;

class ListOrders extends ListRecords
{
    public function getTabs(): array
    {
        return StateTabs::make(Order::class)
            ->tenant($tenantId)         // Workflows scoped to an owner
            ->attribute('state')        // State attribute name
            ->query($scopedQuery)       // Base query for the badge counts
            ->badge()                   // Show record count badges
            ->includeAll()              // Include an "All" tab
            ->toArray();
    }
}

Options:

  • tenant(?int $tenantId) — Owner of the scoped workflow; read from the model instance when it implements getWorkflowTenantId()
  • query(Builder $query) — Base query for the badge counts (record/tenant scoped)
  • data() — The same tabs as plain arrays (state, label, color, icon, count) for hosts that render the tabs themselves (a page with a table)
  • attribute(string $attribute) — Specify the state attribute (default: first state attribute)
  • badge(bool $badge = true) — Show/hide record count badges
  • includeAll(bool $include = true) — Include an "All records" tab

Proprietary software. All rights reserved.