feat(tables): show loading state while a server-side sort runs

This commit is contained in:
Andras Bacsai
2026-10-29 11:05:39 +01:00
parent 103af830e5
commit c6f5899949
7 changed files with 23 additions and 11 deletions
@@ -5,7 +5,8 @@
Alpine tables set `model` (the sort variable in the enclosing x-data scope) and an
optional `then` expression that runs after the change (for example `page = 1`).
Livewire tables set `action` (a component method that takes the sort value) and
`current` (the current server-side sort value).
`current` (the current server-side sort value). While that request runs, the
clicked header shows a spinner.
@param string $label
@param string $asc sort value for ascending order
@@ -37,15 +38,22 @@
default => null,
};
$nextValue = $current === $firstValue ? $otherValue : $firstValue;
$call = $action.'('.\Illuminate\Support\Js::from($nextValue).')';
@endphp
<button type="button" wire:click="{{ $action }}(@js($nextValue))"
<button type="button" wire:click="{{ $call }}" wire:loading.attr="aria-busy" wire:target="{{ $call }}"
aria-label="{{ $ariaBase }}{{ $direction ? ', sorted '.($direction === 'asc' ? 'ascending' : 'descending') : '' }}"
{{ $attributes->class(['table-sort-header group', 'is-sorted' => $direction]) }}>
<span class="truncate">{{ $label }}</span>
<svg @class(['table-sort-header-icon', 'rotate-180' => $direction === 'desc', 'is-visible' => $direction]) viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"
wire:loading.remove wire:target="{{ $call }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 15l6-6 6 6" />
</svg>
<svg class="size-3 shrink-0 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true"
wire:loading wire:target="{{ $call }}">
<circle class="opacity-20" cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
</svg>
</button>
@else
<button type="button"
+4 -2
View File
@@ -160,7 +160,8 @@
@endforeach
</div>
<div x-show="viewMode === 'table'" class="{{ $tableShell }}">
<div x-show="viewMode === 'table'" class="{{ $tableShell }} transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
wire:target="setProjectSort">
<div class="grid grid-cols-[minmax(0,1fr)_7rem_4rem] gap-4 @2xl:min-w-[560px] @2xl:grid-cols-[minmax(0,1.2fr)_7rem_minmax(0,1.5fr)_4rem] {{ $tableHead }}">
<x-table.sort-header label="Project" asc="name-asc" desc="name-desc"
action="setProjectSort" :current="$projectSort" />
@@ -344,7 +345,8 @@
? 'grid-cols-[minmax(0,1fr)_9.5rem] @xl:grid-cols-[minmax(0,1fr)_9rem_9.5rem] @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_9rem_9.5rem]'
: 'grid-cols-[minmax(0,1fr)_9.5rem] @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_9.5rem]';
@endphp
<div x-show="viewMode === 'table'" class="{{ $tableShell }}">
<div x-show="viewMode === 'table'" class="{{ $tableShell }} transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
wire:target="setServerSort">
<div class="grid {{ $serverTableColumns }} gap-4 @2xl:min-w-[480px] {{ $tableHead }}">
<x-table.sort-header label="Server" asc="name-asc" desc="name-desc"
action="setServerSort" :current="$serverSort" />
@@ -183,7 +183,7 @@
@if ($deployments->isNotEmpty())
<div class="data-table relative w-full transition-opacity"
wire:loading.class="opacity-50 pointer-events-none"
wire:target="goToPage,previousPage,nextPage,toggleDeploymentFilter,clearFilter,setPullRequestFilter">
wire:target="goToPage,previousPage,nextPage,toggleDeploymentFilter,clearFilter,setPullRequestFilter,setDeploymentSort">
<x-table.loading id="deployment-table-filter-loading"
target="toggleDeploymentFilter,clearFilter,setPullRequestFilter"
text="Filtering deployments..." class="rounded-lg" />
@@ -48,7 +48,7 @@
: 'Images pulled or built on this server will appear here.'" icon-name="layers" />
</div>
@else
<div class="data-table">
<div class="data-table transition-opacity" wire:loading.class="opacity-50 pointer-events-none" wire:target="setSort">
<div class="data-table-header server-images-table-grid">
<x-table.sort-header label="Repository" asc="repository_asc" desc="repository_desc"
action="setSort" :current="$sort" />
@@ -54,7 +54,7 @@
wire:loading.attr="inert" wire:target="search">
@if ($activeTab === 'managed')
@if ($resources->total() > 0)
<div class="data-table">
<div class="data-table transition-opacity" wire:loading.class="opacity-50 pointer-events-none" wire:target="setSort">
<div class="data-table-header server-resources-managed-table-grid">
<x-table.sort-header label="Name" asc="name_asc" desc="name_desc" action="setSort" :current="$sort" />
<x-table.sort-header label="Project" asc="project_asc" desc="project_desc" action="setSort" :current="$sort" />
@@ -107,7 +107,7 @@
@endif
@else
@if ($resources->total() > 0)
<div class="data-table">
<div class="data-table transition-opacity" wire:loading.class="opacity-50 pointer-events-none" wire:target="setSort">
<div class="data-table-header server-resources-unmanaged-table-grid">
<x-table.sort-header label="Name" asc="name_asc" desc="name_desc" action="setSort" :current="$sort" />
<x-table.sort-header label="Image" asc="image_asc" desc="image_desc" action="setSort" :current="$sort" />
+2 -1
View File
@@ -418,7 +418,8 @@
@endif
</x-slot:actions>
<div wire:poll="getDeployments" class="overflow-x-auto">
<div wire:poll="getDeployments" class="overflow-x-auto transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
wire:target="setDeploymentSort">
@if (count($deploymentsPerTagPerServer ?? []) === 0)
<div class="p-3">
<x-empty title="No active deployments"
@@ -4,7 +4,8 @@
<x-application.settings-section title="Pending invitations"
description="Invitation links that have not been accepted yet." flush>
<div class="overflow-x-auto">
<div class="data-table min-w-[760px]">
<div class="data-table min-w-[760px] transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
wire:target="setSort">
<div
class="data-table-header grid-cols-[minmax(13rem,1.2fr)_7rem_7rem_minmax(15rem,1.5fr)_6rem]">
<x-table.sort-header label="Email" asc="email-asc" desc="email-desc" action="setSort"