diff --git a/docs/content/2.essentials/2.customization.md b/docs/content/2.essentials/2.customization.md index 1c8cb5aa6..9d1f3339c 100644 --- a/docs/content/2.essentials/2.customization.md +++ b/docs/content/2.essentials/2.customization.md @@ -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: diff --git a/docs/content/2.essentials/4.api-reference.md b/docs/content/2.essentials/4.api-reference.md index 4b6fc9752..063453592 100644 --- a/docs/content/2.essentials/4.api-reference.md +++ b/docs/content/2.essentials/4.api-reference.md @@ -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 @@ -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 diff --git a/resources/views/filament/pages/board-header.blade.php b/resources/views/filament/pages/board-header.blade.php new file mode 100644 index 000000000..fb4ca9d2a --- /dev/null +++ b/resources/views/filament/pages/board-header.blade.php @@ -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 + +
+ {{-- Breadcrumbs --}} + @if (filled($breadcrumbs)) + + @endif + + {{-- Title row: heading + filter + search --}} +
+
+

+ {{ $heading }} +

+ + @if (filled($subheading)) +

+ {{ $subheading }} +

+ @endif +
+ +
+ @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 + + + + {{ $filtersTriggerAction->badge($activeFiltersCount) }} + + + {{ $filtersTriggerAction->getModalContent() }} + + {{ $filtersForm }} + + {{ $filtersTriggerAction->getModalContentFooter() }} + + @else + {{-- Wrap in fi-ta-ctn context so Filament's scoped table CSS applies to filters --}} +
+
+ + + {{ $filtersTriggerAction->badge($activeFiltersCount) }} + + + + +
+
+ @endif + @endif + + @if ($isSearchable) + + @endif +
+
+ + {{-- Active filter indicators --}} + @if ($filterIndicators) + @if (filled($filterIndicatorsView = FilamentView::renderHook(TablesRenderHook::FILTER_INDICATORS, scopes: static::class, data: ['filterIndicators' => $filterIndicators]))) + {{ $filterIndicatorsView }} + @else +
+
+ @foreach ($filterIndicators as $indicator) + + {{ $indicator->getLabel() }} + + @if ($indicator->isRemovable()) + + @endif + + @endforeach +
+ + @if (collect($filterIndicators)->contains(fn (Indicator $indicator): bool => $indicator->isRemovable())) + + @endif +
+ @endif + @endif +
diff --git a/resources/views/filament/pages/board-page.blade.php b/resources/views/filament/pages/board-page.blade.php index b35aefc35..cb6b7bf62 100644 --- a/resources/views/filament/pages/board-page.blade.php +++ b/resources/views/filament/pages/board-page.blade.php @@ -1,5 +1,5 @@ -
+
{{ $this->board }}
diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php index f48427f27..dfb447406 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -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
diff --git a/src/Board.php b/src/Board.php index 0aec3e432..564ee28a8 100644 --- a/src/Board.php +++ b/src/Board.php @@ -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); @@ -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 } /** @@ -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(), ], ]; } diff --git a/src/Concerns/BaseBoard.php b/src/Concerns/BaseBoard.php index 77fd45a9f..7bd510dc9 100644 --- a/src/Concerns/BaseBoard.php +++ b/src/Concerns/BaseBoard.php @@ -6,6 +6,7 @@ use Filament\Actions\Concerns\InteractsWithActions; use Filament\Forms\Concerns\InteractsWithForms; +use Illuminate\Contracts\View\View; use Relaticle\Flowforge\Board; /** @@ -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() : [], + ]); + } }