fix(ui): collapse non-active sub-sections + accordion on inline service nav

Follow-up to the settings-sidebar accordion. Two gaps:

- Application sidebar rendered every item's in-page sub-sections, so Advanced's
  Build/Container/Deployment/… showed while you were on General. Gate the
  nav-children on the active item so only the current page's sub-sections expand
  (database/service/server already did this).
- The service *main* configuration page renders its nav inline (the component is
  only used by service sub-pages), so it missed the accordion. Wire it up there
  too.

Extends the regression test to cover both.
This commit is contained in:
Aditya Tripathi
2026-09-18 08:59:38 +00:00
parent 7585480670
commit 1151449221
3 changed files with 35 additions and 16 deletions
@@ -288,23 +288,17 @@
</span>
@endif
</a>
@if (filled($sections))
{{-- Sub-sections belong to the current page only; collapse them for
every other item so the sidebar stays short. --}}
@if ($menuItem['active'] && filled($sections))
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex"
x-data="{ activeSection: '' }">
@foreach ($sections as $section)
@if ($menuItem['active'])
<button type="button" class="menu-subitem"
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
x-on:click="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() }}>
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</a>
@endif
<button type="button" class="menu-subitem"
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
x-on:click="activeSection = '{{ $section['id'] }}'; history.replaceState(null, '', '#{{ $section['id'] }}'); window.scrollToSettingsSection?.('{{ $section['id'] }}')">
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
</button>
@endforeach
</div>
@endif
@@ -47,6 +47,9 @@
->values())
->filter(fn ($items) => $items->isNotEmpty());
// Group that holds the current page — the only one expanded by default.
$activeGroup = (string) $groupedItems->search(fn ($items) => $items->contains(fn ($item) => $item['active'] ?? false));
$storageSections = $applications
->concat($databases)
->map(fn ($resource): array => [
@@ -59,13 +62,23 @@
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-8">
<aside class="application-settings-navigation min-w-0 xl:self-start">
<nav aria-label="Service settings"
x-data="settingsSidebarAccordion({ activeGroup: @js($activeGroup), storageKey: 'coolify.settings-sidebar.service' })"
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
@foreach ($groupedItems as $groupLabel => $groupItems)
@unless ($loop->first)
<div class="my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]"
aria-hidden="true"></div>
@endunless
<div class="nav-section hidden xl:block">{{ $groupLabel }}</div>
<button type="button" class="nav-section-toggle hidden xl:flex" @click="toggle(@js($groupLabel))"
:aria-expanded="isOpen(@js($groupLabel))">
<span>{{ $groupLabel }}</span>
<svg class="size-3 shrink-0 opacity-60 transition-transform"
:class="!isOpen(@js($groupLabel)) && '-rotate-90'" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" />
</svg>
</button>
<div class="contents" :class="isOpen(@js($groupLabel)) ? 'xl:block' : 'xl:hidden'">
@foreach ($groupItems as $menuItem)
<a @class([
'menu-item',
@@ -93,6 +106,7 @@
</div>
@endif
@endforeach
</div>
@endforeach
</nav>
</aside>
+12 -1
View File
@@ -8,6 +8,7 @@ $groupedSidebars = [
'application' => 'resources/views/components/application/configuration-sidebar.blade.php',
'database' => 'resources/views/components/database/configuration-sidebar.blade.php',
'service' => 'resources/views/components/service/configuration-sidebar.blade.php',
'service-page' => 'resources/views/livewire/project/service/configuration.blade.php',
'server' => 'resources/views/components/server/sidebar.blade.php',
];
@@ -18,10 +19,20 @@ it('wires the collapsible accordion into every grouped settings sidebar', functi
->toContain('settingsSidebarAccordion(') // shared Alpine data provider
->toContain('$activeGroup') // only the active group opens by default
->toContain('nav-section-toggle') // group header is a toggle button
->toContain("toggle(") // header collapses/expands the group
->toContain('toggle(') // header collapses/expands the group
->toContain("? 'xl:block' : 'xl:hidden'"); // desktop-only collapse wrapper
})->with($groupedSidebars);
it('only expands in-page sub-sections for the active page', function () {
// Regression: the application sidebar used to render every item's sub-sections
// (Advanced's Build/Container/… showed while you were on General).
$contents = file_get_contents(base_path('resources/views/components/application/configuration-sidebar.blade.php'));
expect($contents)
->toContain("\$menuItem['active'] && filled(\$sections)")
->not->toContain('@if (filled($sections))');
});
it('registers the accordion Alpine provider', function () {
expect(file_get_contents(base_path('resources/js/app.js')))
->toContain('initializeSettingsSidebarAccordionComponent');