Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/content/2.essentials/2.customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@ public function board(Board $board): Board
}
```

## Header Toolbar

By default, filters and search render in a separate toolbar above the board. Enable `headerToolbar()` to move them inline with the page title for a more compact layout:

```php
public function board(Board $board): Board
{
return $board
->searchable(['title', 'description'])
->filters([
SelectFilter::make('priority')->options([...]),
])
->headerToolbar();
}
```

The header toolbar supports `Dropdown` and `Modal` filter layouts. For other layouts (AboveContent, BeforeContent, etc.), leave `headerToolbar` disabled (the default) and the full filter view will be used instead.

## Search and Filtering

Enable powerful search and filtering capabilities:
Expand Down
2 changes: 2 additions & 0 deletions docs/content/2.essentials/4.api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ navigation:
| `filtersFormWidth(Width)` | Filter panel width | |
| `filtersFormColumns(int)` | Columns in filter form | |
| `filtersLayout(FiltersLayout)` | Filter display layout (Dropdown, AboveContent, etc.) | |
| `headerToolbar(bool)` | Render filters/search inline with page title | |

## Board Methods

Expand Down Expand Up @@ -84,6 +85,7 @@ use Filament\Support\Enums\Width;
->filtersLayout(FiltersLayout::AboveContent) // Display filters above board
->filtersFormWidth(Width::Large) // Filter panel width
->filtersFormColumns(3) // Columns in filter form
->headerToolbar() // Inline filters/search in page header
```

## Column Configuration
Expand Down
184 changes: 184 additions & 0 deletions resources/views/filament/pages/board-header.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
@php
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Width;
use Filament\Support\Facades\FilamentView;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Filters\Indicator;
use Filament\Tables\View\TablesIconAlias;
use Filament\Tables\View\TablesRenderHook;

use function Filament\Support\generate_icon_html;

$table = $this->getTable();
$isFilterable = $table->isFilterable();
$isSearchable = $table->isSearchable();
$filterIndicators = $table->getFilterIndicators();

$filtersLayout = $table->getFiltersLayout();
$filtersTriggerAction = $table->getFiltersTriggerAction();
$filtersApplyAction = $table->getFiltersApplyAction();
$filtersForm = $this->getTableFiltersForm();
$filtersFormWidth = $table->getFiltersFormWidth();
$filtersFormMaxHeight = $table->getFiltersFormMaxHeight();
$filtersResetActionPosition = $table->getFiltersResetActionPosition();
$activeFiltersCount = $table->getActiveFiltersCount();

if (is_string($filtersFormWidth)) {
$filtersFormWidth = Width::tryFrom($filtersFormWidth) ?? $filtersFormWidth;
}

$hasFiltersDialog = $isFilterable && in_array($filtersLayout, [FiltersLayout::Dropdown, FiltersLayout::Modal]);
$isModalLayout = ($filtersLayout === FiltersLayout::Modal) || ($hasFiltersDialog && $filtersTriggerAction->isModalSlideOver());
@endphp

<div class="fi-page-header-main-ctn !gap-y-2 !pt-4 !pb-0">
{{-- Breadcrumbs --}}
@if (filled($breadcrumbs))
<x-filament::breadcrumbs :breadcrumbs="$breadcrumbs" class="mb-2" />
@endif

{{-- Title row: heading + filter + search --}}
<div class="flex items-center gap-4">
<div class="flex-1 min-w-0">
<h1 class="fi-header-heading text-2xl font-bold tracking-tight text-gray-950 dark:text-white sm:text-3xl">
{{ $heading }}
</h1>

@if (filled($subheading))
<p class="fi-header-subheading mt-1 max-w-2xl text-sm text-gray-600 dark:text-gray-400">
{{ $subheading }}
</p>
@endif
</div>

