Skip to content

Commit 3a3940f

Browse files
MyesteryDrJKL
authored andcommitted
feat: add fuzzy search with debouncing to context menu
Replaces simple substring matching with Fuse.js fuzzy search for better user experience. Adds 300ms debouncing to reduce processing overhead. Includes debug logging for troubleshooting.
1 parent b89b6ba commit 3a3940f

File tree

1 file changed

+67
-7
lines changed

1 file changed

+67
-7
lines changed

src/components/graph/selectionToolbox/NodeOptions.vue

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
</template>
5555

5656
<script setup lang="ts">
57-
import { useRafFn } from '@vueuse/core'
57+
import { debouncedRef, useRafFn } from '@vueuse/core'
58+
import { useFuse } from '@vueuse/integrations/useFuse'
5859
import Popover from 'primevue/popover'
5960
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
6061
import { useI18n } from 'vue-i18n'
@@ -87,6 +88,7 @@ const popover = ref<InstanceType<typeof Popover>>()
8788
const targetElement = ref<HTMLElement | null>(null)
8889
const searchInput = ref<HTMLInputElement | null>(null)
8990
const searchQuery = ref('')
91+
const debouncedSearchQuery = debouncedRef(searchQuery, 300)
9092
const isTriggeredByToolbox = ref<boolean>(true)
9193
// Track open state ourselves so we can restore after drag/move
9294
const isOpen = ref(false)
@@ -101,18 +103,75 @@ const { menuOptions, menuOptionsWithSubmenu, bump } = useMoreOptionsMenu()
101103
const { toggleSubmenu, hideAllSubmenus } = useSubmenuPositioning()
102104
// const canvasInteractions = useCanvasInteractions()
103105
104-
// Filter menu options based on search query
106+
// Prepare searchable menu options (exclude dividers and categories)
107+
const searchableMenuOptions = computed(() =>
108+
menuOptions.value.filter(
109+
(option) => option.type !== 'divider' && option.type !== 'category'
110+
)
111+
)
112+
113+
// Set up fuzzy search with useFuse
114+
const { results } = useFuse(debouncedSearchQuery, searchableMenuOptions, {
115+
fuseOptions: {
116+
keys: ['label'],
117+
threshold: 0.4
118+
},
119+
matchAllWhenSearchEmpty: true
120+
})
121+
122+
// Debug logging
123+
watch(searchQuery, (newVal) => {
124+
console.warn('[NodeOptions] searchQuery changed:', newVal)
125+
})
126+
127+
watch(debouncedSearchQuery, (newVal) => {
128+
console.warn('[NodeOptions] debouncedSearchQuery changed:', newVal)
129+
})
130+
131+
watch(results, (newVal) => {
132+
console.warn('[NodeOptions] useFuse results:', newVal)
133+
console.warn('[NodeOptions] results count:', newVal.length)
134+
if (newVal.length > 0) {
135+
console.warn('[NodeOptions] first result:', newVal[0])
136+
}
137+
})
138+
139+
watch(searchableMenuOptions, (newVal) => {
140+
console.warn('[NodeOptions] searchableMenuOptions:', newVal)
141+
console.warn('[NodeOptions] searchableMenuOptions count:', newVal.length)
142+
})
143+
144+
// Filter menu options based on fuzzy search results
105145
const filteredMenuOptions = computed(() => {
106-
const query = searchQuery.value.toLowerCase().trim()
146+
const query = debouncedSearchQuery.value.trim()
147+
console.warn('[NodeOptions] filteredMenuOptions computed - query:', query)
148+
console.warn(
149+
'[NodeOptions] filteredMenuOptions computed - results.value:',
150+
results.value
151+
)
152+
107153
if (!query) {
154+
console.warn(
155+
'[NodeOptions] No query, returning all menuOptions:',
156+
menuOptions.value.length
157+
)
108158
return menuOptions.value
109159
}
110160
161+
// Extract matched items from Fuse results and create a Set of labels for fast lookup
162+
const matchedItems = results.value.map((result) => result.item)
163+
console.warn('[NodeOptions] matchedItems:', matchedItems)
164+
console.warn('[NodeOptions] matchedItems count:', matchedItems.length)
165+
166+
// Create a Set of matched labels for O(1) lookup
167+
const matchedLabels = new Set(matchedItems.map((item) => item.label))
168+
console.warn('[NodeOptions] matchedLabels:', Array.from(matchedLabels))
169+
111170
const filtered: MenuOption[] = []
112171
let lastWasDivider = false
113172
173+
// Reconstruct with dividers based on original structure
114174
for (const option of menuOptions.value) {
115-
// Skip category labels and dividers during filtering, add them back contextually
116175
if (option.type === 'divider') {
117176
lastWasDivider = true
118177
continue
@@ -122,9 +181,8 @@ const filteredMenuOptions = computed(() => {
122181
continue
123182
}
124183
125-
// Check if option matches search query
126-
const label = option.label?.toLowerCase() || ''
127-
if (label.includes(query)) {
184+
// Check if this option was matched by fuzzy search (compare by label)
185+
if (option.label && matchedLabels.has(option.label)) {
128186
// Add divider before this item if the last item was separated by a divider
129187
if (lastWasDivider && filtered.length > 0) {
130188
const lastItem = filtered[filtered.length - 1]
@@ -137,6 +195,8 @@ const filteredMenuOptions = computed(() => {
137195
}
138196
}
139197
198+
console.warn('[NodeOptions] final filtered results:', filtered)
199+
console.warn('[NodeOptions] final filtered count:', filtered.length)
140200
return filtered
141201
})
142202

0 commit comments

Comments
 (0)