fix: show empty state when node library search has no matches (#12254)

The left-sidebar node library search fell back to rendering all visible
node defs whenever the filter returned zero hits, so gibberish queries
looked like the filter wasn't applied. Gates the fallback on the query
string and renders a "No nodes match" empty state across all tabs
(Essentials/All/Blueprints) when the active query has no results.

Before:


https://github.com/user-attachments/assets/ab11ef5e-c757-41f1-9e07-3427942b9929

After: 



https://github.com/user-attachments/assets/a724aaab-95a2-4832-a694-3d8e543fdabf


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12254-fix-show-empty-state-when-node-library-search-has-no-matches-3606d73d365081d19eaaff1095355072)
by [Unito](https://www.unito.io)
This commit is contained in:
Robin Huang
2026-05-22 03:15:26 -07:00
committed by GitHub
parent c703db5f6c
commit 7b4fef5eca
3 changed files with 89 additions and 7 deletions

View File

@@ -3,10 +3,20 @@ import { ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import { render, screen } from '@testing-library/vue'
import { fireEvent, render, screen } from '@testing-library/vue'
import NodeLibrarySidebarTabV2 from './NodeLibrarySidebarTabV2.vue'
const hoisted = vi.hoisted(() => ({
mockSearchNode: vi.fn<(query: string) => unknown[]>(() => [])
}))
vi.mock('@/services/nodeSearchService', () => ({
NodeSearchService: class {
searchNode = hoisted.mockSearchNode
}
}))
vi.mock('@vueuse/core', async () => {
const actual = await vi.importActual('@vueuse/core')
return {
@@ -72,8 +82,10 @@ vi.mock('./nodeLibrary/NodeDragPreview.vue', () => ({
vi.mock('@/components/ui/search-input/SearchInput.vue', () => ({
default: {
name: 'SearchBox',
template: '<input data-testid="search-box" />',
template:
'<input data-testid="search-box" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
props: ['modelValue', 'placeholder'],
emits: ['update:modelValue', 'search'],
setup() {
return { focus: vi.fn() }
},
@@ -84,12 +96,22 @@ vi.mock('@/components/ui/search-input/SearchInput.vue', () => ({
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: {} }
messages: {
en: {
sideToolbar: {
nodeLibraryTab: {
noMatchingNodes: 'No nodes match "{query}"'
}
}
}
}
})
describe('NodeLibrarySidebarTabV2', () => {
beforeEach(() => {
vi.clearAllMocks()
hoisted.mockSearchNode.mockReset()
hoisted.mockSearchNode.mockReturnValue([])
})
function renderComponent() {
@@ -123,4 +145,49 @@ describe('NodeLibrarySidebarTabV2', () => {
expect(screen.queryByTestId('all-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('blueprints-panel')).not.toBeInTheDocument()
})
describe('search empty state', () => {
it('does not render the empty state when search query is empty', () => {
renderComponent()
expect(screen.queryByText(/No nodes match/)).not.toBeInTheDocument()
expect(screen.getByTestId('essential-panel')).toBeInTheDocument()
})
it('renders the empty state with the query when search has no matches', async () => {
hoisted.mockSearchNode.mockReturnValue([])
renderComponent()
await fireEvent.update(screen.getByTestId('search-box'), 'gibberish')
expect(screen.getByText('No nodes match "gibberish"')).toBeInTheDocument()
expect(screen.queryByTestId('essential-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('all-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('blueprints-panel')).not.toBeInTheDocument()
})
it('hides the empty state when the search has matches', async () => {
hoisted.mockSearchNode.mockReturnValue([{ name: 'KSampler' }])
renderComponent()
await fireEvent.update(screen.getByTestId('search-box'), 'ksampler')
expect(screen.queryByText(/No nodes match/)).not.toBeInTheDocument()
expect(screen.getByTestId('essential-panel')).toBeInTheDocument()
})
it('hides the empty state once the query is cleared', async () => {
hoisted.mockSearchNode.mockReturnValue([])
renderComponent()
const input = screen.getByTestId('search-box')
await fireEvent.update(input, 'gibberish')
expect(screen.getByText('No nodes match "gibberish"')).toBeInTheDocument()
await fireEvent.update(input, '')
expect(screen.queryByText(/No nodes match/)).not.toBeInTheDocument()
expect(screen.getByTestId('essential-panel')).toBeInTheDocument()
})
})
})

View File

@@ -117,7 +117,17 @@
<template #body>
<NodeDragPreview />
<div class="flex h-full flex-col">
<div class="min-h-0 flex-1 overflow-y-auto py-2">
<div
v-if="hasNoMatches"
class="flex min-h-0 flex-1 items-center justify-center px-6 py-8 text-center text-sm text-muted-foreground"
>
{{
$t('sideToolbar.nodeLibraryTab.noMatchingNodes', {
query: searchQuery
})
}}
</div>
<div v-else class="min-h-0 flex-1 overflow-y-auto py-2">
<TabPanel
v-if="flags.nodeLibraryEssentialsEnabled"
:model-value="selectedTab"
@@ -274,9 +284,13 @@ const filteredNodeDefs = computed(() => {
})
const activeNodes = computed(() =>
filteredNodeDefs.value.length > 0
? filteredNodeDefs.value
: nodeDefStore.visibleNodeDefs
searchQuery.value.length === 0
? nodeDefStore.visibleNodeDefs
: filteredNodeDefs.value
)
const hasNoMatches = computed(
() => searchQuery.value.length > 0 && filteredNodeDefs.value.length === 0
)
const sections = computed(() => {

View File

@@ -904,6 +904,7 @@
"alphabetical": "A-Z",
"alphabeticalDesc": "Sort alphabetically within groups"
},
"noMatchingNodes": "No nodes match \"{query}\"",
"sections": {
"favorites": "Bookmarks",
"favoriteNode": "Bookmark Node",