<div class="flex items-center gap-x-6 shrink-0">
@if ($isFilterable && $hasFiltersDialog)
@if ($isModalLayout)
@php
$filtersTriggerActionModalAlignment = $filtersTriggerAction->getModalAlignment();
$filtersTriggerActionIsModalAutofocused = $filtersTriggerAction->isModalAutofocused();
$filtersTriggerActionHasModalCloseButton = $filtersTriggerAction->hasModalCloseButton();
$filtersTriggerActionIsModalClosedByClickingAway = $filtersTriggerAction->isModalClosedByClickingAway();
$filtersTriggerActionIsModalClosedByEscaping = $filtersTriggerAction->isModalClosedByEscaping();
$filtersTriggerActionModalDescription = $filtersTriggerAction->getModalDescription();
$filtersTriggerActionVisibleModalFooterActions = $filtersTriggerAction->getVisibleModalFooterActions();
$filtersTriggerActionModalFooterActionsAlignment = $filtersTriggerAction->getModalFooterActionsAlignment();
$filtersTriggerActionModalHeading = $filtersTriggerAction->getCustomModalHeading() ?? __('filament-tables::table.filters.heading');
$filtersTriggerActionModalIcon = $filtersTriggerAction->getModalIcon();
$filtersTriggerActionModalIconColor = $filtersTriggerAction->getModalIconColor();
$filtersTriggerActionIsModalSlideOver = $filtersTriggerAction->isModalSlideOver();
$filtersTriggerActionIsModalFooterSticky = $filtersTriggerAction->isModalFooterSticky();
$filtersTriggerActionIsModalHeaderSticky = $filtersTriggerAction->isModalHeaderSticky();
@endphp

<x-filament::modal
:alignment="$filtersTriggerActionModalAlignment"
:autofocus="$filtersTriggerActionIsModalAutofocused"
:close-button="$filtersTriggerActionHasModalCloseButton"
:close-by-clicking-away="$filtersTriggerActionIsModalClosedByClickingAway"
:close-by-escaping="$filtersTriggerActionIsModalClosedByEscaping"
:description="$filtersTriggerActionModalDescription"
:footer-actions="$filtersTriggerActionVisibleModalFooterActions"
:footer-actions-alignment="$filtersTriggerActionModalFooterActionsAlignment"
:heading="$filtersTriggerActionModalHeading"
:icon="$filtersTriggerActionModalIcon"
:icon-color="$filtersTriggerActionModalIconColor"
:slide-over="$filtersTriggerActionIsModalSlideOver"
:sticky-footer="$filtersTriggerActionIsModalFooterSticky"
:sticky-header="$filtersTriggerActionIsModalHeaderSticky"
:width="$filtersFormWidth"
:wire:key="$this->getId() . '.board.filters'"
class="fi-ta-filters-modal"
>
<x-slot name="trigger">
{{ $filtersTriggerAction->badge($activeFiltersCount) }}
</x-slot>

{{ $filtersTriggerAction->getModalContent() }}

{{ $filtersForm }}

