mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-22 08:02:40 +00:00
* feat: add fuzzy search to instance settings sidebar Adds a search input at the top of the superadmin settings sidebar that uses uFuzzy for fuzzy matching against all setting labels, descriptions, and categories. Selecting a result navigates to the correct tab and scrolls to the specific setting card with a brief highlight. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: improve settings search display and description matching - Extract only the label portion from uFuzzy highlighted text for cleaner dropdown display - Show description only when the match is in the description and NOT in the label - Truncate descriptions to 80 chars in searchable items - Add maxHeight prop to SelectDropdown for configurable height Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: fix search description truncation and handle undefined marked values Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove description from settings search dropdown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add smooth outline transition for setting highlight animation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: highlight first search result by default for enter-to-select Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * nit * clean code * fix: address review feedback - sanitize html, remove max-w-40, document description field Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: constrain search dropdown width to prevent long title overflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * perf: add 150ms debounce to settings search filter Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: clean up timeouts on destroy and re-invocation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract settings search into reusable SettingsSearchInput component Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use twMerge for class prop in SettingsSearchInput Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: wrap debounced state write in untrack to prevent re-triggering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
2.6 KiB
Svelte
90 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import SearchItems from '../SearchItems.svelte'
|
|
import SelectDropdown from '../select/SelectDropdown.svelte'
|
|
import type { ProcessedItem } from '../select/utils.svelte'
|
|
import TextInput from '../text_input/TextInput.svelte'
|
|
import { Search } from 'lucide-svelte'
|
|
import {
|
|
extractMarkedLabel,
|
|
type SearchableSettingItem
|
|
} from '../instanceSettings'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { untrack } from 'svelte'
|
|
|
|
let {
|
|
searchableItems,
|
|
onSelect,
|
|
class: className = ''
|
|
}: {
|
|
searchableItems: SearchableSettingItem[]
|
|
onSelect: (item: SearchableSettingItem) => void
|
|
class?: string
|
|
} = $props()
|
|
|
|
let settingsSearchFilter = $state('')
|
|
let debouncedSearchFilter = $state('')
|
|
let filteredSearchItems: (SearchableSettingItem & { marked: string })[] = $state([])
|
|
let searchInputEl: HTMLDivElement | undefined = $state()
|
|
|
|
// Debounce search to avoid running uFuzzy on every keystroke
|
|
$effect(() => {
|
|
const val = settingsSearchFilter
|
|
const timeout = setTimeout(() => untrack(() => (debouncedSearchFilter = val)), 150)
|
|
return () => clearTimeout(timeout)
|
|
})
|
|
|
|
const searchDropdownOpen = $derived(
|
|
settingsSearchFilter.trim().length > 0 && filteredSearchItems.length > 0
|
|
)
|
|
|
|
let searchProcessedItems: ProcessedItem<SearchableSettingItem & { marked: string }>[] = $derived(
|
|
filteredSearchItems.map((item) => ({
|
|
label: item.label,
|
|
value: item,
|
|
subtitle: item.category
|
|
}))
|
|
)
|
|
|
|
function handleSelect(item: SearchableSettingItem) {
|
|
settingsSearchFilter = ''
|
|
onSelect(item)
|
|
}
|
|
</script>
|
|
|
|
<SearchItems
|
|
filter={debouncedSearchFilter}
|
|
items={searchableItems}
|
|
bind:filteredItems={filteredSearchItems}
|
|
f={(x) => x.label + ' ' + (x.description ?? '') + ' ' + x.category}
|
|
/>
|
|
|
|
<div class={twMerge('relative', className)}>
|
|
<div bind:this={searchInputEl} class="relative w-full">
|
|
<Search class="absolute left-2 top-1/2 -translate-y-1/2 text-tertiary" size={14} />
|
|
<TextInput
|
|
inputProps={{ placeholder: 'Search settings...' }}
|
|
bind:value={settingsSearchFilter}
|
|
class="pl-7 text-xs w-full"
|
|
/>
|
|
</div>
|
|
<SelectDropdown
|
|
processedItems={searchProcessedItems}
|
|
value={undefined}
|
|
open={searchDropdownOpen}
|
|
disablePortal
|
|
class="max-w-full"
|
|
itemLabelWrapperClasses="hidden"
|
|
itemButtonWrapperClasses="overflow-hidden"
|
|
getInputRect={searchInputEl ? () => searchInputEl!.getBoundingClientRect() : undefined}
|
|
onSelectValue={(item) => handleSelect(item.value)}
|
|
highlightFirstOnOpen
|
|
maxHeight={400}
|
|
>
|
|
{#snippet startSnippet({ item })}
|
|
<div class="text-xs truncate w-full min-w-0"
|
|
>{@html extractMarkedLabel(item.value.marked, item.value.label.length)}</div
|
|
>
|
|
{/snippet}
|
|
</SelectDropdown>
|
|
</div>
|