Skip to content

Commit ad2f0b4

Browse files
committed
fix(search): resolve persistent keyword highlight in card list
1 parent 6fb75c8 commit ad2f0b4

2 files changed

Lines changed: 60 additions & 6 deletions

File tree

app/composables/useStructuredFilters.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,22 +118,36 @@ export function useStructuredFilters(options: UseStructuredFiltersOptions) {
118118
const { t } = useI18n()
119119

120120
const searchQuery = shallowRef(normalizeSearchParam(route.query.q))
121+
122+
// Filter state - must be declared before the watcher that uses it
123+
const filters = ref<StructuredFilters>({
124+
...DEFAULT_FILTERS,
125+
...initialFilters,
126+
})
127+
128+
// Watch route query changes and sync filter state
121129
watch(
122130
() => route.query.q,
123131
urlQuery => {
124132
const value = normalizeSearchParam(urlQuery)
125133
if (searchQuery.value !== value) {
126134
searchQuery.value = value
127135
}
136+
137+
// Sync filters with URL
138+
// When URL changes (e.g. from search input or navigation),
139+
// we need to update our local filter state to match
140+
const parsed = parseSearchOperators(value)
141+
142+
filters.value.text = parsed.text ?? ''
143+
filters.value.keywords = [...(parsed.keywords ?? [])]
144+
145+
// Note: We intentionally don't reset other filters (security, downloadRange, etc.)
146+
// as those are not typically driven by the search query string structure
128147
},
148+
{ immediate: true },
129149
)
130150

131-
// Filter state
132-
const filters = ref<StructuredFilters>({
133-
...DEFAULT_FILTERS,
134-
...initialFilters,
135-
})
136-
137151
// Sort state
138152
const sortOption = shallowRef<SortOption>(initialSort ?? 'updated-desc')
139153

test/nuxt/composables/useStructuredFilters.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,43 @@ describe('hasSearchOperators', () => {
185185
expect(hasSearchOperators({ name: [], keywords: [] })).toBe(false)
186186
})
187187
})
188+
189+
describe('keyword clearing scenarios', () => {
190+
it('returns keywords when kw: operator is present', () => {
191+
const result = parseSearchOperators('test kw:react')
192+
expect(result.keywords).toEqual(['react'])
193+
expect(result.text).toBe('test')
194+
})
195+
196+
it('returns undefined keywords when kw: operator is removed', () => {
197+
const result = parseSearchOperators('test')
198+
expect(result.keywords).toBeUndefined()
199+
expect(result.text).toBe('test')
200+
})
201+
202+
it('handles transition from keyword to no keyword', () => {
203+
// Simulate the state transition when user removes keyword from search
204+
const withKeyword = parseSearchOperators('test kw:react')
205+
expect(withKeyword.keywords).toEqual(['react'])
206+
207+
const withoutKeyword = parseSearchOperators('test')
208+
expect(withoutKeyword.keywords).toBeUndefined()
209+
210+
// This is what useStructuredFilters does in the watcher:
211+
// filters.value.keywords = [...(parsed.keywords ?? [])]
212+
const updatedKeywords = [...(withoutKeyword.keywords ?? [])]
213+
expect(updatedKeywords).toEqual([])
214+
})
215+
216+
it('returns empty keywords array after nullish coalescing', () => {
217+
// Verify the exact logic used in useStructuredFilters watcher
218+
const testCases = ['', 'test', 'some search query', 'name:package', 'desc:something']
219+
220+
for (const query of testCases) {
221+
const parsed = parseSearchOperators(query)
222+
// This is the exact line from useStructuredFilters.ts:
223+
const keywords = [...(parsed.keywords ?? [])]
224+
expect(keywords).toEqual([])
225+
}
226+
})
227+
})

0 commit comments

Comments
 (0)