Files
chatwoot/app/javascript/dashboard/modules/search/components/SearchInput.vue
T
Sivin VargheseandGitHub ac15339456 fix: Resolve Firefox input issues and persist advanced filters (#14781)
# 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
2026-06-19 10:46:47 +04:00

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>