feat(ui): polish email modal, search loading, and volume tables

Move profile email change into a modal with Alpine focus/close handling,
show a loading overlay during command-palette navigation, and adjust
volumes/config-diff grids plus related service/storage UI. Cover with
feature tests for search, profile, storage, and domains.
This commit is contained in:
Andras Bacsai committed 2026-08-05 14:07:19 +02:00
1 parent a7a06aa6c8
commit 0312cc8ffb
18 files changed
+236 -109

No files matched your search

+1
View File
@@ -60,6 +60,7 @@ it('renders frontend-only application backup search data for volume names and fr
->assertSee('Target A–Z')
->assertSee('filterOpen')
->assertSee('sortOpen')
->assertDontSee('class="font-mono text-xs">daily', false)
->assertDontSee('backup-type-filter-trigger', false)
->assertDontSee('backup-sort-trigger', false);
});
@@ -0,0 +1,14 @@
<?php
it('shows a loading overlay while resource selection steps load', function () {
$view = file_get_contents(resource_path('views/livewire/global-search.blade.php'));
expect($view)
->toContain('wire:loading.class="pointer-events-none opacity-40 blur-[2px]"')
->toContain('wire:loading.flex')
->toContain('selectServer,selectDestination,selectProject,selectEnvironment')
->toContain('Loading selection…')
->toContain('isPaletteTransitioning')
->toContain('runPaletteTransition')
->toContain('Loading…');
});
@@ -131,6 +131,12 @@ it('uses the same eye-off2 icon in configuration changes expand toggle', functio
->not->toContain('M22.2954 6.31083');
});
it('uses a wide configuration changes dialog', function () {
$view = file_get_contents(resource_path('views/livewire/project/shared/configuration-checker.blade.php'));
expect($view)->toContain('max-w-6xl');
});
it('renders env var password input before visibility toggle in tab order', function () {
$html = Blade::render('<x-forms.env-var-input type="password" id="secret" />');
@@ -118,7 +118,9 @@ it('batches volume backup meta and exposes forms for every volume', function ()
$component = Livewire::test(All::class, ['resource' => $application]);
expect($component->get('volumeBackupMeta'))->toHaveCount(5)
->and($component->get('forms'))->toHaveCount(5);
->and($component->get('forms'))->toHaveCount(5)
->and($component->html())->toContain('volumes-col-backup')
->not->toContain('table-badge table-badge-success');
foreach ($component->get('volumeBackupMeta') as $meta) {
expect($meta['enabled'])->toBeTrue()
@@ -101,13 +101,15 @@ it('renders volumes as a data table with shared column headers', function () {
->toContain('Volume Name')
->toContain('Source Path')
->toContain('Destination Path')
->toContain('volumes-col-backup')
->toContain('supportsPreviewSuffix')
->toContain('openBackupModal')
->toContain('data-table-row')
->toContain('volumes-mobile-label')
->toContain('table-badge-success')
->not->toContain('table-badge table-badge-success')
->not->toContain('livewire:project.shared.storages.show')
->not->toContain('x-status-badge');
->not->toContain('x-status-badge')
->not->toContain('Service volume mounts are read-only here.');
// Show remains available for isolated embeds/tests but is no longer nested from All.
expect($showView)
@@ -118,7 +120,16 @@ it('renders volumes as a data table with shared column headers', function () {
expect($storageView)
->toContain('Str::headline($resource->name)')
->toContain(':flush="true"')
->toContain('storage-service-');
->toContain("'storage-service-'.\$resource->uuid");
$serviceConfigurationView = file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php'));
expect($serviceConfigurationView)
->toContain('storageSections')
->toContain('menu-subitem')
->toContain('scrollToSettingsSection')
->toContain('Service volume mounts are read-only here.')
->toContain("'storage-service-'.\$resource->uuid");
$css = file_get_contents(resource_path('css/app.css'));
@@ -131,6 +142,8 @@ it('renders volumes as a data table with shared column headers', function () {
->toContain('@media (max-width: 768px)')
->toContain('.table-badge-success');
expect($css)->toContain('17.5rem');
// Settings form labels are 13px (not Tailwind text-sm 14px).
expect($css)
->toMatch('/\.application-settings-form label\s*\{[^}]*font-size:\s*13px/s');
@@ -17,6 +17,22 @@ it('adds profile navigation with an appearance tab and route', function () {
->not->toContain('<h1>Profile</h1>\n <div class="subtitle -mt-2">');
});
it('opens the email change form without a Livewire request', function () {
$profileView = file_get_contents(resource_path('views/livewire/profile/index.blade.php'));
expect($profileView)
->toContain('@click="openEmailModal()"')
->toContain('this.$refs.newEmailInput?.focus()')
->toContain('x-ref="newEmailInput"')
->toContain('x-show="emailModalOpen"')
->toContain('x-teleport="body"')
->toContain('Change email')
->toContain('Send code')
->toContain('Verify email')
->not->toContain('>Cancel</x-forms.button>')
->not->toContain('wire:click="showEmailChangeForm"');
});
it('keeps color theme preferences on the profile appearance view without page width or density controls', function () {
$appearanceView = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
+22 -4
View File
@@ -185,7 +185,8 @@ it('auto-adds the suggested www pair when adding a service domain with both dire
->set('newDomain', 'https://web.example.com')
->call('addDomain')
->assertHasNoErrors()
->assertDispatched('success');
->assertDispatched('success')
->assertSee('DNS skipped');
expect(explode(',', (string) $this->webApp->fresh()->fqdn))
->toContain('https://web.example.com')
@@ -203,6 +204,22 @@ it('adds a domain to a selected service application', function () {
$this->webApp->refresh();
expect(explode(',', (string) $this->webApp->fqdn))
->toBe(['https://web.example.com', 'https://www.web.example.com']);
$dnsStatuses = $this->webApp->domain_dns_statuses;
expect($dnsStatuses)
->toHaveKey('https://web.example.com')
->toHaveKey('https://www.web.example.com')
->and($dnsStatuses['https://web.example.com']['status'])
->toBe('skipped')
->and($dnsStatuses['https://web.example.com']['checked_at'])
->not->toBeNull();
});
it('does not duplicate the service name as a badge in the domain cell', function () {
$view = file_get_contents(resource_path('views/livewire/project/service/partials/domain-table.blade.php'));
expect($view)->not->toContain('domains-service-mobile table-badge');
});
it('rolls back a domain change when compose regeneration fails', function () {
@@ -273,7 +290,9 @@ it('prunes the previous dns status when a service domain is renamed', function (
$this->apiApp->refresh();
expect($this->apiApp->fqdn)->toBe('https://renamed.example.com')
->and($this->apiApp->domain_dns_statuses)->toBeNull();
->and($this->apiApp->domain_dns_statuses)->not->toHaveKey('https://api.example.com')
->and($this->apiApp->domain_dns_statuses)->toHaveKey('https://renamed.example.com')
->and($this->apiApp->domain_dns_statuses['https://renamed.example.com']['status'])->toBe('skipped');
});
it('does not restore stale dns status when a removed service domain is re-added', function () {
@@ -392,8 +411,7 @@ it('hides dns message text when service domain dns status is ok', function () {
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
->assertSee('DNS OK')
->assertDontSee('DNS points to 203.0.113.10')
->assertDontSee('Last checked');
->assertDontSee('DNS points to 203.0.113.10');
});
it('forbids read-only users from checking service domain dns', function (string $action, array $parameters) {