Fix search bar behaviour (step 1)

This commit is contained in:
Joshua Boniface 2025-03-02 14:27:28 -05:00
parent 4e5274c6f0
commit e588df9fca

View File

@ -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)"
>
<div v-if="selectedVM === vm.name || vmFromUrl === vm.name" class="active-indicator"></div>
<div class="vm-item-content">
@ -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();
}
});
};
</script>
<style scoped>