Created
September 2, 2025 19:55
-
-
Save vlados/43baed3424f9b181026e195af9619f66 to your computer and use it in GitHub Desktop.
filament floating toolbar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @import '../../../../vendor/filament/filament/resources/css/theme.css'; | |
| @source '../../../../app/Filament/**/*'; | |
| @source '../../../../resources/views/filament/**/*'; | |
| .fi-body { | |
| @apply bg-white; | |
| } | |
| .fi-sidebar-nav-groups { | |
| @apply gap-0; | |
| } | |
| .fi-sidebar-group-label { | |
| @apply text-xs; | |
| } | |
| .fi-sidebar-item { | |
| .fi-sidebar-item-btn { | |
| @apply p-1.5 ; | |
| } | |
| .fi-badge { | |
| display: block; | |
| padding: 2px 7px; | |
| } | |
| .fi-sidebar-item-label { | |
| @apply text-black; | |
| } | |
| .fi-icon { | |
| @apply size-4 text-black; | |
| } | |
| } | |
| .fi-page-header-main-ctn { | |
| @apply gap-y-4 pt-4; | |
| } | |
| .fi-header-heading { | |
| @apply text-black text-2xl; | |
| } | |
| .fi-ta-ctn { | |
| @apply ring-0 shadow-none; | |
| :where(& > :not(:last-child)) { | |
| @apply border-0; | |
| } | |
| } | |
| .fi-ta-content-ctn { | |
| .fi-ta-header-cell { | |
| @apply p-2.5; | |
| } | |
| @apply border rounded-lg; | |
| } | |
| .fi-ta-cell { | |
| &:last-of-type, | |
| &.fi-ta-selection-cell:first-of-type { | |
| @apply p-2; | |
| } | |
| } | |
| .fi-ta-text-item { | |
| &.fi-size-sm { | |
| @apply p-2.5; | |
| } | |
| } | |
| .fi-ta-selection-indicator { | |
| @apply shadow-xl rounded-xl fixed bottom-10 left-1/2 -translate-x-1/2 z-10 flex items-center gap-x-2 border bg-gray-900 text-white p-2 px-2; | |
| .fi-dropdown-panel { | |
| @apply !absolute; | |
| } | |
| & > :nth-child(1) { | |
| @apply text-white; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @php | |
| use Filament\Support\Enums\Alignment; | |
| use Filament\Support\Enums\VerticalAlignment; | |
| use Filament\Support\Enums\Width; | |
| use Filament\Support\Facades\FilamentView; | |
| use Filament\Tables\Actions\HeaderActionsPosition; | |
| use Filament\Tables\Columns\Column; | |
| use Filament\Tables\Columns\ColumnGroup; | |
| use Filament\Tables\Enums\FiltersLayout; | |
| use Filament\Tables\Enums\RecordActionsPosition; | |
| use Filament\Tables\Enums\RecordCheckboxPosition; | |
| use Filament\Tables\View\TablesRenderHook; | |
| use Illuminate\Support\Str; | |
| use Illuminate\View\ComponentAttributeBag; | |
| $defaultRecordActions = $getRecordActions(); | |
| $flatRecordActionsCount = count($getFlatRecordActions()); | |
| $recordActionsAlignment = $getRecordActionsAlignment(); | |
| $recordActionsPosition = $getRecordActionsPosition(); | |
| $recordActionsColumnLabel = $getRecordActionsColumnLabel(); | |
| if (! $recordActionsAlignment instanceof Alignment) { | |
| $recordActionsAlignment = filled($recordActionsAlignment) ? (Alignment::tryFrom($recordActionsAlignment) ?? $recordActionsAlignment) : null; | |
| } | |
| $activeFiltersCount = $getActiveFiltersCount(); | |
| $isSelectionDisabled = $isSelectionDisabled(); | |
| $maxSelectableRecords = $getMaxSelectableRecords(); | |
| $columns = $getVisibleColumns(); | |
| $collapsibleColumnsLayout = $getCollapsibleColumnsLayout(); | |
| $columnsLayout = $getColumnsLayout(); | |
| $content = $getContent(); | |
| $contentGrid = $getContentGrid(); | |
| $contentFooter = $getContentFooter(); | |
| $filterIndicators = $getFilterIndicators(); | |
| $filtersApplyAction = $getFiltersApplyAction(); | |
| $filtersForm = $getFiltersForm(); | |
| $filtersFormWidth = $getFiltersFormWidth(); | |
| $hasColumnGroups = $hasColumnGroups(); | |
| $hasColumnsLayout = $hasColumnsLayout(); | |
| $hasSummary = $hasSummary($this->getAllTableSummaryQuery()); | |
| $header = $getHeader(); | |
| $headerActions = array_filter( | |
| $getHeaderActions(), | |
| fn (\Filament\Actions\Action | \Filament\Actions\ActionGroup $action): bool => $action->isVisible(), | |
| ); | |
| $headerActionsPosition = $getHeaderActionsPosition(); | |
| $heading = $getHeading(); | |
| $group = $getGrouping(); | |
| $toolbarActions = array_filter( | |
| $getToolbarActions(), | |
| fn (\Filament\Actions\Action | \Filament\Actions\ActionGroup $action): bool => $action->isVisible(), | |
| ); | |
| $hasNonBulkToolbarAction = false; | |
| foreach ($toolbarActions as $toolbarAction) { | |
| if ($toolbarAction instanceof \Filament\Actions\BulkActionGroup) { | |
| continue; | |
| } | |
| if ($toolbarAction instanceof \Filament\Actions\ActionGroup) { | |
| if ($toolbarAction->hasNonBulkAction()) { | |
| $hasNonBulkToolbarAction = true; | |
| break; | |
| } | |
| continue; | |
| } | |
| if (! $toolbarAction->isBulk()) { | |
| $hasNonBulkToolbarAction = true; | |
| break; | |
| } | |
| } | |
| $groups = $getGroups(); | |
| $description = $getDescription(); | |
| $isGroupsOnly = $isGroupsOnly() && $group; | |
| $isReorderable = $isReorderable(); | |
| $isReordering = $isReordering(); | |
| $areGroupingSettingsVisible = (! $isReordering) && count($groups) && (! $areGroupingSettingsHidden()); | |
| $isGroupingDirectionSettingHidden = $isGroupingDirectionSettingHidden(); | |
| $areGroupingSettingsInDropdownOnDesktop = $areGroupingSettingsInDropdownOnDesktop(); | |
| $isColumnSearchVisible = $isSearchableByColumn(); | |
| $isGlobalSearchVisible = $isSearchable(); | |
| $isSearchOnBlur = $isSearchOnBlur(); | |
| $isSelectionEnabled = $isSelectionEnabled() && (! $isGroupsOnly); | |
| $selectsCurrentPageOnly = $selectsCurrentPageOnly(); | |
| $recordCheckboxPosition = $getRecordCheckboxPosition(); | |
| $isStriped = $isStriped(); | |
| $isLoaded = $isLoaded(); | |
| $hasFilters = $isFilterable(); | |
| $filtersLayout = $getFiltersLayout(); | |
| $filtersTriggerAction = $getFiltersTriggerAction(); | |
| $hasFiltersDialog = $hasFilters && in_array($filtersLayout, [FiltersLayout::Dropdown, FiltersLayout::Modal]); | |
| $hasFiltersAboveContent = $hasFilters && in_array($filtersLayout, [FiltersLayout::AboveContent, FiltersLayout::AboveContentCollapsible]); | |
| $hasFiltersAboveContentCollapsible = $hasFilters && ($filtersLayout === FiltersLayout::AboveContentCollapsible); | |
| $hasFiltersBelowContent = $hasFilters && ($filtersLayout === FiltersLayout::BelowContent); | |
| $hasColumnManagerDropdown = $hasColumnManager(); | |
| $hasReorderableColumns = $hasReorderableColumns(); | |
| $hasToggleableColumns = $hasToggleableColumns(); | |
| $columnManagerApplyAction = $getColumnManagerApplyAction(); | |
| $columnManagerTriggerAction = $getColumnManagerTriggerAction(); | |
| $hasHeader = $header || $heading || $description || ($headerActions && (! $isReordering)) || $isReorderable || $areGroupingSettingsVisible || $isGlobalSearchVisible || $hasFilters || count($filterIndicators) || $hasColumnManagerDropdown; | |
| $hasHeaderToolbar = $isReorderable || $areGroupingSettingsVisible || $isGlobalSearchVisible || $hasFiltersDialog || $hasColumnManagerDropdown; | |
| $headingTag = $getHeadingTag(); | |
| $secondLevelHeadingTag = $heading ? $getHeadingTag(1) : $headingTag; | |
| $pluralModelLabel = $getPluralModelLabel(); | |
| $records = $isLoaded ? $getRecords() : null; | |
| $searchDebounce = $getSearchDebounce(); | |
| $allSelectableRecordsCount = ($isSelectionEnabled && $isLoaded) ? $getAllSelectableRecordsCount() : null; | |
| $columnsCount = count($columns); | |
| $reorderRecordsTriggerAction = $getReorderRecordsTriggerAction($isReordering); | |
| $page = $this->getTablePage(); | |
| $defaultSortOptionLabel = $getDefaultSortOptionLabel(); | |
| $sortDirection = $getSortDirection(); | |
| if (count($defaultRecordActions) && (! $isReordering)) { | |
| $columnsCount++; | |
| } | |
| if ($isSelectionEnabled || $isReordering) { | |
| $columnsCount++; | |
| } | |
| if ($group) { | |
| $groupedSummarySelectedState = $this->getTableSummarySelectedState($this->getAllTableSummaryQuery(), modifyQueryUsing: fn (\Illuminate\Database\Query\Builder $query) => $group->groupQuery($query, model: $getQuery()->getModel())); | |
| } | |
| @endphp | |
| <div | |
| @if (! $isLoaded) | |
| wire:init="loadTable" | |
| @endif | |
| x-data="filamentTable({ | |
| canTrackDeselectedRecords: @js($canTrackDeselectedRecords()), | |
| currentSelectionLivewireProperty: @js($getCurrentSelectionLivewireProperty()), | |
| maxSelectableRecords: @js($maxSelectableRecords), | |
| selectsCurrentPageOnly: @js($selectsCurrentPageOnly), | |
| $wire, | |
| })" | |
| {{ | |
| $getExtraAttributeBag()->class([ | |
| 'fi-ta', | |
| 'fi-loading' => $records === null, | |
| ]) | |
| }} | |
| > | |
| <input | |
| type="hidden" | |
| value="{{ $allSelectableRecordsCount }}" | |
| x-ref="allSelectableRecordsCount" | |
| /> | |
| <div | |
| @class([ | |
| 'fi-ta-ctn', | |
| 'fi-ta-ctn-with-header' => $hasHeader, | |
| ]) | |
| > | |
| <div | |
| @if (! $hasHeader) x-cloak @endif | |
| x-show="@js($hasHeader) || @js($hasNonBulkToolbarAction) || (getSelectedRecordsCount() && @js(count($toolbarActions)))" | |
| class="fi-ta-header-ctn" | |
| > | |
| {{ FilamentView::renderHook(TablesRenderHook::HEADER_BEFORE, scopes: static::class) }} | |
| @if ($header) | |
| {{ $header }} | |
| @elseif (($heading || $description || $headerActions) && ! $isReordering) | |
| <div | |
| @class([ | |
| 'fi-ta-header', | |
| 'fi-ta-header-adaptive-actions-position' => $headerActions && ($headerActionsPosition === HeaderActionsPosition::Adaptive), | |
| ]) | |
| > | |
| @if ($heading || $description) | |
| <div> | |
| @if ($heading) | |
| <{{ $headingTag }} | |
| class="fi-ta-header-heading" | |
| > | |
| {{ $heading }} | |
| </{{ $headingTag }}> | |
| @endif | |
| @if ($description) | |
| <p class="fi-ta-header-description"> | |
| {{ $description }} | |
| </p> | |
| @endif | |
| </div> | |
| @endif | |
| @if ((! $isReordering) && $headerActions) | |
| <div class="fi-ta-actions fi-align-start fi-wrapped"> | |
| @foreach ($headerActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| </div> | |
| @endif | |
| </div> | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::HEADER_AFTER, scopes: static::class) }} | |
| @if ($hasFiltersAboveContent) | |
| <div | |
| x-data="{ areFiltersOpen: @js(! $hasFiltersAboveContentCollapsible) }" | |
| x-bind:class="{ 'fi-open': areFiltersOpen }" | |
| @class([ | |
| 'fi-ta-filters-above-content-ctn', | |
| ]) | |
| > | |
| <x-filament-tables::filters | |
| :apply-action="$filtersApplyAction" | |
| :form="$filtersForm" | |
| :heading-tag="$secondLevelHeadingTag" | |
| x-cloak | |
| x-show="areFiltersOpen" | |
| /> | |
| @if ($hasFiltersAboveContentCollapsible) | |
| <span | |
| x-on:click="areFiltersOpen = ! areFiltersOpen" | |
| class="fi-ta-filters-trigger-action-ctn" | |
| > | |
| {{ $filtersTriggerAction->badge($activeFiltersCount) }} | |
| </span> | |
| @endif | |
| </div> | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_BEFORE, scopes: static::class) }} | |
| <div | |
| @if (! $hasHeaderToolbar) x-cloak @endif | |
| x-show="@js($hasHeaderToolbar) || @js($hasNonBulkToolbarAction) || (getSelectedRecordsCount() && @js(count($toolbarActions)))" | |
| class="fi-ta-header-toolbar" | |
| > | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_START, scopes: static::class) }} | |
| @if ($isGlobalSearchVisible || $hasFiltersDialog || $hasColumnManagerDropdown) | |
| <div> | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_SEARCH_BEFORE, scopes: static::class) }} | |
| @if ($isGlobalSearchVisible) | |
| @php | |
| $searchPlaceholder = $getSearchPlaceholder(); | |
| @endphp | |
| <x-filament-tables::search-field | |
| :debounce="$searchDebounce" | |
| :on-blur="$isSearchOnBlur" | |
| :placeholder="$searchPlaceholder" | |
| /> | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_SEARCH_AFTER, scopes: static::class) }} | |
| @if ($hasFiltersDialog || $hasColumnManagerDropdown) | |
| @if ($hasFiltersDialog) | |
| @if (($filtersLayout === FiltersLayout::Modal) || $filtersTriggerAction->isModalSlideOver()) | |
| @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() . '.table.filters'" | |
| class="fi-ta-filters-modal" | |
| > | |
| <x-slot name="trigger"> | |
| {{ $filtersTriggerAction->badge($activeFiltersCount) }} | |
| </x-slot> | |
| {{ $filtersTriggerAction->getModalContent() }} | |
| {{ $filtersForm }} | |
| {{ $filtersTriggerAction->getModalContentFooter() }} | |
| </x-filament::modal> | |
| @else | |
| @php | |
| $filtersFormMaxHeight = $getFiltersFormMaxHeight(); | |
| @endphp | |
| <x-filament::dropdown | |
| :max-height="$filtersFormMaxHeight" | |
| placement="bottom-end" | |
| shift | |
| :width="$filtersFormWidth ?? Width::ExtraSmall" | |
| :wire:key="$this->getId() . '.table.filters'" | |
| class="fi-ta-filters-dropdown" | |
| > | |
| <x-slot name="trigger"> | |
| {{ $filtersTriggerAction->badge($activeFiltersCount) }} | |
| </x-slot> | |
| <x-filament-tables::filters | |
| :apply-action="$filtersApplyAction" | |
| :form="$filtersForm" | |
| :heading-tag="$secondLevelHeadingTag" | |
| /> | |
| </x-filament::dropdown> | |
| @endif | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_COLUMN_MANAGER_TRIGGER_BEFORE, scopes: static::class) }} | |
| @if ($hasColumnManagerDropdown) | |
| @php | |
| $columnManagerMaxHeight = $getColumnManagerMaxHeight(); | |
| $columnManagerWidth = $getColumnManagerWidth(); | |
| $columnManagerColumns = $getColumnManagerColumns(); | |
| @endphp | |
| <x-filament::dropdown | |
| :max-height="$columnManagerMaxHeight" | |
| placement="bottom-end" | |
| shift | |
| :width="$columnManagerWidth" | |
| :wire:key="$this->getId() . '.table.column-manager'" | |
| class="fi-ta-col-manager-dropdown" | |
| > | |
| <x-slot name="trigger"> | |
| {{ $columnManagerTriggerAction }} | |
| </x-slot> | |
| <x-filament-tables::column-manager | |
| :apply-action="$columnManagerApplyAction" | |
| :columns="$columnManagerColumns" | |
| :has-reorderable-columns="$hasReorderableColumns" | |
| :has-toggleable-columns="$hasToggleableColumns" | |
| :heading-tag="$secondLevelHeadingTag" | |
| :reorder-animation-duration="$getReorderAnimationDuration()" | |
| /> | |
| </x-filament::dropdown> | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_COLUMN_MANAGER_TRIGGER_AFTER, scopes: static::class) }} | |
| @endif | |
| </div> | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_END) }} | |
| </div> | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_AFTER) }} | |
| </div> | |
| @if ($isReordering) | |
| <div | |
| x-cloak | |
| wire:key="{{ $this->getId() }}.table.reorder.indicator" | |
| class="fi-ta-reorder-indicator" | |
| > | |
| {{ | |
| \Filament\Support\generate_loading_indicator_html(new \Illuminate\View\ComponentAttributeBag([ | |
| 'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '', | |
| 'wire:target' => 'reorderTable', | |
| ])) | |
| }} | |
| {{ __('filament-tables::table.reorder_indicator') }} | |
| </div> | |
| @elseif ($isSelectionEnabled && ($maxSelectableRecords !== 1) && $isLoaded) | |
| <div | |
| x-cloak | |
| x-bind:hidden="! getSelectedRecordsCount()" | |
| x-show="getSelectedRecordsCount()" | |
| wire:key="{{ $this->getId() }}.table.selection.indicator" | |
| class="fi-ta-selection-indicator" | |
| > | |
| @if(!$isSelectionDisabled) | |
| <x-filament::link | |
| color="danger" | |
| tag="button" | |
| x-on:click="deselectAllRecords" | |
| x-tooltip="{ | |
| content: '{{ __('filament-tables::table.selection_indicator.actions.deselect_all.label') }}', | |
| theme: $store.theme, | |
| }" | |
| class="rounded-full fi-icon-btn" | |
| > | |
| {{ \Filament\Support\generate_icon_html(\Filament\Support\Icons\Heroicon::XCircle, alias: \Filament\Tables\View\TablesIconAlias::REORDER_HANDLE) }} | |
| </x-filament::link> | |
| @else | |
| @endif | |
| <div> | |
| {{ | |
| \Filament\Support\generate_loading_indicator_html(new \Illuminate\View\ComponentAttributeBag([ | |
| 'x-show' => 'isLoading', | |
| ])) | |
| }} | |
| <span class="" | |
| x-text=" | |
| window.pluralize(@js(__('filament-tables::table.selection_indicator.selected_count')), getSelectedRecordsCount(), { | |
| count: new Intl.NumberFormat(@js(str_replace('_', '-', app()->getLocale()))).format(getSelectedRecordsCount()), | |
| }) | |
| " | |
| ></span> | |
| </div> | |
| @if (! $isSelectionDisabled) | |
| <div> | |
| {{ FilamentView::renderHook(TablesRenderHook::SELECTION_INDICATOR_ACTIONS_BEFORE, scopes: static::class) }} | |
| <div class="fi-ta-selection-indicator-actions-ctn"> | |
| <x-filament::link | |
| color="primary" | |
| tag="button" | |
| x-on:click="selectAllRecords" | |
| x-show="canSelectAllRecords()" | |
| {{-- Make sure the Alpine attributes get re-evaluated after a Livewire request: --}} | |
| :wire:key="$this->getId() . 'table.selection.indicator.actions.select-all.' . $allSelectableRecordsCount . '.' . $page" | |
| > | |
| {{ trans_choice('filament-tables::table.selection_indicator.actions.select_all.label', $allSelectableRecordsCount, ['count' => \Illuminate\Support\Number::format($allSelectableRecordsCount, locale: app()->getLocale())]) }} | |
| </x-filament::link> | |
| </div> | |
| {{ FilamentView::renderHook(TablesRenderHook::SELECTION_INDICATOR_ACTIONS_AFTER, scopes: static::class) }} | |
| </div> | |
| @endif | |
| <div class="fi-ta-actions fi-align-start fi-wrapped"> | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_REORDER_TRIGGER_BEFORE, scopes: static::class) }} | |
| @if ($isReorderable) | |
| {{ $reorderRecordsTriggerAction }} | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_REORDER_TRIGGER_AFTER, scopes: static::class) }} | |
| @if ((! $isReordering) && count($toolbarActions)) | |
| @foreach ($toolbarActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_GROUPING_SELECTOR_BEFORE, scopes: static::class) }} | |
| @if ($areGroupingSettingsVisible) | |
| <div | |
| x-data="{ | |
| grouping: $wire.$entangle('tableGrouping', true), | |
| group: null, | |
| direction: null, | |
| }" | |
| x-init=" | |
| if (grouping) { | |
| ;[group, direction] = grouping.split(':') | |
| direction ??= 'asc' | |
| } | |
| $watch('grouping', function () { | |
| if (! grouping) { | |
| group = null | |
| direction = null | |
| return | |
| } | |
| ;[group, direction] = grouping.split(':') | |
| direction ??= 'asc' | |
| }) | |
| $watch('direction', function () { | |
| grouping = group ? `${group}:${direction}` : null | |
| }) | |
| $watch('group', function (newGroup, oldGroup) { | |
| if (! newGroup) { | |
| direction = null | |
| grouping = group ? `${group}:${direction}` : null | |
| return | |
| } | |
| if (oldGroup) { | |
| grouping = group ? `${group}:${direction}` : null | |
| return | |
| } | |
| direction ??= 'asc' | |
| grouping = group ? `${group}:${direction}` : null | |
| }) | |
| " | |
| class="fi-ta-grouping-settings" | |
| > | |
| <x-filament::dropdown | |
| placement="bottom-start" | |
| shift | |
| width="xs" | |
| wire:key="{{ $this->getId() }}.table.grouping" | |
| @class([ | |
| 'sm:fi-hidden' => ! $areGroupingSettingsInDropdownOnDesktop, | |
| ]) | |
| > | |
| <x-slot name="trigger"> | |
| {{ $getGroupRecordsTriggerAction() }} | |
| </x-slot> | |
| <div class="fi-ta-grouping-settings-fields"> | |
| <label> | |
| <span> | |
| {{ __('filament-tables::table.grouping.fields.group.label') }} | |
| </span> | |
| <x-filament::input.wrapper> | |
| <x-filament::input.select | |
| x-model="group" | |
| x-on:change="resetCollapsedGroups()" | |
| > | |
| <option value="">-</option> | |
| @foreach ($groups as $groupOption) | |
| <option | |
| value="{{ $groupOption->getId() }}" | |
| > | |
| {{ $groupOption->getLabel() }} | |
| </option> | |
| @endforeach | |
| </x-filament::input.select> | |
| </x-filament::input.wrapper> | |
| </label> | |
| @if (! $isGroupingDirectionSettingHidden) | |
| <label x-cloak x-show="group"> | |
| <span> | |
| {{ __('filament-tables::table.grouping.fields.direction.label') }} | |
| </span> | |
| <x-filament::input.wrapper> | |
| <x-filament::input.select | |
| x-model="direction" | |
| > | |
| <option value="asc"> | |
| {{ __('filament-tables::table.grouping.fields.direction.options.asc') }} | |
| </option> | |
| <option value="desc"> | |
| {{ __('filament-tables::table.grouping.fields.direction.options.desc') }} | |
| </option> | |
| </x-filament::input.select> | |
| </x-filament::input.wrapper> | |
| </label> | |
| @endif | |
| </div> | |
| </x-filament::dropdown> | |
| @if (! $areGroupingSettingsInDropdownOnDesktop) | |
| <div class="fi-ta-grouping-settings-fields"> | |
| <label> | |
| <x-filament::input.wrapper | |
| :prefix="__('filament-tables::table.grouping.fields.group.label')" | |
| > | |
| <x-filament::input.select | |
| x-model="group" | |
| x-on:change="resetCollapsedGroups()" | |
| > | |
| <option value="">-</option> | |
| @foreach ($groups as $groupOption) | |
| <option | |
| value="{{ $groupOption->getId() }}" | |
| > | |
| {{ $groupOption->getLabel() }} | |
| </option> | |
| @endforeach | |
| </x-filament::input.select> | |
| </x-filament::input.wrapper> | |
| </label> | |
| @if (! $isGroupingDirectionSettingHidden) | |
| <label x-cloak x-show="group"> | |
| <span class="fi-sr-only"> | |
| {{ __('filament-tables::table.grouping.fields.direction.label') }} | |
| </span> | |
| <x-filament::input.wrapper> | |
| <x-filament::input.select | |
| x-model="direction" | |
| > | |
| <option value="asc"> | |
| {{ __('filament-tables::table.grouping.fields.direction.options.asc') }} | |
| </option> | |
| <option value="desc"> | |
| {{ __('filament-tables::table.grouping.fields.direction.options.desc') }} | |
| </option> | |
| </x-filament::input.select> | |
| </x-filament::input.wrapper> | |
| </label> | |
| @endif | |
| </div> | |
| @endif | |
| </div> | |
| @endif | |
| {{ FilamentView::renderHook(TablesRenderHook::TOOLBAR_GROUPING_SELECTOR_AFTER, scopes: static::class) }} | |
| </div> | |
| </div> | |
| @endif | |
| @if ($filterIndicators) | |
| @if (filled($filterIndicatorsView = FilamentView::renderHook(TablesRenderHook::FILTER_INDICATORS, scopes: static::class, data: ['filterIndicators' => $filterIndicators]))) | |
| {{ $filterIndicatorsView }} | |
| @else | |
| <div class="fi-ta-filter-indicators"> | |
| <div> | |
| <span class="fi-ta-filter-indicators-label"> | |
| {{ __('filament-tables::table.filters.indicator') }} | |
| </span> | |
| <div class="fi-ta-filter-indicators-badges-ctn"> | |
| @foreach ($filterIndicators as $indicator) | |
| @php | |
| $indicatorColor = $indicator->getColor(); | |
| @endphp | |
| <x-filament::badge :color="$indicatorColor"> | |
| {{ $indicator->getLabel() }} | |
| @if ($indicator->isRemovable()) | |
| @php | |
| $indicatorRemoveLivewireClickHandler = $indicator->getRemoveLivewireClickHandler(); | |
| @endphp | |
| <x-slot | |
| name="deleteButton" | |
| :label="__('filament-tables::table.filters.actions.remove.label')" | |
| :wire:click="$indicatorRemoveLivewireClickHandler" | |
| wire:loading.attr="disabled" | |
| wire:target="removeTableFilter" | |
| ></x-slot> | |
| @endif | |
| </x-filament::badge> | |
| @endforeach | |
| </div> | |
| </div> | |
| @if (collect($filterIndicators)->contains(fn (\Filament\Tables\Filters\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" | |
| > | |
| {{ \Filament\Support\generate_icon_html(\Filament\Support\Icons\Heroicon::XMark, alias: \Filament\Tables\View\TablesIconAlias::FILTERS_REMOVE_ALL_BUTTON, size: \Filament\Support\Enums\IconSize::Small) }} | |
| </button> | |
| @endif | |
| </div> | |
| @endif | |
| @endif | |
| @if (((! $content) && (! $hasColumnsLayout)) || ($records === null) || count($records)) | |
| <div | |
| @if ((! $isReordering) && ($pollingInterval = $getPollingInterval())) | |
| wire:poll.{{ $pollingInterval }} | |
| @endif | |
| class="fi-ta-content-ctn" | |
| > | |
| @if (($content || $hasColumnsLayout) && ($records !== null) && count($records)) | |
| @if (! $isReordering) | |
| @php | |
| $sortableColumns = array_filter( | |
| $columns, | |
| fn (\Filament\Tables\Columns\Column $column): bool => $column->isSortable(), | |
| ); | |
| @endphp | |
| @if ($isSelectionEnabled || count($sortableColumns)) | |
| <div class="fi-ta-content-header"> | |
| @if ($isSelectionEnabled && ($maxSelectableRecords !== 1) && (! $isReordering)) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_page.label') }}" | |
| type="checkbox" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @elseif ($maxSelectableRecords) | |
| x-bind:disabled=" | |
| const recordsOnPage = getRecordsOnPage() | |
| return recordsOnPage.length && ! areRecordsToggleable(recordsOnPage) | |
| " | |
| @endif | |
| x-bind:checked=" | |
| const recordsOnPage = getRecordsOnPage() | |
| if (recordsOnPage.length && areRecordsSelected(recordsOnPage)) { | |
| $el.checked = true | |
| return 'checked' | |
| } | |
| $el.checked = false | |
| return null | |
| " | |
| x-on:click="toggleSelectRecordsOnPage" | |
| {{-- Make sure the "checked" state gets re-evaluated after a Livewire request: --}} | |
| wire:key="{{ $this->getId() }}.table.bulk-select-page.checkbox.{{ \Illuminate\Support\Str::random() }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-page-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| @if (count($sortableColumns)) | |
| <div | |
| x-data="{ | |
| sort: $wire.$entangle('tableSort', true), | |
| column: null, | |
| direction: null, | |
| }" | |
| x-init=" | |
| if (sort) { | |
| ;[column, direction] = sort.split(':') | |
| direction ??= 'asc' | |
| } | |
| $watch('sort', function () { | |
| if (! sort) { | |
| return | |
| } | |
| ;[column, direction] = sort.split(':') | |
| direction ??= 'asc' | |
| }) | |
| $watch('direction', function () { | |
| sort = column ? `${column}:${direction}` : null | |
| }) | |
| $watch('column', function (newColumn, oldColumn) { | |
| if (! newColumn) { | |
| direction = null | |
| sort = column ? `${column}:${direction}` : null | |
| return | |
| } | |
| if (oldColumn) { | |
| sort = column ? `${column}:${direction}` : null | |
| return | |
| } | |
| direction = 'asc' | |
| sort = column ? `${column}:${direction}` : null | |
| }) | |
| " | |
| class="fi-ta-sorting-settings" | |
| > | |
| <label> | |
| <x-filament::input.wrapper | |
| :prefix="__('filament-tables::table.sorting.fields.column.label')" | |
| > | |
| <x-filament::input.select | |
| x-model="column" | |
| > | |
| <option value=""> | |
| {{ $defaultSortOptionLabel }} | |
| </option> | |
| @foreach ($sortableColumns as $column) | |
| <option | |
| value="{{ $column->getName() }}" | |
| > | |
| {{ $column->getLabel() }} | |
| </option> | |
| @endforeach | |
| </x-filament::input.select> | |
| </x-filament::input.wrapper> | |
| </label> | |
| <label x-cloak x-show="column"> | |
| <span class="fi-sr-only"> | |
| {{ __('filament-tables::table.sorting.fields.direction.label') }} | |
| </span> | |
| <x-filament::input.wrapper> | |
| <x-filament::input.select | |
| x-model="direction" | |
| > | |
| <option value="asc"> | |
| {{ __('filament-tables::table.sorting.fields.direction.options.asc') }} | |
| </option> | |
| <option value="desc"> | |
| {{ __('filament-tables::table.sorting.fields.direction.options.desc') }} | |
| </option> | |
| </x-filament::input.select> | |
| </x-filament::input.wrapper> | |
| </label> | |
| </div> | |
| @endif | |
| </div> | |
| @endif | |
| @endif | |
| @if ($content) | |
| {{ $content->with(['records' => $records]) }} | |
| @else | |
| <div | |
| @if ($isReorderable) | |
| x-on:end.stop=" | |
| $wire.reorderTable( | |
| $event.target.sortable.toArray(), | |
| $event.item.getAttribute('x-sortable-item'), | |
| ) | |
| " | |
| x-sortable | |
| data-sortable-animation-duration="{{ $getReorderAnimationDuration() }}" | |
| @endif | |
| {{ | |
| (new ComponentAttributeBag) | |
| ->when($contentGrid, fn (ComponentAttributeBag $attributes) => $attributes->grid($contentGrid)) | |
| ->class([ | |
| 'fi-ta-content', | |
| 'fi-ta-content-grid' => $contentGrid, | |
| 'fi-ta-content-grouped' => $this->getTableGrouping(), | |
| ]) | |
| }} | |
| > | |
| @php | |
| $previousRecord = null; | |
| $previousRecordGroupKey = null; | |
| $previousRecordGroupTitle = null; | |
| @endphp | |
| @foreach ($records as $record) | |
| @php | |
| $recordAction = $getRecordAction($record); | |
| $recordKey = $getRecordKey($record); | |
| $recordUrl = $getRecordUrl($record); | |
| $openRecordUrlInNewTab = $shouldOpenRecordUrlInNewTab($record); | |
| $recordGroupKey = $group?->getStringKey($record); | |
| $recordGroupTitle = $group?->getTitle($record); | |
| $isRecordGroupCollapsible = $group?->isCollapsible(); | |
| $collapsibleColumnsLayout?->record($record)->recordKey($recordKey); | |
| $hasCollapsibleColumnsLayout = (bool) $collapsibleColumnsLayout?->isVisible(); | |
| $recordActions = array_reduce( | |
| $defaultRecordActions, | |
| function (array $carry, $action) use ($record): array { | |
| $action = $action->getClone(); | |
| if (! $action instanceof \Filament\Actions\BulkAction) { | |
| $action->record($record); | |
| } | |
| if ($action->isHidden()) { | |
| return $carry; | |
| } | |
| $carry[] = $action; | |
| return $carry; | |
| }, | |
| initial: [], | |
| ); | |
| @endphp | |
| @if ($recordGroupTitle !== $previousRecordGroupTitle) | |
| @if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle)) | |
| <table | |
| @class([ | |
| 'fi-ta-table', | |
| 'fi-ta-table-reordering' => $isReordering, | |
| ]) | |
| > | |
| <tbody> | |
| @php | |
| $groupScopedAllTableSummaryQuery = $group->scopeQuery($this->getAllTableSummaryQuery(), $previousRecord); | |
| @endphp | |
| <x-filament-tables::summary.row | |
| :columns="$columns" | |
| extra-heading-column | |
| :heading=" | |
| __('filament-tables::table.summary.subheadings.group', [ | |
| 'group' => $previousRecordGroupTitle, | |
| 'label' => $pluralModelLabel, | |
| ]) | |
| " | |
| :placeholder-columns="false" | |
| :query="$groupScopedAllTableSummaryQuery" | |
| :selected-state="$groupedSummarySelectedState[$previousRecordGroupKey] ?? []" | |
| /> | |
| </tbody> | |
| </table> | |
| @endif | |
| <div | |
| @if ($isRecordGroupCollapsible = $group->isCollapsible()) | |
| x-on:click="toggleCollapseGroup(@js($recordGroupTitle))" | |
| @if (! $hasSummary) | |
| x-bind:class="{ 'fi-collapsed': isGroupCollapsed(@js($recordGroupTitle)) }" | |
| @endif | |
| @endif | |
| @class([ | |
| 'fi-ta-group-header', | |
| 'fi-collapsible' => $isRecordGroupCollapsible, | |
| ]) | |
| > | |
| @if ($isSelectionEnabled && ($maxSelectableRecords !== 1)) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_group.label', ['title' => $recordGroupTitle]) }}" | |
| type="checkbox" | |
| data-group-selectable-record-keys="{{ json_encode($this->getGroupedSelectableTableRecordKeys($recordGroupKey)) }}" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @else | |
| x-on:click="toggleSelectRecords(JSON.parse($el.dataset.groupSelectableRecordKeys))" | |
| @if ($maxSelectableRecords) | |
| x-bind:disabled=" | |
| const recordsInGroup = JSON.parse($el.dataset.groupSelectableRecordKeys) | |
| return recordsInGroup.length && ! areRecordsToggleable(recordsInGroup) | |
| " | |
| @endif | |
| @endif | |
| x-bind:checked=" | |
| const recordsInGroup = JSON.parse($el.dataset.groupSelectableRecordKeys) | |
| if (recordsInGroup.length && areRecordsSelected(recordsInGroup)) { | |
| $el.checked = true | |
| return 'checked' | |
| } | |
| $el.checked = false | |
| return null | |
| " | |
| wire:key="{{ $this->getId() }}.table.bulk_select_group.checkbox.{{ $page }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-group-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| <div> | |
| <{{ $secondLevelHeadingTag }} | |
| class | |
| ="fi-ta-group-heading" | |
| > | |
| @if (filled($recordGroupLabel = ($group->isTitlePrefixedWithLabel() ? $group->getLabel() : null))) | |
| {{ $recordGroupLabel }}: | |
| @endif | |
| {{ $recordGroupTitle }} | |
| </{{ $secondLevelHeadingTag }}> | |
| @if (filled($recordGroupDescription = $group->getDescription($record, $recordGroupTitle))) | |
| <p | |
| class="fi-ta-group-description" | |
| > | |
| {{ $recordGroupDescription }} | |
| </p> | |
| @endif | |
| </div> | |
| @if ($isRecordGroupCollapsible) | |
| <button | |
| aria-label="{{ filled($recordGroupLabel) ? ($recordGroupLabel . ': ' . $recordGroupTitle) : $recordGroupTitle }}" | |
| x-bind:aria-expanded="! isGroupCollapsed(@js($recordGroupTitle))" | |
| type="button" | |
| class="fi-icon-btn fi-size-sm" | |
| > | |
| {{ \Filament\Support\generate_icon_html(\Filament\Support\Icons\Heroicon::ChevronUp, alias: \Filament\Tables\View\TablesIconAlias::GROUPING_COLLAPSE_BUTTON, size: \Filament\Support\Enums\IconSize::Small) }} | |
| </button> | |
| @endif | |
| </div> | |
| @endif | |
| <div | |
| @if ($hasCollapsibleColumnsLayout) | |
| x-data="{ isCollapsed: @js($collapsibleColumnsLayout->isCollapsed()) }" | |
| x-init="$dispatch('collapsible-table-row-initialized')" | |
| x-on:collapse-all-table-rows.window="isCollapsed = true" | |
| x-on:expand-all-table-rows.window="isCollapsed = false" | |
| x-bind:class="isCollapsed && 'fi-ta-record-collapsed'" | |
| @endif | |
| wire:key="{{ $this->getId() }}.table.records.{{ $recordKey }}" | |
| @if ($isReordering) | |
| x-sortable-item="{{ $recordKey }}" | |
| x-sortable-handle | |
| @endif | |
| @class([ | |
| 'fi-ta-record', | |
| 'fi-clickable' => $recordUrl || $recordAction, | |
| 'fi-ta-record-with-content-prefix' => $isReordering || ($isSelectionEnabled && $isRecordSelectable($record)), | |
| 'fi-ta-record-with-content-suffix' => $hasCollapsibleColumnsLayout && (! $isReordering), | |
| ...$getRecordClasses($record), | |
| ]) | |
| x-bind:class="{ | |
| {{ $group?->isCollapsible() ? '\'fi-collapsed\': isGroupCollapsed(' . \Illuminate\Support\Js::from($recordGroupTitle) . '),' : '' }} | |
| 'fi-selected': isRecordSelected(@js($recordKey)), | |
| }" | |
| > | |
| @php | |
| $hasItemBeforeRecordContent = $isReordering || ($isSelectionEnabled && $isRecordSelectable($record)); | |
| $hasItemAfterRecordContent = $hasCollapsibleColumnsLayout && (! $isReordering); | |
| @endphp | |
| @if ($isReordering) | |
| <button | |
| class="fi-ta-reorder-handle fi-icon-btn" | |
| type="button" | |
| > | |
| {{ \Filament\Support\generate_icon_html(\Filament\Support\Icons\Heroicon::Bars2, alias: \Filament\Tables\View\TablesIconAlias::REORDER_HANDLE) }} | |
| </button> | |
| @elseif ($isSelectionEnabled && $isRecordSelectable($record)) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_record.label', ['key' => $recordKey]) }}" | |
| type="checkbox" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @elseif ($maxSelectableRecords && ($maxSelectableRecords !== 1)) | |
| x-bind:disabled="! areRecordsToggleable([@js($recordKey)])" | |
| @endif | |
| value="{{ $recordKey }}" | |
| x-on:click="toggleSelectedRecord(@js($recordKey))" | |
| x-bind:checked="isRecordSelected(@js($recordKey)) ? 'checked' : null" | |
| data-group="{{ $recordGroupKey }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-record-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| <div class="fi-ta-record-content-ctn"> | |
| <div> | |
| @if ($recordUrl) | |
| <a | |
| {{ \Filament\Support\generate_href_html($recordUrl, $openRecordUrlInNewTab) }} | |
| class="fi-ta-record-content" | |
| > | |
| @foreach ($columnsLayout as $columnsLayoutComponent) | |
| {{ | |
| $columnsLayoutComponent | |
| ->record($record) | |
| ->recordKey($recordKey) | |
| ->rowLoop($loop) | |
| ->renderInLayout() | |
| }} | |
| @endforeach | |
| </a> | |
| @elseif ($recordAction) | |
| @php | |
| $recordWireClickAction = $getRecordAction($record) | |
| ? "mountTableAction('{$recordAction}', '{$recordKey}')" | |
| : $recordWireClickAction = "{$recordAction}('{$recordKey}')"; | |
| @endphp | |
| <button | |
| type="button" | |
| wire:click="{{ $recordWireClickAction }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ $recordWireClickAction }}" | |
| class="fi-ta-record-content" | |
| > | |
| @foreach ($columnsLayout as $columnsLayoutComponent) | |
| {{ | |
| $columnsLayoutComponent | |
| ->record($record) | |
| ->recordKey($recordKey) | |
| ->rowLoop($loop) | |
| ->renderInLayout() | |
| }} | |
| @endforeach | |
| </button> | |
| @else | |
| <div | |
| class="fi-ta-record-content" | |
| > | |
| @foreach ($columnsLayout as $columnsLayoutComponent) | |
| {{ | |
| $columnsLayoutComponent | |
| ->record($record) | |
| ->recordKey($recordKey) | |
| ->rowLoop($loop) | |
| ->renderInLayout() | |
| }} | |
| @endforeach | |
| </div> | |
| @endif | |
| @if ($hasCollapsibleColumnsLayout && (! $isReordering)) | |
| <div | |
| x-collapse | |
| x-show="! isCollapsed" | |
| class="fi-ta-record-content fi-collapsible" | |
| > | |
| {{ $collapsibleColumnsLayout }} | |
| </div> | |
| @endif | |
| </div> | |
| @if ($recordActions && (! $isReordering)) | |
| <div | |
| @class([ | |
| 'fi-ta-actions fi-wrapped sm:fi-not-wrapped', | |
| match ($recordActionsAlignment ?? Alignment::Start) { | |
| Alignment::Start => 'fi-align-start', | |
| Alignment::Center => 'fi-align-center', | |
| Alignment::End => 'fi-align-end', | |
| } => $contentGrid, | |
| 'fi-align-start md:fi-align-end' => ! $contentGrid, | |
| 'fi-ta-actions-before-columns-position' => $recordActionsPosition === RecordActionsPosition::BeforeColumns, | |
| ]) | |
| > | |
| @foreach ($recordActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| </div> | |
| @endif | |
| </div> | |
| @if ($hasCollapsibleColumnsLayout && (! $isReordering)) | |
| <button | |
| type="button" | |
| x-on:click="isCollapsed = ! isCollapsed" | |
| class="fi-ta-record-collapse-btn fi-icon-btn" | |
| > | |
| {{ \Filament\Support\generate_icon_html(\Filament\Support\Icons\Heroicon::ChevronDown, alias: \Filament\Tables\View\TablesIconAlias::COLUMNS_COLLAPSE_BUTTON) }} | |
| </button> | |
| @endif | |
| </div> | |
| @php | |
| $previousRecordGroupKey = $recordGroupKey; | |
| $previousRecordGroupTitle = $recordGroupTitle; | |
| $previousRecord = $record; | |
| @endphp | |
| @endforeach | |
| @if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle) && ((! $records instanceof \Illuminate\Contracts\Pagination\Paginator) || (! $records->hasMorePages()))) | |
| <table class="fi-ta-table"> | |
| <tbody> | |
| @php | |
| $groupScopedAllTableSummaryQuery = $group->scopeQuery($this->getAllTableSummaryQuery(), $previousRecord); | |
| @endphp | |
| <x-filament-tables::summary.row | |
| :columns="$columns" | |
| extra-heading-column | |
| :heading="__('filament-tables::table.summary.subheadings.group', ['group' => $previousRecordGroupTitle, 'label' => $pluralModelLabel])" | |
| :placeholder-columns="false" | |
| :query="$groupScopedAllTableSummaryQuery" | |
| :selected-state="$groupedSummarySelectedState[$previousRecordGroupKey] ?? []" | |
| /> | |
| </tbody> | |
| </table> | |
| @endif | |
| </div> | |
| @endif | |
| @if (($content || $hasColumnsLayout) && $contentFooter) | |
| {{ | |
| $contentFooter->with([ | |
| 'columns' => $columns, | |
| 'records' => $records, | |
| ]) | |
| }} | |
| @endif | |
| @if ($hasSummary && (! $isReordering)) | |
| <table class="fi-ta-table"> | |
| <tbody> | |
| <x-filament-tables::summary | |
| :columns="$columns" | |
| extra-heading-column | |
| :placeholder-columns="false" | |
| :plural-model-label="$pluralModelLabel" | |
| :records="$records" | |
| /> | |
| </tbody> | |
| </table> | |
| @endif | |
| @elseif ((! ($content || $hasColumnsLayout)) && ($records !== null)) | |
| <table class="fi-ta-table"> | |
| <thead> | |
| @if ($hasColumnGroups) | |
| <tr class="fi-ta-table-head-groups-row"> | |
| @if (count($records)) | |
| @if ($isReordering) | |
| <th></th> | |
| @else | |
| @if (count($defaultRecordActions) && in_array($recordActionsPosition, [RecordActionsPosition::BeforeCells, RecordActionsPosition::BeforeColumns])) | |
| <th></th> | |
| @endif | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::BeforeCells) | |
| <th></th> | |
| @endif | |
| @endif | |
| @endif | |
| @foreach ($columnsLayout as $columnGroup) | |
| @if ($columnGroup instanceof Column) | |
| @if ($columnGroup->isVisible() && (! $columnGroup->isToggledHidden())) | |
| <th></th> | |
| @endif | |
| @elseif ($columnGroup instanceof ColumnGroup) | |
| @php | |
| $columnGroupColumnsCount = count($columnGroup->getVisibleColumns()); | |
| @endphp | |
| @if ($columnGroupColumnsCount) | |
| <th | |
| colspan="{{ $columnGroupColumnsCount }}" | |
| {{ | |
| $columnGroup->getExtraHeaderAttributeBag()->class([ | |
| 'fi-ta-header-group-cell', | |
| 'fi-wrapped' => $columnGroup->canHeaderWrap(), | |
| ((($columnGroupAlignment = $columnGroup->getAlignment()) instanceof \Filament\Support\Enums\Alignment) ? "fi-align-{$columnGroupAlignment->value}" : (is_string($columnGroupAlignment) ? $columnGroupAlignment : '')), | |
| (filled($columnGroupHiddenFrom = $columnGroup->getHiddenFrom()) ? "{$columnGroupHiddenFrom}:fi-hidden" : ''), | |
| (filled($columnGroupVisibleFrom = $columnGroup->getVisibleFrom()) ? "{$columnGroupVisibleFrom}:fi-visible" : ''), | |
| ]) | |
| }} | |
| > | |
| {{ $columnGroup->getLabel() }} | |
| </th> | |
| @endif | |
| @endif | |
| @endforeach | |
| @if ((! $isReordering) && count($records)) | |
| @if (count($defaultRecordActions) && in_array($recordActionsPosition, [RecordActionsPosition::AfterColumns, RecordActionsPosition::AfterCells])) | |
| <th></th> | |
| @endif | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::AfterCells) | |
| <th></th> | |
| @endif | |
| @endif | |
| </tr> | |
| @endif | |
| <tr> | |
| @if (count($records)) | |
| @if ($isReordering) | |
| <th></th> | |
| @else | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::BeforeCells) | |
| @if ($recordActionsColumnLabel) | |
| <th class="fi-ta-header-cell"> | |
| {{ $recordActionsColumnLabel }} | |
| </th> | |
| @else | |
| <th | |
| aria-label="{{ trans_choice('filament-tables::table.columns.actions.label', $flatRecordActionsCount) }}" | |
| class="fi-ta-actions-header-cell fi-ta-empty-header-cell" | |
| ></th> | |
| @endif | |
| @endif | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::BeforeCells) | |
| <th | |
| class="fi-ta-cell fi-ta-selection-cell" | |
| > | |
| @if ($maxSelectableRecords !== 1) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_page.label') }}" | |
| type="checkbox" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @elseif ($maxSelectableRecords) | |
| x-bind:disabled=" | |
| const recordsOnPage = getRecordsOnPage() | |
| return recordsOnPage.length && ! areRecordsToggleable(recordsOnPage) | |
| " | |
| @endif | |
| x-bind:checked=" | |
| const recordsOnPage = getRecordsOnPage() | |
| if (recordsOnPage.length && areRecordsSelected(recordsOnPage)) { | |
| $el.checked = true | |
| return 'checked' | |
| } | |
| $el.checked = false | |
| return null | |
| " | |
| x-on:click="toggleSelectRecordsOnPage" | |
| {{-- Make sure the "checked" state gets re-evaluated after a Livewire request: --}} | |
| wire:key="{{ $this->getId() }}.table.bulk-select-page.checkbox.{{ \Illuminate\Support\Str::random() }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-page-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| </th> | |
| @endif | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::BeforeColumns) | |
| @if ($recordActionsColumnLabel) | |
| <th class="fi-ta-header-cell"> | |
| {{ $recordActionsColumnLabel }} | |
| </th> | |
| @else | |
| <th | |
| aria-label="{{ trans_choice('filament-tables::table.columns.actions.label', $flatRecordActionsCount) }}" | |
| class="fi-ta-actions-header-cell fi-ta-empty-header-cell" | |
| ></th> | |
| @endif | |
| @endif | |
| @endif | |
| @endif | |
| @foreach ($columns as $column) | |
| @php | |
| $columnName = $column->getName(); | |
| $columnLabel = $column->getLabel(); | |
| $columnAlignment = $column->getAlignment(); | |
| $columnWidth = $column->getWidth(); | |
| $isColumnActivelySorted = $getSortColumn() === $column->getName(); | |
| $isColumnSortable = $column->isSortable() && (! $isReordering); | |
| @endphp | |
| <th | |
| @if ($isColumnActivelySorted) | |
| aria-sort="{{ $sortDirection === 'asc' ? 'ascending' : 'descending' }}" | |
| @endif | |
| {{ | |
| $column->getExtraHeaderAttributeBag() | |
| ->class([ | |
| 'fi-ta-header-cell', | |
| 'fi-ta-header-cell-' . str($columnName)->camel()->kebab(), | |
| 'fi-growable' => blank($columnWidth) && $column->canGrow(default: false), | |
| 'fi-grouped' => $column->getGroup(), | |
| 'fi-wrapped' => $column->canHeaderWrap(), | |
| 'fi-ta-header-cell-sorted' => $isColumnActivelySorted, | |
| ((($columnAlignment = $column->getAlignment()) instanceof \Filament\Support\Enums\Alignment) ? "fi-align-{$columnAlignment->value}" : (is_string($columnAlignment) ? $columnAlignment : '')), | |
| (filled($columnHiddenFrom = $column->getHiddenFrom()) ? "{$columnHiddenFrom}:fi-hidden" : ''), | |
| (filled($columnVisibleFrom = $column->getVisibleFrom()) ? "{$columnVisibleFrom}:fi-visible" : ''), | |
| ]) | |
| ->style([ | |
| ('width: ' . $columnWidth) => filled($columnWidth), | |
| ]) | |
| }} | |
| > | |
| @if ($isColumnSortable) | |
| <span | |
| aria-label="{{ trim(strip_tags($columnLabel)) }}" | |
| role="button" | |
| tabindex="0" | |
| wire:click="sortTable('{{ $columnName }}')" | |
| x-on:keydown.enter.prevent.stop="$wire.sortTable('{{ $columnName }}')" | |
| x-on:keydown.space.prevent.stop="$wire.sortTable('{{ $columnName }}')" | |
| wire:loading.attr="disabled" | |
| class="fi-ta-header-cell-sort-btn" | |
| > | |
| {{ $columnLabel }} | |
| {{ | |
| \Filament\Support\generate_icon_html(($isColumnActivelySorted && $sortDirection === 'asc') ? \Filament\Support\Icons\Heroicon::ChevronUp : \Filament\Support\Icons\Heroicon::ChevronDown, alias: match (true) { | |
| $isColumnActivelySorted && ($sortDirection === 'asc') => \Filament\Tables\View\TablesIconAlias::HEADER_CELL_SORT_ASC_BUTTON, | |
| $isColumnActivelySorted && ($sortDirection === 'desc') => \Filament\Tables\View\TablesIconAlias::HEADER_CELL_SORT_DESC_BUTTON, | |
| default => \Filament\Tables\View\TablesIconAlias::HEADER_CELL_SORT_BUTTON, | |
| }) | |
| }} | |
| </span> | |
| @else | |
| {{ $columnLabel }} | |
| @endif | |
| </th> | |
| @endforeach | |
| @if ((! $isReordering) && count($records)) | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::AfterColumns) | |
| @if ($recordActionsColumnLabel) | |
| <th | |
| class="fi-ta-header-cell fi-align-end" | |
| > | |
| {{ $recordActionsColumnLabel }} | |
| </th> | |
| @else | |
| <th | |
| aria-label="{{ trans_choice('filament-tables::table.columns.actions.label', $flatRecordActionsCount) }}" | |
| class="fi-ta-actions-header-cell fi-ta-empty-header-cell" | |
| ></th> | |
| @endif | |
| @endif | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::AfterCells) | |
| <th | |
| class="fi-ta-cell fi-ta-selection-cell" | |
| > | |
| @if ($maxSelectableRecords !== 1) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_page.label') }}" | |
| type="checkbox" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @elseif ($maxSelectableRecords) | |
| x-bind:disabled=" | |
| const recordsOnPage = getRecordsOnPage() | |
| return recordsOnPage.length && ! areRecordsToggleable(recordsOnPage) | |
| " | |
| @endif | |
| x-bind:checked=" | |
| const recordsOnPage = getRecordsOnPage() | |
| if (recordsOnPage.length && areRecordsSelected(recordsOnPage)) { | |
| $el.checked = true | |
| return 'checked' | |
| } | |
| $el.checked = false | |
| return null | |
| " | |
| x-on:click="toggleSelectRecordsOnPage" | |
| {{-- Make sure the "checked" state gets re-evaluated after a Livewire request: --}} | |
| wire:key="{{ $this->getId() }}.table.bulk-select-page.checkbox.{{ \Illuminate\Support\Str::random() }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-page-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| </th> | |
| @endif | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::AfterCells) | |
| @if ($recordActionsColumnLabel) | |
| <th | |
| class="fi-ta-header-cell fi-align-end" | |
| > | |
| {{ $recordActionsColumnLabel }} | |
| </th> | |
| @else | |
| <th | |
| aria-label="{{ trans_choice('filament-tables::table.columns.actions.label', $flatRecordActionsCount) }}" | |
| class="fi-ta-actions-header-cell fi-ta-empty-header-cell" | |
| ></th> | |
| @endif | |
| @endif | |
| @endif | |
| </tr> | |
| </thead> | |
| @if ($isColumnSearchVisible || count($records)) | |
| <tbody | |
| @if ($isReorderable) | |
| x-on:end.stop=" | |
| $wire.reorderTable( | |
| $event.target.sortable.toArray(), | |
| $event.item.getAttribute('x-sortable-item'), | |
| ) | |
| " | |
| x-sortable | |
| data-sortable-animation-duration="{{ $getReorderAnimationDuration() }}" | |
| @endif | |
| > | |
| @if ($isColumnSearchVisible) | |
| <tr | |
| class="fi-ta-row fi-ta-row-not-reorderable" | |
| > | |
| @if (count($records)) | |
| @if ($isReordering) | |
| <td></td> | |
| @else | |
| @if (count($defaultRecordActions) && in_array($recordActionsPosition, [RecordActionsPosition::BeforeCells, RecordActionsPosition::BeforeColumns])) | |
| <td></td> | |
| @endif | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::BeforeCells) | |
| <td></td> | |
| @endif | |
| @endif | |
| @endif | |
| @foreach ($columns as $column) | |
| @php | |
| $columnName = $column->getName(); | |
| @endphp | |
| <td | |
| @class([ | |
| 'fi-ta-cell', | |
| 'fi-ta-individual-search-cell' => $isIndividuallySearchable = $column->isIndividuallySearchable(), | |
| 'fi-ta-individual-search-cell-' . str($columnName)->camel()->kebab() => $isIndividuallySearchable, | |
| ]) | |
| > | |
| @if ($isIndividuallySearchable) | |
| <x-filament-tables::search-field | |
| :debounce="$searchDebounce" | |
| :on-blur="$isSearchOnBlur" | |
| :wire-model="'tableColumnSearches.' . $columnName" | |
| /> | |
| @endif | |
| </td> | |
| @endforeach | |
| @if ((! $isReordering) && count($records)) | |
| @if (count($defaultRecordActions) && in_array($recordActionsPosition, [RecordActionsPosition::AfterColumns, RecordActionsPosition::AfterCells])) | |
| <td></td> | |
| @endif | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::AfterCells) | |
| <td></td> | |
| @endif | |
| @endif | |
| </tr> | |
| @endif | |
| @if (count($records)) | |
| @php | |
| $isRecordRowStriped = false; | |
| $previousRecord = null; | |
| $previousRecordGroupKey = null; | |
| $previousRecordGroupTitle = null; | |
| @endphp | |
| @foreach ($records as $record) | |
| @php | |
| $recordAction = $getRecordAction($record); | |
| $recordKey = $getRecordKey($record); | |
| $recordUrl = $getRecordUrl($record); | |
| $openRecordUrlInNewTab = $shouldOpenRecordUrlInNewTab($record); | |
| $recordGroupKey = $group?->getStringKey($record); | |
| $recordGroupTitle = $group?->getTitle($record); | |
| $recordActions = array_reduce( | |
| $defaultRecordActions, | |
| function (array $carry, $action) use ($record): array { | |
| $action = $action->getClone(); | |
| if (! $action instanceof \Filament\Actions\BulkAction) { | |
| $action->record($record); | |
| } | |
| if ($action->isHidden()) { | |
| return $carry; | |
| } | |
| $carry[] = $action; | |
| return $carry; | |
| }, | |
| initial: [], | |
| ); | |
| @endphp | |
| @if ($recordGroupTitle !== $previousRecordGroupTitle) | |
| @if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle)) | |
| @php | |
| $groupColumn = $group->getColumn(); | |
| $groupScopedAllTableSummaryQuery = $group->scopeQuery($this->getAllTableSummaryQuery(), $previousRecord); | |
| @endphp | |
| <x-filament-tables::summary.row | |
| :actions="count($defaultRecordActions)" | |
| :actions-position="$recordActionsPosition" | |
| :columns="$columns" | |
| :group-column="$groupColumn" | |
| :groups-only="$isGroupsOnly" | |
| :heading="$isGroupsOnly ? $previousRecordGroupTitle : __('filament-tables::table.summary.subheadings.group', ['group' => $previousRecordGroupTitle, 'label' => $pluralModelLabel])" | |
| :query="$groupScopedAllTableSummaryQuery" | |
| :record-checkbox-position="$recordCheckboxPosition" | |
| :selected-state="$groupedSummarySelectedState[$previousRecordGroupKey] ?? []" | |
| :selection-enabled="$isSelectionEnabled" | |
| /> | |
| @endif | |
| @if (! $isGroupsOnly) | |
| <tr | |
| class="fi-ta-row fi-ta-group-header-row" | |
| > | |
| @php | |
| $isRecordGroupCollapsible = $group?->isCollapsible(); | |
| $groupHeaderColspan = $columnsCount; | |
| if ($isSelectionEnabled) { | |
| $groupHeaderColspan--; | |
| if ( | |
| ($recordCheckboxPosition === RecordCheckboxPosition::BeforeCells) && | |
| count($defaultRecordActions) && | |
| ($recordActionsPosition === RecordActionsPosition::BeforeCells) | |
| ) { | |
| $groupHeaderColspan--; | |
| } | |
| } | |
| @endphp | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::BeforeCells) | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::BeforeCells) | |
| <td></td> | |
| @endif | |
| <td | |
| class="fi-ta-cell fi-ta-group-selection-cell" | |
| > | |
| @if ($maxSelectableRecords !== 1) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_group.label', ['title' => $recordGroupTitle]) }}" | |
| type="checkbox" | |
| data-group-selectable-record-keys="{{ json_encode($this->getGroupedSelectableTableRecordKeys($recordGroupKey)) }}" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @else | |
| x-on:click="toggleSelectRecords(JSON.parse($el.dataset.groupSelectableRecordKeys))" | |
| @if ($maxSelectableRecords) | |
| x-bind:disabled=" | |
| const recordsInGroup = JSON.parse($el.dataset.groupSelectableRecordKeys) | |
| return recordsInGroup.length && ! areRecordsToggleable(recordsInGroup) | |
| " | |
| @endif | |
| @endif | |
| x-bind:checked=" | |
| const recordsInGroup = JSON.parse($el.dataset.groupSelectableRecordKeys) | |
| if (recordsInGroup.length && areRecordsSelected(recordsInGroup)) { | |
| $el.checked = true | |
| return 'checked' | |
| } | |
| $el.checked = false | |
| return null | |
| " | |
| wire:key="{{ $this->getId() }}.table.bulk_select_group.checkbox.{{ $page }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-group-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| </td> | |
| @endif | |
| <td | |
| colspan="{{ $groupHeaderColspan }}" | |
| class="fi-ta-group-header-cell" | |
| > | |
| <div | |
| @if ($isRecordGroupCollapsible) | |
| x-on:click="toggleCollapseGroup(@js($recordGroupTitle))" | |
| x-bind:class="isGroupCollapsed(@js($recordGroupTitle)) ? 'fi-collapsed' : null" | |
| @endif | |
| @class([ | |
| 'fi-ta-group-header', | |
| 'fi-collapsible' => $isRecordGroupCollapsible, | |
| ]) | |
| > | |
| <div> | |
| <{{ $secondLevelHeadingTag }} | |
| class | |
| ="fi-ta-group-heading" | |
| > | |
| @if (filled($recordGroupLabel = ($group->isTitlePrefixedWithLabel() ? $group->getLabel() : null))) | |
| {{ $recordGroupLabel }}: | |
| @endif | |
| {{ $recordGroupTitle }} | |
| </{{ $secondLevelHeadingTag }}> | |
| @if (filled($recordGroupDescription = $group->getDescription($record, $recordGroupTitle))) | |
| <p | |
| class="fi-ta-group-description" | |
| > | |
| {{ $recordGroupDescription }} | |
| </p> | |
| @endif | |
| </div> | |
| @if ($isRecordGroupCollapsible) | |
| <button | |
| aria-label="{{ filled($recordGroupLabel) ? ($recordGroupLabel . ': ' . $recordGroupTitle) : $recordGroupTitle }}" | |
| x-bind:aria-expanded="! isGroupCollapsed(@js($recordGroupTitle))" | |
| type="button" | |
| class="fi-icon-btn fi-size-sm" | |
| > | |
| {{ \Filament\Support\generate_icon_html(\Filament\Support\Icons\Heroicon::ChevronUp, alias: \Filament\Tables\View\TablesIconAlias::GROUPING_COLLAPSE_BUTTON, size: \Filament\Support\Enums\IconSize::Small) }} | |
| </button> | |
| @endif | |
| </div> | |
| </td> | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::AfterCells) | |
| <td | |
| class="fi-ta-cell fi-ta-group-selection-cell" | |
| > | |
| @if ($maxSelectableRecords !== 1) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_group.label', ['title' => $recordGroupTitle]) }}" | |
| type="checkbox" | |
| data-group-selectable-record-keys="{{ json_encode($this->getGroupedSelectableTableRecordKeys($recordGroupKey)) }}" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @else | |
| x-on:click="toggleSelectRecords(JSON.parse($el.dataset.groupSelectableRecordKeys))" | |
| @if ($maxSelectableRecords) | |
| x-bind:disabled=" | |
| const recordsInGroup = JSON.parse($el.dataset.groupSelectableRecordKeys) | |
| return recordsInGroup.length && ! areRecordsToggleable(recordsInGroup) | |
| " | |
| @endif | |
| @endif | |
| x-bind:checked=" | |
| const recordsInGroup = JSON.parse($el.dataset.groupSelectableRecordKeys) | |
| if (recordsInGroup.length && areRecordsSelected(recordsInGroup)) { | |
| $el.checked = true | |
| return 'checked' | |
| } | |
| $el.checked = false | |
| return null | |
| " | |
| wire:key="{{ $this->getId() }}.table.bulk_select_group.checkbox.{{ $page }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-group-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| </td> | |
| @endif | |
| </tr> | |
| @endif | |
| @php | |
| $isRecordRowStriped = false; | |
| @endphp | |
| @endif | |
| @if (! $isGroupsOnly) | |
| <tr | |
| wire:key="{{ $this->getId() }}.table.records.{{ $recordKey }}" | |
| {{ $isReordering ? 'x-sortable-handle' : null }} | |
| {!! $isReordering ? 'x-sortable-item="' . e($recordKey) . '"' : null !!} | |
| x-bind:class="{ | |
| {{ $group?->isCollapsible() ? '\'fi-collapsed\': isGroupCollapsed(' . \Illuminate\Support\Js::from($recordGroupTitle) . '),' : '' }} | |
| 'fi-selected': isRecordSelected(@js($recordKey)), | |
| }" | |
| @class([ | |
| 'fi-ta-row', | |
| 'fi-clickable' => $recordAction || $recordUrl, | |
| 'fi-striped' => $isStriped && $isRecordRowStriped, | |
| ...$getRecordClasses($record), | |
| ]) | |
| > | |
| @if ($isReordering) | |
| <td class="fi-ta-cell"> | |
| <button | |
| class="fi-ta-reorder-handle fi-icon-btn" | |
| type="button" | |
| > | |
| {{ \Filament\Support\generate_icon_html(\Filament\Support\Icons\Heroicon::Bars2, alias: \Filament\Tables\View\TablesIconAlias::REORDER_HANDLE) }} | |
| </button> | |
| </td> | |
| @endif | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::BeforeCells && (! $isReordering)) | |
| <td class="fi-ta-cell"> | |
| <div | |
| @class([ | |
| 'fi-ta-actions', | |
| match ($recordActionsAlignment) { | |
| Alignment::Center => 'fi-align-center', | |
| Alignment::Start, Alignment::Left => 'fi-align-start', | |
| Alignment::Between, Alignment::Justify => 'fi-align-between', | |
| Alignment::End, Alignment::Right => '', | |
| default => is_string($recordActionsAlignment) ? $recordActionsAlignment : '', | |
| }, | |
| ]) | |
| > | |
| @foreach ($recordActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| </div> | |
| </td> | |
| @endif | |
| @if ($isSelectionEnabled && ($recordCheckboxPosition === RecordCheckboxPosition::BeforeCells) && (! $isReordering)) | |
| <td | |
| class="fi-ta-cell fi-ta-selection-cell" | |
| > | |
| @if ($isRecordSelectable($record)) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_record.label', ['key' => $recordKey]) }}" | |
| type="checkbox" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @elseif ($maxSelectableRecords && ($maxSelectableRecords !== 1)) | |
| x-bind:disabled="! areRecordsToggleable([@js($recordKey)])" | |
| @endif | |
| value="{{ $recordKey }}" | |
| x-on:click="toggleSelectedRecord(@js($recordKey))" | |
| x-bind:checked="isRecordSelected(@js($recordKey)) ? 'checked' : null" | |
| data-group="{{ $recordGroupKey }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-record-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| </td> | |
| @endif | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::BeforeColumns && (! $isReordering)) | |
| <td class="fi-ta-cell"> | |
| <div | |
| @class([ | |
| 'fi-ta-actions', | |
| match ($recordActionsAlignment) { | |
| Alignment::Center => 'fi-align-center', | |
| Alignment::Start, Alignment::Left => 'fi-align-start', | |
| Alignment::Between, Alignment::Justify => 'fi-align-between', | |
| Alignment::End, Alignment::Right => '', | |
| default => is_string($recordActionsAlignment) ? $recordActionsAlignment : '', | |
| }, | |
| ]) | |
| > | |
| @foreach ($recordActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| </div> | |
| </td> | |
| @endif | |
| @foreach ($columns as $column) | |
| @php | |
| $column->record($record); | |
| $column->rowLoop($loop->parent); | |
| $column->recordKey($recordKey); | |
| $columnAction = $column->getAction(); | |
| $columnUrl = $column->getUrl(); | |
| $columnHasStateBasedUrls = $column->hasStateBasedUrls(); | |
| $isColumnClickDisabled = $column->isClickDisabled() || $isReordering; | |
| $columnWrapperTag = match (true) { | |
| ($columnUrl || ($recordUrl && $columnAction === null)) && (! $columnHasStateBasedUrls) && (! $isColumnClickDisabled) => 'a', | |
| ($columnAction || $recordAction) && (! $columnHasStateBasedUrls) && (! $isColumnClickDisabled) => 'button', | |
| default => 'div', | |
| }; | |
| if ($columnWrapperTag === 'button') { | |
| if ($columnAction instanceof \Filament\Actions\Action) { | |
| $columnWireClickAction = "mountTableAction('{$columnAction->getName()}', '{$recordKey}')"; | |
| } elseif ($columnAction) { | |
| $columnWireClickAction = "callTableColumnAction('{$column->getName()}', '{$recordKey}')"; | |
| } else { | |
| if ($this->getTable()->getAction($recordAction)) { | |
| $columnWireClickAction = "mountTableAction('{$recordAction}', '{$recordKey}')"; | |
| } else { | |
| $columnWireClickAction = "{$recordAction}('{$recordKey}')"; | |
| } | |
| } | |
| } | |
| @endphp | |
| <td | |
| wire:key="{{ $this->getId() }}.table.record.{{ $recordKey }}.column.{{ $column->getName() }}" | |
| {{ | |
| $column->getExtraCellAttributeBag()->class([ | |
| 'fi-ta-cell', | |
| 'fi-ta-cell-' . str($column->getName())->camel()->kebab(), | |
| ((($columnAlignment = $column->getAlignment()) instanceof \Filament\Support\Enums\Alignment) ? "fi-align-{$columnAlignment->value}" : (is_string($columnAlignment) ? $columnAlignment : '')), | |
| ((($columnVerticalAlignment = $column->getVerticalAlignment()) instanceof \Filament\Support\Enums\VerticalAlignment) ? "fi-vertical-align-{$columnVerticalAlignment->value}" : (is_string($columnVerticalAlignment) ? $columnVerticalAlignment : '')), | |
| (filled($columnHiddenFrom = $column->getHiddenFrom()) ? "{$columnHiddenFrom}:fi-hidden" : ''), | |
| (filled($columnVisibleFrom = $column->getVisibleFrom()) ? "{$columnVisibleFrom}:fi-visible" : ''), | |
| ]) | |
| }} | |
| > | |
| <{{ $columnWrapperTag }} | |
| @if ($columnWrapperTag === 'a') | |
| {{ \Filament\Support\generate_href_html($columnUrl ?: $recordUrl, $columnUrl ? $column->shouldOpenUrlInNewTab() : $openRecordUrlInNewTab) }} | |
| @elseif ($columnWrapperTag === 'button') | |
| type | |
| ="button" | |
| wire:click.prevent.stop="{{ $columnWireClickAction }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ $columnWireClickAction }}" | |
| @endif | |
| @class([ | |
| 'fi-ta-col', | |
| 'fi-ta-col-has-column-url' => ($columnWrapperTag === 'a') && filled($columnUrl), | |
| ]) | |
| > | |
| {{ $column }} | |
| </{{ $columnWrapperTag }}> | |
| </td> | |
| @endforeach | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::AfterColumns && (! $isReordering)) | |
| <td class="fi-ta-cell"> | |
| <div | |
| @class([ | |
| 'fi-ta-actions', | |
| match ($recordActionsAlignment) { | |
| Alignment::Center => 'fi-align-center', | |
| Alignment::Start, Alignment::Left => 'fi-align-start', | |
| Alignment::Between, Alignment::Justify => 'fi-align-between', | |
| Alignment::End, Alignment::Right => '', | |
| default => is_string($recordActionsAlignment) ? $recordActionsAlignment : '', | |
| }, | |
| ]) | |
| > | |
| @foreach ($recordActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| </div> | |
| </td> | |
| @endif | |
| @if ($isSelectionEnabled && $recordCheckboxPosition === RecordCheckboxPosition::AfterCells && (! $isReordering)) | |
| <td | |
| class="fi-ta-cell fi-ta-selection-cell" | |
| > | |
| @if ($isRecordSelectable($record)) | |
| <input | |
| aria-label="{{ __('filament-tables::table.fields.bulk_select_record.label', ['key' => $recordKey]) }}" | |
| type="checkbox" | |
| @if ($isSelectionDisabled) | |
| disabled | |
| @elseif ($maxSelectableRecords && ($maxSelectableRecords !== 1)) | |
| x-bind:disabled="! areRecordsToggleable([@js($recordKey)])" | |
| @endif | |
| value="{{ $recordKey }}" | |
| x-on:click="toggleSelectedRecord(@js($recordKey))" | |
| x-bind:checked="isRecordSelected(@js($recordKey)) ? 'checked' : null" | |
| data-group="{{ $recordGroupKey }}" | |
| wire:loading.attr="disabled" | |
| wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}" | |
| class="fi-ta-record-checkbox fi-checkbox-input" | |
| /> | |
| @endif | |
| </td> | |
| @endif | |
| @if (count($defaultRecordActions) && $recordActionsPosition === RecordActionsPosition::AfterCells && (! $isReordering)) | |
| <td class="fi-ta-cell"> | |
| <div | |
| @class([ | |
| 'fi-ta-actions', | |
| match ($recordActionsAlignment) { | |
| Alignment::Center => 'fi-align-center', | |
| Alignment::Start, Alignment::Left => 'fi-align-start', | |
| Alignment::Between, Alignment::Justify => 'fi-align-between', | |
| Alignment::End, Alignment::Right => '', | |
| default => is_string($recordActionsAlignment) ? $recordActionsAlignment : '', | |
| }, | |
| ]) | |
| > | |
| @foreach ($recordActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| </div> | |
| </td> | |
| @endif | |
| </tr> | |
| @endif | |
| @php | |
| $isRecordRowStriped = ! $isRecordRowStriped; | |
| $previousRecord = $record; | |
| $previousRecordGroupKey = $recordGroupKey; | |
| $previousRecordGroupTitle = $recordGroupTitle; | |
| @endphp | |
| @endforeach | |
| @if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle) && ((! $records instanceof \Illuminate\Contracts\Pagination\Paginator) || (! $records->hasMorePages()))) | |
| @php | |
| $groupColumn = $group->getColumn(); | |
| $groupScopedAllTableSummaryQuery = $group->scopeQuery($this->getAllTableSummaryQuery(), $previousRecord); | |
| @endphp | |
| <x-filament-tables::summary.row | |
| :actions="count($defaultRecordActions)" | |
| :actions-position="$recordActionsPosition" | |
| :columns="$columns" | |
| :group-column="$groupColumn" | |
| :groups-only="$isGroupsOnly" | |
| :heading="$isGroupsOnly ? $previousRecordGroupTitle : __('filament-tables::table.summary.subheadings.group', ['group' => $previousRecordGroupTitle, 'label' => $pluralModelLabel])" | |
| :query="$groupScopedAllTableSummaryQuery" | |
| :record-checkbox-position="$recordCheckboxPosition" | |
| :selected-state="$groupedSummarySelectedState[$previousRecordGroupKey] ?? []" | |
| :selection-enabled="$isSelectionEnabled" | |
| /> | |
| @endif | |
| @if ($hasSummary && (! $isReordering)) | |
| @php | |
| $groupColumn = $group?->getColumn(); | |
| @endphp | |
| <x-filament-tables::summary | |
| :actions="count($defaultRecordActions)" | |
| :actions-position="$recordActionsPosition" | |
| :columns="$columns" | |
| :group-column="$groupColumn" | |
| :groups-only="$isGroupsOnly" | |
| :plural-model-label="$pluralModelLabel" | |
| :record-checkbox-position="$recordCheckboxPosition" | |
| :records="$records" | |
| :selection-enabled="$isSelectionEnabled" | |
| /> | |
| @endif | |
| @endif | |
| </tbody> | |
| @endif | |
| @if (($records !== null) && count($records) && $contentFooter) | |
| <tfoot> | |
| <tr> | |
| {{ | |
| $contentFooter->with([ | |
| 'columns' => $columns, | |
| 'records' => $records, | |
| ]) | |
| }} | |
| </tr> | |
| </tfoot> | |
| @endif | |
| </table> | |
| @elseif ($records === null) | |
| <div class="fi-ta-table-loading-ctn"> | |
| {{ \Filament\Support\generate_loading_indicator_html(size: \Filament\Support\Enums\IconSize::TwoExtraLarge) }} | |
| </div> | |
| @endif | |
| </div> | |
| @endif | |
| @if (($records !== null) && ! count($records)) | |
| @if ($emptyState = $getEmptyState()) | |
| {{ $emptyState }} | |
| @else | |
| <div class="fi-ta-empty-state"> | |
| <div class="fi-ta-empty-state-content"> | |
| <div class="fi-ta-empty-state-icon-bg"> | |
| {{ \Filament\Support\generate_icon_html($getEmptyStateIcon(), size: \Filament\Support\Enums\IconSize::Large) }} | |
| </div> | |
| <{{ $secondLevelHeadingTag }} | |
| class | |
| ="fi-ta-empty-state-heading" | |
| > | |
| {{ $getEmptyStateHeading() }} | |
| </{{ $secondLevelHeadingTag }}> | |
| @if (filled($emptyStateDescription = $getEmptyStateDescription())) | |
| <p class="fi-ta-empty-state-description"> | |
| {{ $emptyStateDescription }} | |
| </p> | |
| @endif | |
| @if ($emptyStateActions = array_filter( | |
| $getEmptyStateActions(), | |
| fn (\Filament\Actions\Action | \Filament\Actions\ActionGroup $action): bool => $action->isVisible(), | |
| )) | |
| <div | |
| class="fi-ta-actions fi-align-center fi-wrapped" | |
| > | |
| @foreach ($emptyStateActions as $action) | |
| {{ $action }} | |
| @endforeach | |
| </div> | |
| @endif | |
| </div> | |
| </div> | |
| @endif | |
| @endif | |
| @if ((($records instanceof \Illuminate\Contracts\Pagination\Paginator) || ($records instanceof \Illuminate\Contracts\Pagination\CursorPaginator)) && | |
| ((! ($records instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator)) || $records->total())) | |
| @php | |
| $hasExtremePaginationLinks = $hasExtremePaginationLinks(); | |
| $paginationPageOptions = $getPaginationPageOptions(); | |
| @endphp | |
| <x-filament::pagination | |
| :extreme-links="$hasExtremePaginationLinks" | |
| :page-options="$paginationPageOptions" | |
| :paginator="$records" | |
| /> | |
| @endif | |
| @if ($hasFiltersBelowContent) | |
| <x-filament-tables::filters | |
| :apply-action="$filtersApplyAction" | |
| :form="$filtersForm" | |
| :heading-tag="$secondLevelHeadingTag" | |
| class="fi-ta-filters-below-content" | |
| /> | |
| @endif | |
| </div> | |
| <x-filament-actions::modals /> | |
| </div> |
Author
vlados
commented
Sep 2, 2025
result so far
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment