mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-19 08:15:02 +00:00
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:
1 parent
7bcdc23086
commit
0a35909ddf
32 files changed
+610
-624
No files matched your search
@@ -39,6 +39,29 @@ test('listbox component uses shared trigger label truncation', function () {
|
||||
->not->toContain('class="truncate" x-text="current"');
|
||||
});
|
||||
|
||||
test('portaled listboxes center in the mobile viewport', function () {
|
||||
$html = Blade::render('<x-forms.listbox id="region" :options="[]" :wire="false" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('window.innerWidth < 768')
|
||||
->toContain('(window.innerWidth - panelWidth) / 2')
|
||||
->toContain('panel.style.width = `${panelWidth}px`')
|
||||
->toContain('panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`')
|
||||
->toContain('x-show="open"')
|
||||
->not->toContain('x-show="open && positioned"');
|
||||
});
|
||||
|
||||
test('legacy floating panels have a centered mobile fallback', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($css)
|
||||
->toContain('@media (max-width: 767px)')
|
||||
->toContain('.listbox-panel:not([style*="position: fixed"])')
|
||||
->toContain('position: fixed !important;')
|
||||
->toContain('left: 50% !important;')
|
||||
->toContain('transform: translate(-50%, -50%) !important;');
|
||||
});
|
||||
|
||||
test('searchable listbox component uses shared trigger label truncation', function () {
|
||||
$html = Blade::render(<<<'BLADE'
|
||||
<x-forms.searchable-listbox id="tz" label="Timezone"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
it('renders the standard table toolbar controls', function () {
|
||||
$html = Blade::render(<<<'BLADE'
|
||||
@@ -21,6 +22,16 @@ it('renders the standard table toolbar controls', function () {
|
||||
->toContain('table-toolbar')
|
||||
->toContain('table-search')
|
||||
->toContain('wire:model.live="search"')
|
||||
->toContain('x-teleport="body"')
|
||||
->toContain('positionPanel')
|
||||
->toContain('position: fixed')
|
||||
->toContain('floating-dropdown-panel')
|
||||
->toContain('window.innerWidth < 768')
|
||||
->toContain('(window.innerWidth - panelWidth) / 2')
|
||||
->toContain('panel.style.width = `${panelWidth}px`')
|
||||
->toContain('panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`')
|
||||
->toContain('x-show="open"')
|
||||
->not->toContain('x-show="open && positioned"')
|
||||
->toContain('aria-multiselectable="true"')
|
||||
->toContain('Reset filters')
|
||||
->toContain('wire:click="clearFilters"')
|
||||
@@ -61,3 +72,30 @@ it('uses the standard search control for frontend filtered infrastructure tables
|
||||
->toContain('clear-when="search"')
|
||||
->toContain("clear-action=\"search = ''\"");
|
||||
});
|
||||
|
||||
it('uses the collision aware dropdown on the projects page', function () {
|
||||
$projects = file_get_contents(resource_path('views/livewire/project/index.blade.php'));
|
||||
|
||||
expect($projects)
|
||||
->toContain('<x-table.dropdown')
|
||||
->not->toContain('x-show="sortOpen"');
|
||||
});
|
||||
|
||||
it('uses collision aware filter and sort dropdowns on the resources page', function () {
|
||||
$resources = file_get_contents(resource_path('views/livewire/project/resource/index.blade.php'));
|
||||
|
||||
expect(substr_count($resources, '<x-table.dropdown'))->toBeGreaterThanOrEqual(2)
|
||||
->and($resources)
|
||||
->not->toContain('x-show="filterOpen"')
|
||||
->not->toContain('x-show="sortOpen"');
|
||||
});
|
||||
|
||||
it('does not use legacy floating sort or filter panels', function () {
|
||||
$views = collect(File::allFiles(resource_path('views')))
|
||||
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'php')
|
||||
->map(fn (SplFileInfo $file): string => $file->getContents())
|
||||
->implode("\n");
|
||||
|
||||
expect($views)
|
||||
->not->toMatch('/x-show="(?:sortOpen|filterOpen|filtersOpen)"/');
|
||||
});
|
||||
@@ -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',
|
||||
]);
|
||||
Reference in new issue
Block a user