feat(ui): standardize paginated tables and responsive dropdowns

Add reusable pagination and page-size controls, improve mobile dropdown positioning, and migrate paginated views to consistent table components.
This commit is contained in:
Andras Bacsai committed 2026-08-16 00:58:23 +02:00
1 parent 7bcdc23086
commit 0a35909ddf
32 files changed
+610 -624

No files matched your search

+81 -10
View File
@@ -2,6 +2,47 @@
use Illuminate\Support\Facades\Blade;
it('renders the shared page size selector', function () {
$html = Blade::render('<x-page-size-select model="perPage" livewire storage-key="tests.page-size" />');
expect($html)
->toContain('aria-label="Items per page"')
->toContain("\$wire.set('perPage'")
->toContain('h-7!')
->toContain('w-12!')
->toContain('opacity-0')
->toContain('selectedPageSize')
->toContain('border-0')
->toContain('text-[11px]!')
->toContain('mb-0!')
->toContain("localStorage.getItem('tests.page-size')")
->toContain("localStorage.setItem('tests.page-size'")
->toContain('value="10"')
->toContain('value="25"')
->toContain('value="50"')
->toContain('value="100"');
expect($html)
->toContain('value="custom"')
->toContain('data-custom-page-size')
->toContain('customOption?.remove()')
->not->toContain('<option value="" disabled selected>')
->toContain('min="1"')
->toContain('max="100"');
});
it('renders compact client-side pagination', function () {
$html = Blade::render('<x-client-pagination summary="1-10 of 20" page-size-model="pageSize" storage-key="tests.page-size" />');
expect($html)
->toContain('1-10 of 20')
->toContain('pageSize = pageSizeValue')
->toContain("localStorage.getItem('tests.page-size')")
->toContain('aria-label="Previous page"')
->toContain('aria-label="Next page"')
->not->toContain('aria-label="First page"')
->not->toContain('aria-label="Last page"');
});
it('renders loading indicators for livewire page navigation', function () {
$html = Blade::render(
'<x-table-pagination
@@ -19,24 +60,21 @@ it('renders loading indicators for livewire page navigation', function () {
);
expect($html)
->toContain('Showing')
->toContain('1–10')
->toContain('13')
->toContain('wire:target="goToPage,previousPage,nextPage"')
->toContain('wire:loading')
->toContain('wire:loading.remove')
->toContain('wire:loading.attr="disabled"')
->toContain('wire:loading.inline-flex')
->toContain('animate-spin')
->toContain('Loading page…')
->toContain('wire:click="goToPage(1)"')
->toContain('wire:click="previousPage"')
->toContain('wire:click="nextPage"')
->toContain('wire:click="goToPage(2)"')
->toContain('aria-label="First page"')
->toContain('aria-label="Next page"');
->toContain('aria-label="Next page"')
->not->toContain('aria-label="First page"')
->not->toContain('aria-label="Last page"');
// Single spinner only (page number slot), not duplicated next to the "Showing" label.
// A single spinner appears next to the range while Livewire loads the next page.
expect(substr_count($html, 'animate-spin'))->toBe(1)
->and(substr_count($html, 'Loading page…'))->toBe(1);
});
@@ -53,7 +91,6 @@ it('omits livewire loading markup when no wire target is provided', function ()
);
expect($html)
->toContain('Showing')
->toContain('1–5')
->not->toContain('wire:loading')
->not->toContain('wire:target')
@@ -65,9 +102,10 @@ it('uses the shared pagination loading component on deployment history', functio
expect($view)
->toContain('<x-table-pagination')
->toContain('<x-page-size-select')
->toContain('wire-target="goToPage,previousPage,nextPage"')
->toContain('wire:loading.class="opacity-50 pointer-events-none"')
->toContain('wire:target="goToPage,previousPage,nextPage"');
->toContain('wire:target="goToPage,previousPage,nextPage,toggleDeploymentFilter,clearFilter,setPullRequestFilter"');
});
it('uses the shared pagination loading component on environment variables', function () {
@@ -75,8 +113,9 @@ it('uses the shared pagination loading component on environment variables', func
expect($view)
->toContain('<x-table-pagination')
->toContain('<x-page-size-select')
->toContain('wire-target="setEnvironmentVariablePage,previousEnvironmentVariablePage,nextEnvironmentVariablePage"')
->toContain('wire:loading.class="opacity-50 pointer-events-none"');
->toContain('wire:loading.class="pointer-events-none opacity-40 blur-[2px]"');
});
it('uses the shared pagination loading component on team admin view', function () {
@@ -84,5 +123,37 @@ it('uses the shared pagination loading component on team admin view', function (
expect($view)
->toContain('<x-table-pagination')
->toContain('<x-page-size-select')
->toContain('wire-target="setPage,previousPage,nextPage"');
});
it('offers page size selection on client-side paginated tables', function (string $view) {
$contents = file_get_contents(resource_path("views/{$view}"));
expect($contents)
->toContain('storage-key="coolify.page-size.')
->toMatch('/<x-(page-size-select|client-pagination)/');
})->with([
'team members' => 'livewire/team/member/index.blade.php',
'api tokens' => 'livewire/security/api-tokens.blade.php',
'scheduled executions' => 'livewire/settings/scheduled-jobs.blade.php',
'volume backup executions' => 'livewire/project/shared/storages/volume-backups/executions.blade.php',
'environment resources' => 'livewire/project/resource/index.blade.php',
'projects' => 'livewire/project/index.blade.php',
'project environments' => 'livewire/project/show.blade.php',
'tagged resources' => 'livewire/tags/show.blade.php',
]);
it('uses compact client pagination on collection views', function (string $view) {
$contents = file_get_contents(resource_path("views/{$view}"));
expect($contents)->toContain('<x-client-pagination');
})->with([
'team members' => 'livewire/team/member/index.blade.php',
'api tokens' => 'livewire/security/api-tokens.blade.php',
'scheduled jobs' => 'livewire/settings/scheduled-jobs.blade.php',
'environment resources' => 'livewire/project/resource/index.blade.php',
'projects' => 'livewire/project/index.blade.php',
'project environments' => 'livewire/project/show.blade.php',
'tags' => 'livewire/tags/show.blade.php',
]);