feat: Search improvements for conversations (#6645)

* feat: Shows search as a popover

* Reverts search from popover to page

* Fixes review comments on usability

* Fixes keyboard navigation issues
This commit is contained in:
Nithin David Thomas
2023-03-14 13:09:43 +05:30
committed by GitHub
parent da76537011
commit cae3ac94cd
10 changed files with 203 additions and 98 deletions

View File

@@ -14,7 +14,8 @@
"EMPTY_STATE": "No %{item} found for query '%{query}'", "EMPTY_STATE": "No %{item} found for query '%{query}'",
"EMPTY_STATE_FULL": "No results found for query '%{query}'", "EMPTY_STATE_FULL": "No results found for query '%{query}'",
"PLACEHOLDER_KEYBINDING": "/ to focus", "PLACEHOLDER_KEYBINDING": "/ to focus",
"INPUT_PLACEHOLDER": "Search message content, contact name, email or phone or conversations", "INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
"BOT_LABEL": "Bot", "BOT_LABEL": "Bot",
"READ_MORE": "Read more", "READ_MORE": "Read more",
"WROTE": "wrote:" "WROTE": "wrote:"

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="input-container"> <div class="input-container" :class="{ 'is-focused': isInputFocused }">
<div class="icon-container"> <div class="icon-container">
<fluent-icon icon="search" class="icon" aria-hidden="true" /> <fluent-icon icon="search" class="icon" aria-hidden="true" />
</div> </div>
@@ -8,11 +8,15 @@
type="search" type="search"
:placeholder="$t('SEARCH.INPUT_PLACEHOLDER')" :placeholder="$t('SEARCH.INPUT_PLACEHOLDER')"
:value="searchQuery" :value="searchQuery"
@focus="onFocus"
@blur="onBlur"
@input="debounceSearch" @input="debounceSearch"
/> />
<div class="key-binding"> <woot-label
<span>{{ $t('SEARCH.PLACEHOLDER_KEYBINDING') }}</span> :title="$t('SEARCH.PLACEHOLDER_KEYBINDING')"
</div> :show-close="false"
small
/>
</div> </div>
</template> </template>
@@ -21,6 +25,7 @@ export default {
data() { data() {
return { return {
searchQuery: '', searchQuery: '',
isInputFocused: false,
}; };
}, },
mounted() { mounted() {
@@ -35,6 +40,12 @@ export default {
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') { if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
e.preventDefault(); e.preventDefault();
this.$refs.searchInput.focus(); this.$refs.searchInput.focus();
} else if (
e.key === 'Escape' &&
document.activeElement.tagName === 'INPUT'
) {
e.preventDefault();
this.$refs.searchInput.blur();
} }
}, },
debounceSearch(e) { debounceSearch(e) {
@@ -43,56 +54,50 @@ export default {
this.debounce = setTimeout(async () => { this.debounce = setTimeout(async () => {
if (this.searchQuery.length > 2 || this.searchQuery.match(/^[0-9]+$/)) { if (this.searchQuery.length > 2 || this.searchQuery.match(/^[0-9]+$/)) {
this.$emit('search', this.searchQuery); this.$emit('search', this.searchQuery);
} else {
this.$emit('search', '');
} }
}, 500); }, 500);
}, },
onFocus() {
this.isInputFocused = true;
},
onBlur() {
this.isInputFocused = false;
},
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import 'app/javascript/dashboard/assets/scss/_mixins.scss';
.input-container { .input-container {
position: relative; position: relative;
border-radius: var(--border-radius-normal); display: flex;
align-items: center;
padding: var(--space-small) var(--space-normal);
border-bottom: 1px solid var(--s-100);
transition: border-bottom 0.2s ease-in-out;
input[type='search'] { input[type='search'] {
@include ghost-input;
width: 100%; width: 100%;
padding-left: calc(var(--space-large) + var(--space-small)); margin: 0;
margin-bottom: 0; }
padding-right: var(--space-mega);
&:focus { &.is-focused {
.icon { border-bottom: 1px solid var(--w-100);
color: var(--w-500) !important;
} .icon {
color: var(--w-400);
} }
} }
} }
.icon-container { .icon-container {
padding-left: var(--space-small);
display: flex; display: flex;
align-items: center; align-items: center;
top: 0;
bottom: 0;
left: 0;
position: absolute;
.icon { .icon {
color: var(--s-400); color: var(--s-400);
} }
} }
.key-binding {
position: absolute;
top: 0;
bottom: 0;
right: 0;
padding: var(--space-small) var(--space-small) 0 var(--space-small);
span {
color: var(--s-400);
font-weight: var(--font-weight-medium);
font-size: calc(var(--space-slab) + var(--space-micro));
padding: 0 var(--space-small);
border: 1px solid var(--s-100);
border-radius: var(--border-radius-normal);
display: inline-flex;
align-items: center;
}
}
</style> </style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="search-input-box">
<woot-button
class="hollow"
size="small"
color-scheme="secondary"
is-expanded
>
<div class="search-input">
<fluent-icon icon="search" size="14px" class="search--icon" />
<span class="text-ellipsis search-placeholder">
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
</span>
</div>
</woot-button>
</div>
</template>
<script>
export default {
props: {
tabs: {
type: Array,
default: () => [],
},
},
data() {
return {};
},
methods: {},
};
</script>
<style lang="scss" scoped>
.search-input-box {
padding: var(--space-small);
}
.search--icon {
flex-shrink: 0;
color: var(--s-500);
}
.search-placeholder {
color: var(--s-500);
}
.search-input {
display: flex;
justify-content: center;
align-items: center;
gap: var(--space-smaller);
}
</style>

View File

@@ -64,7 +64,7 @@ export default {
} }
} }
.contact-details { .contact-details {
margin-left: var(--space-normal); margin-left: var(--space-small);
} }
.name { .name {
margin: 0; margin: 0;

View File

@@ -113,7 +113,7 @@ export default {
margin-left: var(--space-smaller); margin-left: var(--space-smaller);
} }
.conversation-details { .conversation-details {
margin-left: var(--space-normal); margin-left: var(--space-small);
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<section> <section class="result-section">
<div class="label-container"> <div class="header">
<h3 class="text-block-title">{{ title }}</h3> <h3 class="text-block-title">{{ title }}</h3>
</div> </div>
<slot /> <slot />
@@ -38,17 +38,21 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.result-section {
margin-bottom: var(--space-normal);
}
.search-list { .search-list {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: var(--spacing-normal) 0; padding: var(--spacing-normal) 0;
} }
.label-container { .header {
position: sticky; position: sticky;
top: 0; top: 0;
padding: var(--space-small) 0; padding: var(--space-small);
z-index: 50; z-index: 50;
background: var(--white); background: var(--white);
margin-bottom: var(--space-micro);
} }
.empty { .empty {
@@ -56,6 +60,7 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--space-medium) var(--space-normal); padding: var(--space-medium) var(--space-normal);
margin: 0 var(--space-small);
background: var(--s-25); background: var(--s-25);
border-radius: var(--border-radius-medium); border-radius: var(--border-radius-medium);
.icon { .icon {

View File

@@ -34,7 +34,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tab-container { .tab-container {
margin-left: var(--space-minus-normal); margin-top: var(--space-smaller);
margin-top: var(--space-small); border-bottom: 1px solid var(--s-50);
} }
</style> </style>

View File

@@ -1,46 +1,63 @@
<template> <template>
<section class="search-root"> <div class="width-100">
<woot-button <div class="page-header">
color-scheme="secondary" <woot-button
size="large" icon="chevron-left"
icon="dismiss" variant="smooth"
variant="smooth" size="small "
class="modal--close" class="back-button"
@click="onBack" @click="onBack"
/> >
<header> {{ $t('GENERAL_SETTINGS.BACK') }}
<search-header @search="search" /> </woot-button>
<search-tabs :tabs="tabs" @tab-change="tab => (selectedTab = tab)" /> </div>
</header> <section class="search-root">
<div class="search-results"> <header>
<woot-loading-state v-if="uiFlags.isFetching" :message="'Searching'" /> <search-header @search="onSearch" />
<div v-else> <search-tabs
<div v-if="all.length"> v-if="query"
<search-result-contacts-list :tabs="tabs"
v-if="filterContacts" @tab-change="tab => (selectedTab = tab)"
:contacts="contacts" />
:query="query" </header>
/> <div class="search-results">
<search-result-messages-list <woot-loading-state v-if="uiFlags.isFetching" :message="'Searching'" />
v-if="filterMessages" <div v-else>
:messages="messages" <div v-if="all.length">
:query="query" <search-result-contacts-list
/> v-if="filterContacts"
<search-result-conversations-list :contacts="contacts"
v-if="filterConversations" :query="query"
:conversations="conversations" />
:query="query" <search-result-messages-list
/> v-if="filterMessages"
</div> :messages="messages"
<div v-else-if="showEmptySearchResults" class="empty"> :query="query"
<fluent-icon icon="info" size="16px" class="icon" /> />
<p class="empty-state__text"> <search-result-conversations-list
{{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }} v-if="filterConversations"
</p> :conversations="conversations"
:query="query"
/>
</div>
<div v-else-if="showEmptySearchResults && !all.length" class="empty">
<fluent-icon icon="info" size="16px" class="icon" />
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
</p>
</div>
<div v-else class="empty text-center">
<p class="text-center">
<fluent-icon icon="search" size="24px" class="icon" />
</p>
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE_DEFAULT') }}
</p>
</div>
</div> </div>
</div> </div>
</div> </section>
</section> </div>
</template> </template>
<script> <script>
@@ -51,6 +68,7 @@ import SearchResultMessagesList from './SearchResultMessagesList.vue';
import SearchResultContactsList from './SearchResultContactsList.vue'; import SearchResultContactsList from './SearchResultContactsList.vue';
import { isEmptyObject } from 'dashboard/helper/commons.js'; import { isEmptyObject } from 'dashboard/helper/commons.js';
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default { export default {
@@ -61,12 +79,14 @@ export default {
SearchResultConversationsList, SearchResultConversationsList,
SearchResultMessagesList, SearchResultMessagesList,
}, },
mixins: [clickaway],
data() { data() {
return { return {
selectedTab: 'all', selectedTab: 'all',
query: '', query: '',
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
fullSearchRecords: 'conversationSearch/getFullSearchRecords', fullSearchRecords: 'conversationSearch/getFullSearchRecords',
@@ -147,48 +167,68 @@ export default {
); );
}, },
}, },
beforeDestroy() {
this.query = '';
this.$store.dispatch('conversationSearch/clearSearchResults');
},
mounted() {
this.$store.dispatch('conversationSearch/clearSearchResults');
},
methods: { methods: {
search(q) { onSearch(q) {
this.query = q; this.query = q;
if (!q) {
this.$store.dispatch('conversationSearch/clearSearchResults');
return;
}
this.$track(CONVERSATION_EVENTS.SEARCH_CONVERSATION); this.$track(CONVERSATION_EVENTS.SEARCH_CONVERSATION);
this.$store.dispatch('conversationSearch/fullSearch', { q }); this.$store.dispatch('conversationSearch/fullSearch', { q });
}, },
onBack() { onBack() {
this.$router.push({ name: 'home' }); if (window.history.length > 2) {
this.$router.go(-1);
} else {
this.$router.push({ name: 'home' });
}
}, },
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.page-header {
display: flex;
padding: var(--space-normal);
}
.search-root { .search-root {
max-width: 800px;
width: 100%;
margin: 0 auto; margin: 0 auto;
height: 100%; max-width: 64rem;
min-height: 48rem;
width: 100%;
height: fit-content;
box-shadow: var(--shadow);
display: flex; display: flex;
position: relative; position: relative;
padding: var(--space-normal);
flex-direction: column; flex-direction: column;
background: white;
border-radius: var(--border-radius-large);
margin-top: var(--space-large);
border-top: 1px solid var(--s-25);
.search-results { .search-results {
flex-grow: 1; flex-grow: 1;
height: 100%; height: 100%;
max-height: 80vh;
overflow-y: auto; overflow-y: auto;
margin-top: var(--space-normal); padding: 0 var(--space-small);
} }
} }
.modal--close {
position: fixed;
right: var(--space-small);
top: var(--space-small);
}
.empty { .empty {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--space-medium) var(--space-normal); padding: var(--space-medium) var(--space-normal);
background: var(--s-25);
border-radius: var(--border-radius-medium); border-radius: var(--border-radius-medium);
.icon { .icon {
color: var(--s-500); color: var(--s-500);
@@ -196,7 +236,7 @@ export default {
.empty-state__text { .empty-state__text {
text-align: center; text-align: center;
color: var(--s-500); color: var(--s-500);
margin: 0 var(--space-small); margin: var(--space-small);
} }
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<div class="search-wrap"> <div class="search-wrap">
<div class="search" :class="{ 'is-active': showSearchBox }"> <div class="search" :class="{ 'is-active': showSearchBox }">
<woot-sidemenu-icon /> <woot-sidemenu-icon />
<router-link :to="searchUrl" class="search--link" replace> <router-link :to="searchUrl" class="search--link">
<div class="icon"> <div class="icon">
<fluent-icon icon="search" class="search--icon" size="16" /> <fluent-icon icon="search" class="search--icon" size="16" />
</div> </div>

View File

@@ -55,6 +55,9 @@ export const actions = {
commit(types.FULL_SEARCH_SET_UI_FLAG, { isFetching: false }); commit(types.FULL_SEARCH_SET_UI_FLAG, { isFetching: false });
} }
}, },
async clearSearchResults({ commit }) {
commit(types.FULL_SEARCH_SET, {});
},
}; };
export const mutations = { export const mutations = {