mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-25 17:01:49 -04:00
feat(ui): search box on the application settings sidebar
The application settings sidebar is long, so add a client-side search that filters the nav items as you type. While searching it flattens the list (hides group headers/dividers and in-page sub-sections) and shows a 'no results' hint; clearing it restores the accordion. Extends the shared settingsSidebarAccordion Alpine helper (search/matches/hasResults) so other sidebars can opt in later.
This commit is contained in:
@@ -9,6 +9,21 @@ export function initializeSettingsSidebarAccordionComponent() {
|
||||
activeGroup: config.activeGroup || '',
|
||||
storageKey: config.storageKey || 'coolify.settings-sidebar',
|
||||
groups: {},
|
||||
// Optional client-side filter (sidebars that render a search box).
|
||||
search: '',
|
||||
labels: Array.isArray(config.labels) ? config.labels : [],
|
||||
get searching() {
|
||||
return this.search.trim() !== '';
|
||||
},
|
||||
matches(label) {
|
||||
if (!this.searching) {
|
||||
return true;
|
||||
}
|
||||
return String(label).toLowerCase().includes(this.search.trim().toLowerCase());
|
||||
},
|
||||
get hasResults() {
|
||||
return !this.searching || this.labels.some((label) => this.matches(label));
|
||||
},
|
||||
init() {
|
||||
let stored = {};
|
||||
try {
|
||||
|
||||
@@ -182,6 +182,9 @@
|
||||
// Group that holds the current page — the only one expanded by default.
|
||||
$activeGroup = (string) $groupedMenuItems->search(fn ($items) => $items->contains(fn ($item) => $item['active'] ?? false));
|
||||
|
||||
// All item labels, so the search box can show a "no results" state.
|
||||
$allLabels = collect($configurationMenuItems)->pluck('label')->values()->all();
|
||||
|
||||
// In-page sections (cards) shown as sub-items under the active page
|
||||
$isComposeApp = $application->build_pack === 'dockercompose';
|
||||
$pageSections = [
|
||||
@@ -252,14 +255,30 @@
|
||||
'is-flush' => $flush,
|
||||
])>
|
||||
<nav aria-label="Configuration sections"
|
||||
x-data="settingsSidebarAccordion({ activeGroup: @js($activeGroup), storageKey: 'coolify.settings-sidebar.application' })"
|
||||
x-data="settingsSidebarAccordion({ activeGroup: @js($activeGroup), storageKey: 'coolify.settings-sidebar.application', labels: @js($allLabels) })"
|
||||
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]">
|
||||
<div class="relative col-span-full mb-1 xl:mb-2">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input x-model.debounce.100ms="search" type="search" placeholder="Search settings"
|
||||
aria-label="Search settings"
|
||||
class="h-8 w-full rounded-lg border border-neutral-200 bg-white py-0 pr-7 pl-8 text-[12px] shadow-none placeholder:text-neutral-400 focus:border-accent focus:ring-0 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg dark:placeholder:text-fg-faint">
|
||||
<button x-cloak x-show="searching" type="button" @click="search = ''" aria-label="Clear search"
|
||||
class="absolute top-1/2 right-1.5 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
<p x-cloak x-show="searching && !hasResults"
|
||||
class="col-span-full px-2.5 py-2 text-[12px] text-neutral-500 dark:text-fg-dim">
|
||||
No settings match “<span x-text="search"></span>”.
|
||||
</p>
|
||||
@foreach ($groupedMenuItems 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>
|
||||
<div class="my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]"
|
||||
x-show="!searching" aria-hidden="true"></div>
|
||||
@endunless
|
||||
<button type="button" class="nav-section-toggle hidden xl:flex" @click="toggle(@js($groupLabel))"
|
||||
:aria-expanded="isOpen(@js($groupLabel))">
|
||||
<button type="button" class="nav-section-toggle hidden xl:flex" x-show="!searching"
|
||||
@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"
|
||||
@@ -267,10 +286,11 @@
|
||||
<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'">
|
||||
<div class="contents" :class="(searching || isOpen(@js($groupLabel))) ? 'xl:block' : 'xl:hidden'">
|
||||
@foreach ($groupItems as $menuItem)
|
||||
@php $sections = $pageSections[$menuItem['route']] ?? []; @endphp
|
||||
<div wire:key="application-settings-group-{{ str($menuItem['label'])->slug() }}">
|
||||
<div wire:key="application-settings-group-{{ str($menuItem['label'])->slug() }}"
|
||||
x-show="matches(@js($menuItem['label']))">
|
||||
<a wire:key="application-settings-link-{{ str($menuItem['label'])->slug() }}"
|
||||
@class([
|
||||
'menu-item',
|
||||
@@ -291,7 +311,7 @@
|
||||
{{-- 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"
|
||||
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex" x-show="!searching"
|
||||
x-data="{ activeSection: '' }">
|
||||
@foreach ($sections as $section)
|
||||
<button type="button" class="menu-subitem"
|
||||
|
||||
@@ -33,6 +33,19 @@ it('only expands in-page sub-sections for the active page', function () {
|
||||
->not->toContain('@if (filled($sections))');
|
||||
});
|
||||
|
||||
it('adds a client-side search to the application settings sidebar', function () {
|
||||
$sidebar = file_get_contents(base_path('resources/views/components/application/configuration-sidebar.blade.php'));
|
||||
|
||||
expect($sidebar)
|
||||
->toContain('Search settings')
|
||||
->toContain('x-model.debounce.100ms="search"')
|
||||
->toContain('matches(');
|
||||
|
||||
expect(file_get_contents(base_path('resources/js/settings-sidebar-accordion.js')))
|
||||
->toContain('matches(label)')
|
||||
->toContain('hasResults');
|
||||
});
|
||||
|
||||
it('registers the accordion Alpine provider', function () {
|
||||
expect(file_get_contents(base_path('resources/js/app.js')))
|
||||
->toContain('initializeSettingsSidebarAccordionComponent');
|
||||
|
||||
Reference in New Issue
Block a user