From e588df9fca488e0690957b2d03c269e0754c33a6 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 2 Mar 2025 14:27:28 -0500 Subject: [PATCH] Fix search bar behaviour (step 1) --- .../src/components/general/VMSearchBar.vue | 568 ++++++------------ 1 file changed, 191 insertions(+), 377 deletions(-) diff --git a/pvc-vue/src/components/general/VMSearchBar.vue b/pvc-vue/src/components/general/VMSearchBar.vue index a8830b0..fd23a27 100644 --- a/pvc-vue/src/components/general/VMSearchBar.vue +++ b/pvc-vue/src/components/general/VMSearchBar.vue @@ -94,7 +94,7 @@ :data-vm-name="vm.name" class="vm-list-item" :class="{ 'active': selectedVM === vm.name || vmFromUrl === vm.name }" - @click="() => { console.log('VM clicked:', vm); handleVMSelect(vm); }" + @click="handleVMSelect(vm)" >
@@ -140,15 +140,6 @@ const props = defineProps({ } }); -// Direct debug log of props -console.log('VMSearchBar props:', { - modelValue: props.modelValue, - showList: props.showList, - selectedVM: props.selectedVM, - vmFromUrl: props.vmFromUrl, - vmListLength: props.vmList.length -}); - const emit = defineEmits([ 'update:modelValue', 'search', @@ -161,71 +152,187 @@ const emit = defineEmits([ 'reset-filters' ]); +// UI state refs const showFilterMenu = ref(false); const filterDropdown = ref(null); const filterMenu = ref(null); const controlsBar = ref(null); +const vmListContainer = ref(null); -// Add local state for filters +// Search state refs +const inputValue = ref(''); +const searchText = ref(''); +const isFilterActive = ref(false); + +// Filter state const appliedFilters = ref({ states: {}, nodes: {} }); -// Add new ref for tracking search active state -const searchActive = ref(false); +// Initialize the component +onMounted(() => { + // Set up click outside handler + document.addEventListener('click', handleClickOutside); + + // Initialize input value based on selected VM + if (!props.showList && (props.selectedVM || props.vmFromUrl)) { + inputValue.value = props.selectedVM || props.vmFromUrl; + } else if (props.showList && props.modelValue) { + inputValue.value = props.modelValue; + searchText.value = props.modelValue; + } +}); -// Local ref to track input value -const inputValue = ref(''); +onUnmounted(() => { + document.removeEventListener('click', handleClickOutside); +}); -// Add a ref to the VM list container -const vmListContainer = ref(null); +// Handle search input +const handleSearch = (event) => { + const value = event.target.value; + inputValue.value = value; + searchText.value = value; + isFilterActive.value = true; + + emit('update:modelValue', value); + emit('search', value); +}; -// Add a ref to store the last search query -const lastSearchQuery = ref(''); - -// Add a flag to track if filtering should be applied -const filterActive = ref(false); - -// Add a flag to track if a search has been performed -const hasSearched = ref(false); - -// Add a flag to track if we should maintain focus after list refresh -const maintainFocusAfterRefresh = ref(false); - -// Watch both showList and selectedVM to update input value -watch([() => props.showList, () => props.selectedVM, () => props.vmFromUrl, () => props.modelValue], - ([showList, selectedVM, vmFromUrl, modelValue]) => { - console.log('Watch triggered:', { showList, selectedVM, vmFromUrl, modelValue }); +// Handle focus on search input +const handleFocus = (event) => { + // If the list is not shown, show it + if (!props.showList) { + emit('toggle-list'); + } else { + // If the list is already shown, activate filtering + isFilterActive.value = true; - if (!showList) { - // When list is closed, show selected VM or VM from URL - const effectiveVM = selectedVM || vmFromUrl; - if (effectiveVM) { - inputValue.value = effectiveVM; - } - } else { - // When list is open, show search query if it exists - if (modelValue) { - inputValue.value = modelValue; - searchActive.value = true; - } else { - // If no search query, show placeholder - inputValue.value = ''; - } + // Restore search text if available + if (searchText.value) { + inputValue.value = searchText.value; + emit('update:modelValue', searchText.value); } - }, { immediate: true } -); - -// Watch modelValue to update input when searching -watch(() => props.modelValue, (newVal) => { - if (newVal) { - lastSearchQuery.value = newVal; } + emit('focus', event); +}; + +// Handle blur on search input +const handleBlur = (event) => { + emit('blur', event); +}; + +// Handle click on search input +const handleSearchClick = () => { if (props.showList) { - inputValue.value = newVal || ''; + // When clicking the search input while list is open, activate filtering + isFilterActive.value = true; + + // Restore search text if available + if (searchText.value && searchText.value !== inputValue.value) { + inputValue.value = searchText.value; + emit('update:modelValue', searchText.value); + } } +}; + +// Toggle the VM list +const toggleList = () => { + if (props.showList) { + // If we're closing the list, save the search text + if (props.modelValue) { + searchText.value = props.modelValue; + } + } else { + // If we're opening the list, deactivate filtering + isFilterActive.value = false; + + // Restore search text in the input, but don't apply filtering + if (searchText.value) { + inputValue.value = searchText.value; + emit('update:modelValue', searchText.value); + } + } + + emit('toggle-list'); +}; + +// Clear search +const clearSearch = () => { + inputValue.value = ''; + searchText.value = ''; + isFilterActive.value = false; + emit('update:modelValue', ''); + emit('clear'); +}; + +// Select a VM +const handleVMSelect = (vm) => { + console.log('Selecting VM:', vm); + emit('select-vm', vm); +}; + +// Handle click outside +const handleClickOutside = (event) => { + if (showFilterMenu.value && + filterMenu.value && + !filterMenu.value.contains(event.target) && + !filterDropdown.value.contains(event.target)) { + showFilterMenu.value = false; + } +}; + +// Toggle filter menu +const toggleFilterMenu = () => { + showFilterMenu.value = !showFilterMenu.value; +}; + +// Filter toggle +const handleFilterToggle = (type, value) => { + appliedFilters.value[type][value] = !appliedFilters.value[type][value]; +}; + +// Reset filters +const handleResetFilters = () => { + Object.keys(appliedFilters.value.states).forEach(state => { + appliedFilters.value.states[state] = false; + }); + Object.keys(appliedFilters.value.nodes).forEach(node => { + appliedFilters.value.nodes[node] = false; + }); + showFilterMenu.value = false; +}; + +// Computed properties +const filteredVMs = computed(() => { + let filtered = [...props.vmList]; + + // Apply text filter only if filtering is active + if (isFilterActive.value && props.modelValue) { + const query = props.modelValue.toLowerCase(); + filtered = filtered.filter(vm => + vm.name.toLowerCase().includes(query) + ); + } + + // Apply state filters + const activeStates = Object.entries(appliedFilters.value.states) + .filter(([_, isActive]) => isActive) + .map(([state]) => state); + if (activeStates.length > 0) { + filtered = filtered.filter(vm => activeStates.includes(vm.state)); + } + + // Apply node filters + const activeNodes = Object.entries(appliedFilters.value.nodes) + .filter(([_, isActive]) => isActive) + .map(([node]) => node); + if (activeNodes.length > 0) { + filtered = filtered.filter(vm => activeNodes.includes(vm.node)); + } + + return filtered; }); // Calculate available states from vmList @@ -234,23 +341,7 @@ const availableStates = computed(() => { props.vmList.forEach(vm => { if (vm.state) states.add(vm.state); }); - const statesArray = Array.from(states); - - // Remove 'start' if it exists - const startIndex = statesArray.indexOf('start'); - if (startIndex !== -1) { - statesArray.splice(startIndex, 1); - } - - // Sort remaining states - statesArray.sort(); - - // Add 'start' back at beginning if it was present - if (startIndex !== -1) { - statesArray.unshift('start'); - } - - return statesArray; + return Array.from(states).sort(); }); // Calculate available nodes from vmList @@ -279,24 +370,7 @@ watch([availableStates, availableNodes], ([states, nodes]) => { }); }); -// Update handleFilterToggle to work with local state -const handleFilterToggle = (type, value) => { - searchActive.value = true; - appliedFilters.value[type][value] = !appliedFilters.value[type][value]; -}; - -// Update handleResetFilters -const handleResetFilters = () => { - Object.keys(appliedFilters.value.states).forEach(state => { - appliedFilters.value.states[state] = false; - }); - Object.keys(appliedFilters.value.nodes).forEach(node => { - appliedFilters.value.nodes[node] = false; - }); - showFilterMenu.value = false; -}; - -// Computed +// Count active filters const activeFiltersCount = computed(() => { let count = 0; Object.values(appliedFilters.value.states).forEach(isActive => { @@ -308,307 +382,47 @@ const activeFiltersCount = computed(() => { return count; }); -const filteredVMs = computed(() => { - let filtered = [...props.vmList]; - - // Apply search filter if filtering is active OR if a search has been performed - if ((filterActive.value || hasSearched.value) && props.modelValue) { - const query = props.modelValue.toLowerCase(); - filtered = filtered.filter(vm => - vm.name.toLowerCase().includes(query) - ); - } - - // Only apply other filters if search is active - if (searchActive.value) { - // Apply state filters - const activeStates = Object.entries(appliedFilters.value.states) - .filter(([_, isActive]) => isActive) - .map(([state]) => state); - if (activeStates.length > 0) { - filtered = filtered.filter(vm => activeStates.includes(vm.state)); - } - - // Apply node filters - const activeNodes = Object.entries(appliedFilters.value.nodes) - .filter(([_, isActive]) => isActive) - .map(([node]) => node); - if (activeNodes.length > 0) { - filtered = filtered.filter(vm => activeNodes.includes(vm.node)); - } - } - - return filtered; -}); - -// Methods -const handleSearch = (event) => { - // Always update the model value to preserve search - searchActive.value = true; - filterActive.value = true; // Activate filtering when typing in search - lastSearchQuery.value = event.target.value; - hasSearched.value = true; // Mark that a search has been performed - maintainFocusAfterRefresh.value = true; // Maintain focus after refresh - emit('update:modelValue', event.target.value); - emit('search', event.target.value); -}; - -const handleFocus = (event) => { - searchActive.value = true; - filterActive.value = true; // Activate filtering when focusing the search bar - maintainFocusAfterRefresh.value = true; // Maintain focus after refresh - - // If there's a saved search, make sure it's applied - if (props.modelValue) { - inputValue.value = props.modelValue; - - // Force a re-evaluation of the filteredVMs computed property - nextTick(() => { - // This is a hack to force Vue to re-evaluate the computed property - const temp = filteredVMs.value.length; - console.log('Forcing filter application, filtered count:', temp); - }); - } - emit('focus', event); -}; - -const handleBlur = (event) => { - // Only deactivate filtering if no search has been performed - if (!hasSearched.value) { - filterActive.value = false; - } - - // Check if the blur was caused by a click outside the component - const isClickOutside = !controlsBar.value?.contains(event.relatedTarget); - if (isClickOutside) { - maintainFocusAfterRefresh.value = false; - } - - emit('blur', event); -}; - -const clearSearch = () => { - emit('update:modelValue', ''); - emit('clear'); -}; - -const toggleList = () => { - console.log('toggleList called, showList:', props.showList); - - // If we're opening the list (currently closed) - if (!props.showList) { - // When opening the list via List VMs button, don't apply filtering initially - // Only deactivate filtering if no search has been performed - if (!hasSearched.value) { - filterActive.value = false; - } - - // But always restore the previous search text - if (lastSearchQuery.value) { - // Ensure the model value is preserved - if (props.modelValue !== lastSearchQuery.value) { - emit('update:modelValue', lastSearchQuery.value); - } - } - } - // If we're closing the list (currently open) - else { - // When closing, save the current search - if (props.modelValue) { - lastSearchQuery.value = props.modelValue; - // Don't clear the model value when closing - } - } - - emit('toggle-list'); -}; - -const handleVMSelect = (vm) => { - console.log('Selecting VM:', vm); - - // Don't clear the search query or change filter state - // Just emit the select-vm event - emit('select-vm', vm); -}; - -const toggleFilterMenu = () => { - showFilterMenu.value = !showFilterMenu.value; -}; - +// Status class helper const getStatusClass = (state) => { - if (!state) return ''; - + if (!state) return 'status-unknown'; switch(state.toLowerCase()) { - case 'start': - return 'status-running'; - case 'stop': - return 'status-stopped'; - case 'disable': - return 'status-disabled'; - default: - return ''; + case 'start': return 'status-running'; + case 'stop': return 'status-stopped'; + case 'disable': return 'status-paused'; + default: return 'status-unknown'; } }; -// Update click outside handling to close both filter menu and list -const handleClickOutside = (event) => { - // If click is outside the controls bar - if (!controlsBar.value?.contains(event.target)) { - if (props.showList) { - emit('toggle-list'); // Close the list - } - showFilterMenu.value = false; // Close filter menu - } - // If click is inside controls bar but outside filter dropdown - else if (showFilterMenu.value && !filterDropdown.value?.contains(event.target)) { - showFilterMenu.value = false; - } -}; - -onMounted(() => { - document.addEventListener('click', handleClickOutside); - - // Add click handler for search input - const searchInput = document.querySelector('.search-input'); - if (searchInput) { - searchInput.addEventListener('click', () => { - if (props.showList && props.modelValue) { - filterActive.value = true; - applyFilter(); - } - }); - } -}); - -onUnmounted(() => { - document.removeEventListener('click', handleClickOutside); -}); - -// Add a watcher for selectedVM with a debug log -watch(() => props.selectedVM, (newVal, oldVal) => { - console.log('VMSearchBar selectedVM changed:', { newVal, oldVal }); - // Update input value when selectedVM changes - if (!props.showList && newVal) { - inputValue.value = newVal; - } -}, { immediate: true }); - -// Add a function to scroll to the selected VM -const scrollToSelectedVM = () => { - if (!props.showList || (!props.selectedVM && !props.vmFromUrl)) return; - - nextTick(() => { - const selectedVMName = props.selectedVM || props.vmFromUrl; - const activeElement = document.querySelector(`.vm-list-item[data-vm-name="${selectedVMName}"]`); - if (activeElement && vmListContainer.value) { - activeElement.scrollIntoView({ block: 'center', behavior: 'smooth' }); - } - }); -}; - -// Update the watch for showList to handle search state -watch(() => props.showList, (isOpen) => { - if (isOpen) { - // When list opens, restore the last search query - nextTick(() => { - if (lastSearchQuery.value) { - inputValue.value = lastSearchQuery.value; - - // If the user clicked on the search bar to open it, apply the filter - if (document.activeElement && - document.activeElement.classList.contains('search-input')) { - filterActive.value = true; - applyFilter(); - } - } - }); - - // Also scroll to selected VM - scrollToSelectedVM(); - } else { - // When list closes, show the selected VM but preserve search +// Watch for changes in showList +watch(() => props.showList, (isVisible) => { + if (!isVisible) { + // When list is closed, show selected VM const effectiveVM = props.selectedVM || props.vmFromUrl; if (effectiveVM) { inputValue.value = effectiveVM; } - // Reset filter active state when closing - filterActive.value = false; + } else { + // When list is opened, show search text if available + if (searchText.value) { + inputValue.value = searchText.value; + } else { + inputValue.value = ''; + } + + // Only activate filtering if opened via search input, not List VMs button + // This is handled in handleFocus and toggleList } }); -// Also call it when selectedVM or vmFromUrl changes -watch([() => props.selectedVM, () => props.vmFromUrl], () => { - if (props.showList) { - scrollToSelectedVM(); - } -}); - -// Add a method to explicitly apply the filter -const applyFilter = () => { - filterActive.value = true; - // Force computed property re-evaluation - nextTick(() => { - const temp = filteredVMs.value.length; - console.log('Filter applied, filtered count:', temp); - }); -}; - -// Update the watch for props.vmList to maintain focus -watch(() => props.vmList, () => { - // When VM list refreshes, ensure search is preserved - if (props.showList && lastSearchQuery.value) { - // Make sure the model value is preserved - if (props.modelValue !== lastSearchQuery.value) { - emit('update:modelValue', lastSearchQuery.value); - } - - // If a search has been performed, keep filtering active - if (hasSearched.value && props.modelValue) { - filterActive.value = true; - - // If we should maintain focus, refocus the search input - if (maintainFocusAfterRefresh.value) { - nextTick(() => { - const searchInput = document.querySelector('.search-input'); - if (searchInput) { - searchInput.focus(); - } - }); - } - - nextTick(() => { - applyFilter(); - }); +// Watch for changes in selectedVM or vmFromUrl +watch([() => props.selectedVM, () => props.vmFromUrl], ([selectedVM, vmFromUrl]) => { + if (!props.showList) { + const effectiveVM = selectedVM || vmFromUrl; + if (effectiveVM) { + inputValue.value = effectiveVM; } } }); - -// Update handleSearchClick to set maintainFocusAfterRefresh -const handleSearchClick = () => { - if (props.showList) { - // When clicking the search input while list is open, always apply filtering - filterActive.value = true; - maintainFocusAfterRefresh.value = true; // Maintain focus after refresh - - // If there's a saved search, make sure it's applied to the model - if (lastSearchQuery.value && !props.modelValue) { - emit('update:modelValue', lastSearchQuery.value); - } - - // Force filter application - applyFilter(); - } -}; - -// Add a method to force focus on the search input -const focusSearchInput = () => { - nextTick(() => { - const searchInput = document.querySelector('.search-input'); - if (searchInput) { - searchInput.focus(); - } - }); -};