# Pull Request Template ## Description This PR fixes a few issues in Global Search and removes a non-functional control. * Fixes an issue in Firefox where characters could be dropped while typing in the search input. The search now uses the latest input value directly, preventing searches from running one character behind. * Removes a stale query sync in `SearchHeader` that could overwrite recently typed characters during the debounce window, causing the input to appear out of sync. * Fixes advanced search filters being removed from the URL on page reload. The search page now waits for account data to load before parsing URL parameters, ensuring agent, inbox, and date range filters are preserved. * Removes the non-functional "Sort by relevance" button from the search tabs bar, as it was disabled and had no effect. Fixes https://github.com/chatwoot/chatwoot/issues/14684 [CW-7305](https://linear.app/chatwoot/issue/CW-7305/global-search-drops-characters-while-typing-in-firefox-query-truncated) [CW-7370](https://linear.app/chatwoot/issue/CW-7370/remove-non-functional-relevance-placeholder-button-from-global-search) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screencast **Before** https://github.com/user-attachments/assets/48d72a4e-20f4-4f24-91f4-2c9a9c065eba **After** https://github.com/user-attachments/assets/0e41a803-b4fd-410d-a739-549f72d418d6 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
140 lines
3.8 KiB
Vue
140 lines
3.8 KiB
Vue
<script setup>
|
|
import { ref, useTemplateRef, onMounted, onUnmounted } from 'vue';
|
|
import { debounce } from '@chatwoot/utils';
|
|
import RecentSearches from './RecentSearches.vue';
|
|
|
|
const emit = defineEmits(['search', 'selectRecentSearch']);
|
|
|
|
const searchQuery = defineModel({
|
|
type: String,
|
|
default: '',
|
|
});
|
|
const isInputFocused = ref(false);
|
|
const showRecentSearches = ref(false);
|
|
const searchInput = useTemplateRef('searchInput');
|
|
const recentSearchesRef = useTemplateRef('recentSearchesRef');
|
|
|
|
const handler = e => {
|
|
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
|
|
e.preventDefault();
|
|
searchInput.value.focus();
|
|
} else if (e.key === 'Escape' && document.activeElement.tagName === 'INPUT') {
|
|
e.preventDefault();
|
|
searchInput.value.blur();
|
|
}
|
|
};
|
|
|
|
const debouncedEmit = debounce(
|
|
value =>
|
|
emit('search', value.length > 1 || value.match(/^[0-9]+$/) ? value : ''),
|
|
500
|
|
);
|
|
|
|
const onInput = e => {
|
|
// Use the DOM value, not searchQuery.value: the defineModel ref updates a tick
|
|
// later, so reading it back here lags one character behind.
|
|
const value = e.target.value;
|
|
debouncedEmit(value);
|
|
|
|
if (value.trim()) {
|
|
showRecentSearches.value = false;
|
|
} else if (isInputFocused.value) {
|
|
showRecentSearches.value = true;
|
|
}
|
|
};
|
|
|
|
const onFocus = () => {
|
|
isInputFocused.value = true;
|
|
if (!searchQuery.value.trim()) {
|
|
showRecentSearches.value = true;
|
|
}
|
|
};
|
|
|
|
const onBlur = () => {
|
|
isInputFocused.value = false;
|
|
showRecentSearches.value = false;
|
|
};
|
|
|
|
const onSelectRecentSearch = query => {
|
|
searchQuery.value = query;
|
|
emit('selectRecentSearch', query);
|
|
showRecentSearches.value = false;
|
|
searchInput.value.focus();
|
|
};
|
|
|
|
const addToRecentSearches = query => {
|
|
if (recentSearchesRef.value) {
|
|
recentSearchesRef.value.addRecentSearch(query);
|
|
}
|
|
};
|
|
|
|
defineExpose({
|
|
addToRecentSearches,
|
|
});
|
|
|
|
onMounted(() => {
|
|
searchInput.value.focus();
|
|
document.addEventListener('keydown', handler);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('keydown', handler);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="rounded-xl transition-[border-bottom] duration-[0.2s] ease-[ease-in-out] relative flex items-start flex-col border border-solid bg-n-solid-1 divide-y divide-n-strong"
|
|
:class="{
|
|
'border-n-brand': isInputFocused,
|
|
'border-n-strong': !isInputFocused,
|
|
}"
|
|
>
|
|
<div class="flex items-center w-full h-[3.25rem] px-4 gap-2">
|
|
<div class="flex items-center">
|
|
<fluent-icon
|
|
icon="search"
|
|
class="icon"
|
|
aria-hidden="true"
|
|
:class="{
|
|
'text-n-blue-11': isInputFocused,
|
|
'text-n-slate-10': !isInputFocused,
|
|
}"
|
|
/>
|
|
</div>
|
|
<input
|
|
ref="searchInput"
|
|
v-model="searchQuery"
|
|
type="search"
|
|
class="reset-base outline-none w-full m-0 bg-transparent border-transparent shadow-none text-n-slate-12 dark:text-n-slate-12 active:border-transparent active:shadow-none hover:border-transparent hover:shadow-none focus:border-transparent focus:shadow-none placeholder:text-n-slate-10 text-base"
|
|
:placeholder="$t('SEARCH.INPUT_PLACEHOLDER')"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
@input="onInput"
|
|
/>
|
|
<span class="text-sm text-n-slate-10 flex-shrink-0">
|
|
{{ $t('SEARCH.PLACEHOLDER_KEYBINDING') }}
|
|
</span>
|
|
</div>
|
|
|
|
<slot />
|
|
|
|
<div
|
|
class="transition-all duration-200 ease-out grid overflow-hidden w-full !border-t-0"
|
|
:class="
|
|
showRecentSearches
|
|
? 'grid-rows-[1fr] opacity-100'
|
|
: 'grid-rows-[0fr] opacity-0'
|
|
"
|
|
>
|
|
<div class="overflow-hidden w-full">
|
|
<RecentSearches
|
|
ref="recentSearchesRef"
|
|
@select-search="onSelectRecentSearch"
|
|
@clear-all="showRecentSearches = false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|