{{ $filtersTriggerAction->getModalContentFooter() }}
</x-filament::modal>
@else
{{-- Wrap in fi-ta-ctn context so Filament's scoped table CSS applies to filters --}}
<div class="fi-ta-ctn fi-ta-ctn-with-header" style="display: contents;">
<div class="fi-ta-header-toolbar" style="display: contents;">
<x-filament::dropdown
:max-height="$filtersFormMaxHeight"
placement="bottom-end"
shift
:flip="false"
:width="$filtersFormWidth ?? Width::ExtraSmall"
:wire:key="$this->getId() . '.board.filters'"
class="fi-ta-filters-dropdown"
>
<x-slot name="trigger">
{{ $filtersTriggerAction->badge($activeFiltersCount) }}
</x-slot>

<x-filament-tables::filters
:apply-action="$filtersApplyAction"
:form="$filtersForm"
:reset-action-position="$filtersResetActionPosition"
/>
</x-filament::dropdown>
</div>
</div>
@endif
@endif

@if ($isSearchable)
<x-filament-tables::search-field
:debounce="$table->getSearchDebounce()"
:on-blur="$table->isSearchOnBlur()"
:placeholder="$table->getSearchPlaceholder()"
/>
@endif
</div>
</div>

{{-- Active filter indicators --}}
@if ($filterIndicators)
@if (filled($filterIndicatorsView = FilamentView::renderHook(TablesRenderHook::FILTER_INDICATORS, scopes: static::class, data: ['filterIndicators' => $filterIndicators])))
{{ $filterIndicatorsView }}
@else
<div class="fi-ta-filter-indicators flex items-center gap-x-2">
<div class="flex flex-wrap items-center gap-1">
@foreach ($filterIndicators as $indicator)
<x-filament::badge :color="$indicator->getColor()">
{{ $indicator->getLabel() }}

@if ($indicator->isRemovable())
<x-slot
name="deleteButton"
:label="__('filament-tables::table.filters.actions.remove.label')"
:wire:click="$indicator->getRemoveLivewireClickHandler()"
wire:loading.attr="disabled"
wire:target="removeTableFilter"
></x-slot>
@endif
</x-filament::badge>
@endforeach
</div>

@if (collect($filterIndicators)->contains(fn (Indicator $indicator): bool => $indicator->isRemovable()))
<button
type="button"
x-tooltip="{
content: @js(__('filament-tables::table.filters.actions.remove_all.tooltip')),
theme: $store.theme,
}"
wire:click="removeTableFilters"
wire:loading.attr="disabled"
wire:target="removeTableFilters,removeTableFilter"
class="fi-icon-btn fi-size-sm"
>
{{ generate_icon_html(Heroicon::XMark, alias: TablesIconAlias::FILTERS_REMOVE_ALL_BUTTON, size: IconSize::Small) }}
</button>
@endif
</div>
@endif
@endif
</div>
2 changes: 1 addition & 1 deletion resources/views/filament/pages/board-page.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<x-filament-panels::page>
<div class="h-[calc(100vh-11rem)]">
<div class="h-[calc(100vh-11rem)] relative" style="z-index: 0;">
{{ $this->board }}
</div>
</x-filament-panels::page>
4 changes: 3 additions & 1 deletion resources/views/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class="w-full h-full flex flex-col relative"
})"
>

@include('flowforge::components.filters')
@unless($config['headerToolbar'] ?? false)
@include('flowforge::components.filters')
@endunless

<!-- Board Content -->
<div class="flex-1 overflow-hidden h-full">
Expand Down
21 changes: 19 additions & 2 deletions src/Board.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class Board extends ViewComponent

protected string $evaluationIdentifier = 'board';

protected bool $headerToolbar = false;

final public function __construct(HasBoard $livewire)
{
$this->livewire($livewire);
Expand All @@ -45,11 +47,25 @@ public static function make(HasBoard $livewire): static
return $static;
}

/**
* Move the filter/search toolbar into the page header,
* rendering it inline with the page title.
*/
public function headerToolbar(bool $condition = true): static
{
$this->headerToolbar = $condition;

return $this;
}

public function hasHeaderToolbar(): bool
{
return $this->headerToolbar;
}

protected function setUp(): void
{
parent::setUp();

// Any board-specific setup can go here
}

/**
Expand Down Expand Up @@ -87,6 +103,7 @@ public function getViewData(): array
'columnIdentifierAttribute' => $this->getColumnIdentifierAttribute(),
'cardLabel' => __('flowforge::flowforge.card_label'),
'pluralCardLabel' => __('flowforge::flowforge.plural_card_label'),
'headerToolbar' => $this->hasHeaderToolbar(),
],
];
}
Expand Down
23 changes: 23 additions & 0 deletions src/Concerns/BaseBoard.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Forms\Concerns\InteractsWithForms;
use Illuminate\Contracts\View\View;
use Relaticle\Flowforge\Board;

/**
Expand All @@ -32,4 +33,26 @@ protected function getBoardView(): string
{
return 'flowforge::filament.pages.board-page';
}

/**
* Override Filament's page header when headerToolbar is enabled.
*
* Renders the page title with the filter/search toolbar inline,
* replacing the default stacked layout.
*/
public function getHeader(): ?View
{
if (! $this->getBoard()->hasHeaderToolbar()) {
return null;
}

/** @var view-string $viewName */
$viewName = 'flowforge::filament.pages.board-header';

return view($viewName, [
'heading' => $this->getHeading(),
'subheading' => $this->getSubheading(),
'breadcrumbs' => filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : [],
]);
}
}