fix(ui): keep settings subnav in-page and persist sidebar collapse

Scroll active application settings sections with buttons instead of Livewire navigation, persist sidebar collapse even with auto-collapse, drop the unused modal id, and right-align destination status.
This commit is contained in:
Andras Bacsai 2026-09-08 09:20:38 +02:00
parent 0cdb98d969
commit 0afa59bc02
8 changed files with 43 additions and 16 deletions

View file

@ -318,13 +318,19 @@ class="absolute right-1 top-1 flex size-6 items-center justify-center rounded-md
<div x-show="open" x-collapse.duration.200ms x-cloak
class="nav-children flex flex-col gap-0.5 py-1">
@foreach ($sections as $section)
<a class="menu-subitem"
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
href="{{ route($menuItem['route'], $applicationRouteParameters) }}#{{ $section['id'] }}"
{{ wireNavigate() }}
x-on:click="menuOpen = false; if (document.getElementById('{{ $section['id'] }}')) { $event.preventDefault(); activeSection = '{{ $section['id'] }}'; history.replaceState(null, '', '#{{ $section['id'] }}'); window.scrollToSettingsSection?.('{{ $section['id'] }}'); }">
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</a>
@if ($menuItem['active'])
<button type="button" class="menu-subitem"
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
x-on:click="menuOpen = false; activeSection = '{{ $section['id'] }}'; history.replaceState(null, '', '#{{ $section['id'] }}'); window.scrollToSettingsSection?.('{{ $section['id'] }}')">
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</button>
@else
<a class="menu-subitem"
href="{{ route($menuItem['route'], $applicationRouteParameters) }}#{{ $section['id'] }}"
{{ wireNavigate() }} x-on:click="menuOpen = false">
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</a>
@endif
@endforeach
</div>
</div>

View file

@ -44,7 +44,7 @@ class="fixed inset-0 z-99 overflow-y-auto">
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="absolute inset-0 w-full h-full bg-black/50 backdrop-blur-[2px]"></div>
<div @if ($closeOutside) @click.self="modalOpen=false" @endif class="relative flex min-h-full items-start justify-center p-2 sm:items-center sm:p-4">
<div id="{{ $modalId }}" x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"

View file

@ -25,7 +25,7 @@
},
targetCollapsed() {
this.hasSecondBar = !!document.querySelector('.application-settings-navigation');
return this.autoCollapse ? this.hasSecondBar : this.userCollapsed;
return this.userCollapsed || (this.autoCollapse && this.hasSecondBar);
},
applyCollapsed(animate) {
const target = this.targetCollapsed();
@ -41,10 +41,8 @@
},
toggleSidebar() {
this.collapsed = !this.collapsed;
if (!this.autoCollapse) {
this.userCollapsed = this.collapsed;
localStorage.setItem('sidebarCollapsed', this.collapsed);
}
this.userCollapsed = this.collapsed;
localStorage.setItem('sidebarCollapsed', this.userCollapsed);
},
toggleAutoCollapse() {
this.autoCollapse = !this.autoCollapse;

View file

@ -36,7 +36,7 @@ class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs text-neutral-700 d
<a href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}"
{{ wireNavigate() }} class="button">Open server</a>
<x-application.restart-limit-warning :application="$resource" />
<x-status-summary :status="$resource->status" />
<x-status-summary :status="$resource->status" align="right" />
@if ($hasAdditionalDestinations)
<x-forms.button canGate="deploy" :canResource="$resource"
wire:click="redeploy('{{ data_get($resource, 'destination.id') }}','{{ data_get($resource, 'destination.server.id') }}')">

View file

@ -5,7 +5,7 @@
$applicationSection = str($view)->before('@else')->value();
expect($applicationSection)
->toContain('<x-status-summary :status="$resource->status" />')
->toContain('<x-status-summary :status="$resource->status" align="right" />')
->not->toContain('<x-status :resource="$resource"');
});

View file

@ -1,5 +1,13 @@
<?php
use Illuminate\Support\Facades\Blade;
test('input modal renders without requiring a modal id', function () {
$html = Blade::render('<x-modal-input>Modal content</x-modal-input>');
expect($html)->toContain('Modal content');
});
test('confirmation modal closes before dispatching an event that can open another modal', function () {
$modal = file_get_contents(resource_path('views/components/modal-confirmation.blade.php'));

View file

@ -45,6 +45,18 @@
->toContain('stableFrames');
});
test('application configuration subitems scroll without navigating the active page', function () {
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($sidebar)
->toContain("@if (\$menuItem['active'])")
->toContain('<button type="button" class="menu-subitem"')
->toContain("window.scrollToSettingsSection?.('{{ \$section['id'] }}')")
->toContain('@else')
->toContain("href=\"{{ route(\$menuItem['route'], \$applicationRouteParameters) }}#{{ \$section['id'] }}\"")
->not->toContain("if (document.getElementById('{{ \$section['id'] }}'))");
});
test('postgresql general navigation lists each in-page settings section', function () {
$sidebar = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php'));
$general = file_get_contents(resource_path('views/livewire/project/database/postgresql/general.blade.php'));

View file

@ -27,7 +27,10 @@
$layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
expect($layout)
->toContain("collapsed: localStorage.getItem('sidebarCollapsed') === 'true'")
->toContain("userCollapsed: localStorage.getItem('sidebarCollapsed') === 'true'")
->toContain('return this.userCollapsed || (this.autoCollapse && this.hasSecondBar);')
->toContain('this.userCollapsed = this.collapsed;')
->toContain("localStorage.setItem('sidebarCollapsed', this.userCollapsed);")
->toContain('sidebarReady: false')
->toContain(":class=\"[collapsed ? 'lg:w-16' : 'lg:w-56', sidebarReady ? 'transition-[width] duration-200' : '']\"")
->toContain(":class=\"[collapsed ? 'lg:ml-16' : 'lg:ml-56', sidebarReady ? 'transition-[margin] duration-200' : '']\"");