feat: Splits search api by resources to improve query time [cw-47] (#6942)

* feat: Splits search api by resources to improve query time

* Review fixes

* Spacing fixes

* Update app/javascript/dashboard/modules/search/components/SearchView.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Review fixes

* Refactor searchview

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Nithin David Thomas
2023-04-25 17:59:38 +05:30
committed by GitHub
parent 5600b518ac
commit 402428fb4d
9 changed files with 222 additions and 78 deletions

View File

@@ -13,6 +13,30 @@ class SearchAPI extends ApiClient {
}, },
}); });
} }
contacts({ q }) {
return axios.get(`${this.url}/contacts`, {
params: {
q,
},
});
}
conversations({ q }) {
return axios.get(`${this.url}/conversations`, {
params: {
q,
},
});
}
messages({ q }) {
return axios.get(`${this.url}/messages`, {
params: {
q,
},
});
}
} }
export default new SearchAPI(); export default new SearchAPI();

View File

@@ -16,6 +16,7 @@
:title="$t('SEARCH.PLACEHOLDER_KEYBINDING')" :title="$t('SEARCH.PLACEHOLDER_KEYBINDING')"
:show-close="false" :show-close="false"
small small
class="helper-label"
/> />
</div> </div>
</template> </template>
@@ -101,4 +102,8 @@ export default {
color: var(--s-400); color: var(--s-400);
} }
} }
.helper-label {
margin: 0;
}
</style> </style>

View File

@@ -3,6 +3,8 @@
:title="$t('SEARCH.SECTION.CONTACTS')" :title="$t('SEARCH.SECTION.CONTACTS')"
:empty="!contacts.length" :empty="!contacts.length"
:query="query" :query="query"
:show-title="showTitle"
:is-fetching="isFetching"
> >
<ul class="search-list"> <ul class="search-list">
<li v-for="contact in contacts" :key="contact.id"> <li v-for="contact in contacts" :key="contact.id">
@@ -39,6 +41,14 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
isFetching: {
type: Boolean,
default: false,
},
showTitle: {
type: Boolean,
default: true,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({

View File

@@ -3,6 +3,8 @@
:title="$t('SEARCH.SECTION.CONVERSATIONS')" :title="$t('SEARCH.SECTION.CONVERSATIONS')"
:empty="!conversations.length" :empty="!conversations.length"
:query="query" :query="query"
:show-title="showTitle || isFetching"
:is-fetching="isFetching"
> >
<ul class="search-list"> <ul class="search-list">
<li v-for="conversation in conversations" :key="conversation.id"> <li v-for="conversation in conversations" :key="conversation.id">
@@ -37,6 +39,14 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
isFetching: {
type: Boolean,
default: false,
},
showTitle: {
type: Boolean,
default: true,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({

View File

@@ -3,6 +3,8 @@
:title="$t('SEARCH.SECTION.MESSAGES')" :title="$t('SEARCH.SECTION.MESSAGES')"
:empty="!messages.length" :empty="!messages.length"
:query="query" :query="query"
:show-title="showTitle"
:is-fetching="isFetching"
> >
<ul class="search-list"> <ul class="search-list">
<li v-for="message in messages" :key="message.id"> <li v-for="message in messages" :key="message.id">
@@ -45,6 +47,14 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
isFetching: {
type: Boolean,
default: false,
},
showTitle: {
type: Boolean,
default: true,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({

View File

@@ -1,10 +1,11 @@
<template> <template>
<section class="result-section"> <section class="result-section">
<div class="header"> <div v-if="showTitle" class="header">
<h3 class="text-block-title">{{ title }}</h3> <h3 class="text-block-title">{{ title }}</h3>
</div> </div>
<slot /> <woot-loading-state v-if="isFetching" :message="'Searching'" />
<div v-if="empty" class="empty"> <slot v-else />
<div v-if="empty && !isFetching" class="empty">
<fluent-icon icon="info" size="16px" class="icon" /> <fluent-icon icon="info" size="16px" class="icon" />
<p class="empty-state__text"> <p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE', { item: titleCase, query }) }} {{ $t('SEARCH.EMPTY_STATE', { item: titleCase, query }) }}
@@ -28,6 +29,14 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
showTitle: {
type: Boolean,
default: true,
},
isFetching: {
type: Boolean,
default: true,
},
}, },
computed: { computed: {
titleCase() { titleCase() {
@@ -39,7 +48,7 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.result-section { .result-section {
margin-bottom: var(--space-normal); margin: var(--space-small) 0;
} }
.search-list { .search-list {
list-style: none; list-style: none;
@@ -60,7 +69,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); margin: 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

@@ -21,39 +21,44 @@
/> />
</header> </header>
<div class="search-results"> <div class="search-results">
<woot-loading-state v-if="uiFlags.isFetching" :message="'Searching'" /> <div v-if="all.length">
<div v-else> <search-result-contacts-list
<div v-if="all.length"> v-if="filterContacts"
<search-result-contacts-list :is-fetching="uiFlags.contact.isFetching"
v-if="filterContacts" :contacts="contacts"
:contacts="contacts" :query="query"
:query="query" :show-title="isSelectedTabAll"
/> />
<search-result-messages-list
v-if="filterMessages" <search-result-messages-list
:messages="messages" v-if="filterMessages"
:query="query" :is-fetching="uiFlags.message.isFetching"
/> :messages="messages"
<search-result-conversations-list :query="query"
v-if="filterConversations" :show-title="isSelectedTabAll"
:conversations="conversations" />
:query="query"
/> <search-result-conversations-list
</div> v-if="filterConversations"
<div v-else-if="showEmptySearchResults && !all.length" class="empty"> :is-fetching="uiFlags.conversation.isFetching"
<fluent-icon icon="info" size="16px" class="icon" /> :conversations="conversations"
<p class="empty-state__text"> :query="query"
{{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }} :show-title="isSelectedTabAll"
</p> />
</div> </div>
<div v-else class="empty text-center"> <div v-else-if="showEmptySearchResults && !all.length" class="empty">
<p class="text-center margin-bottom-0"> <fluent-icon icon="info" size="16px" class="icon" />
<fluent-icon icon="search" size="24px" class="icon" /> <p class="empty-state__text">
</p> {{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
<p class="empty-state__text"> </p>
{{ $t('SEARCH.EMPTY_STATE_DEFAULT') }} </div>
</p> <div v-else class="empty text-center">
</div> <p class="text-center margin-bottom-0">
<fluent-icon icon="search" size="24px" class="icon" />
</p>
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE_DEFAULT') }}
</p>
</div> </div>
</div> </div>
</section> </section>
@@ -66,7 +71,6 @@ import SearchTabs from './SearchTabs.vue';
import SearchResultConversationsList from './SearchResultConversationsList.vue'; import SearchResultConversationsList from './SearchResultConversationsList.vue';
import SearchResultMessagesList from './SearchResultMessagesList.vue'; import SearchResultMessagesList from './SearchResultMessagesList.vue';
import SearchResultContactsList from './SearchResultContactsList.vue'; import SearchResultContactsList from './SearchResultContactsList.vue';
import { isEmptyObject } from 'dashboard/helper/commons.js';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -89,47 +93,40 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
fullSearchRecords: 'conversationSearch/getFullSearchRecords', contactRecords: 'conversationSearch/getContactRecords',
conversationRecords: 'conversationSearch/getConversationRecords',
messageRecords: 'conversationSearch/getMessageRecords',
uiFlags: 'conversationSearch/getUIFlags', uiFlags: 'conversationSearch/getUIFlags',
}), }),
contacts() { contacts() {
if (this.fullSearchRecords.contacts) { return this.contactRecords.map(contact => ({
return this.fullSearchRecords.contacts.map(contact => ({ ...contact,
...contact, type: 'contact',
type: 'contact', }));
}));
}
return [];
}, },
conversations() { conversations() {
if (this.fullSearchRecords.conversations) { return this.conversationRecords.map(conversation => ({
return this.fullSearchRecords.conversations.map(conversation => ({ ...conversation,
...conversation, type: 'conversation',
type: 'conversation', }));
}));
}
return [];
}, },
messages() { messages() {
if (this.fullSearchRecords.messages) { return this.messageRecords.map(message => ({
return this.fullSearchRecords.messages.map(message => ({ ...message,
...message, type: 'message',
type: 'message', }));
}));
}
return [];
}, },
all() { all() {
return [...this.contacts, ...this.conversations, ...this.messages]; return [...this.contacts, ...this.conversations, ...this.messages];
}, },
filterContacts() { filterContacts() {
return this.selectedTab === 'contacts' || this.selectedTab === 'all'; return this.selectedTab === 'contacts' || this.isSelectedTabAll;
}, },
filterConversations() { filterConversations() {
return this.selectedTab === 'conversations' || this.selectedTab === 'all'; return this.selectedTab === 'conversations' || this.isSelectedTabAll;
}, },
filterMessages() { filterMessages() {
return this.selectedTab === 'messages' || this.selectedTab === 'all'; return this.selectedTab === 'messages' || this.isSelectedTabAll;
}, },
totalSearchResultsCount() { totalSearchResultsCount() {
return ( return (
@@ -162,10 +159,12 @@ export default {
}, },
showEmptySearchResults() { showEmptySearchResults() {
return ( return (
this.totalSearchResultsCount === 0 && this.totalSearchResultsCount === 0 && this.uiFlags.isSearchCompleted
!isEmptyObject(this.fullSearchRecords)
); );
}, },
isSelectedTabAll() {
return this.selectedTab === 'all';
},
}, },
beforeDestroy() { beforeDestroy() {
this.query = ''; this.query = '';

View File

@@ -2,9 +2,15 @@ import SearchAPI from '../../api/search';
import types from '../mutation-types'; import types from '../mutation-types';
export const initialState = { export const initialState = {
records: [], records: [],
fullSearchRecords: {}, contactRecords: [],
conversationRecords: [],
messageRecords: [],
uiFlags: { uiFlags: {
isFetching: false, isFetching: false,
isSearchCompleted: false,
contact: { isFetching: false },
conversation: { isFetching: false },
message: { isFetching: false },
}, },
}; };
@@ -12,8 +18,14 @@ export const getters = {
getConversations(state) { getConversations(state) {
return state.records; return state.records;
}, },
getFullSearchRecords(state) { getContactRecords(state) {
return state.fullSearchRecords; return state.contactRecords;
},
getConversationRecords(state) {
return state.conversationRecords;
},
getMessageRecords(state) {
return state.messageRecords;
}, },
getUIFlags(state) { getUIFlags(state) {
return state.uiFlags; return state.uiFlags;
@@ -40,23 +52,67 @@ export const actions = {
}); });
} }
}, },
async fullSearch({ commit }, { q }) { async fullSearch({ commit, dispatch }, { q }) {
commit(types.FULL_SEARCH_SET, []);
if (!q) { if (!q) {
return; return;
} }
commit(types.FULL_SEARCH_SET_UI_FLAG, { isFetching: true }); commit(types.FULL_SEARCH_SET_UI_FLAG, {
isFetching: true,
isSearchCompleted: false,
});
try { try {
const { data } = await SearchAPI.get({ q }); dispatch('contactSearch', { q });
commit(types.FULL_SEARCH_SET, data.payload); dispatch('conversationSearch', { q });
dispatch('messageSearch', { q });
} catch (error) { } catch (error) {
// Ignore error // Ignore error
} finally { } finally {
commit(types.FULL_SEARCH_SET_UI_FLAG, { isFetching: false }); commit(types.FULL_SEARCH_SET_UI_FLAG, {
isFetching: false,
isSearchCompleted: true,
});
}
},
async contactSearch({ commit }, { q }) {
commit(types.CONTACT_SEARCH_SET, []);
commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true });
try {
const { data } = await SearchAPI.contacts({ q });
commit(types.CONTACT_SEARCH_SET, data.payload.contacts);
} catch (error) {
// Ignore error
} finally {
commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false });
}
},
async conversationSearch({ commit }, { q }) {
commit(types.CONVERSATION_SEARCH_SET, []);
commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true });
try {
const { data } = await SearchAPI.conversations({ q });
commit(types.CONVERSATION_SEARCH_SET, data.payload.conversations);
} catch (error) {
// Ignore error
} finally {
commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false });
}
},
async messageSearch({ commit }, { q }) {
commit(types.MESSAGE_SEARCH_SET, []);
commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true });
try {
const { data } = await SearchAPI.messages({ q });
commit(types.MESSAGE_SEARCH_SET, data.payload.messages);
} catch (error) {
// Ignore error
} finally {
commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false });
} }
}, },
async clearSearchResults({ commit }) { async clearSearchResults({ commit }) {
commit(types.FULL_SEARCH_SET, {}); commit(types.MESSAGE_SEARCH_SET, []);
commit(types.CONVERSATION_SEARCH_SET, []);
commit(types.CONTACT_SEARCH_SET, []);
}, },
}; };
@@ -64,8 +120,14 @@ export const mutations = {
[types.SEARCH_CONVERSATIONS_SET](state, records) { [types.SEARCH_CONVERSATIONS_SET](state, records) {
state.records = records; state.records = records;
}, },
[types.FULL_SEARCH_SET](state, records) { [types.CONTACT_SEARCH_SET](state, records) {
state.fullSearchRecords = records; state.contactRecords = records;
},
[types.CONVERSATION_SEARCH_SET](state, records) {
state.conversationRecords = records;
},
[types.MESSAGE_SEARCH_SET](state, records) {
state.messageRecords = records;
}, },
[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) { [types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) {
state.uiFlags = { ...state.uiFlags, ...uiFlags }; state.uiFlags = { ...state.uiFlags, ...uiFlags };
@@ -73,6 +135,15 @@ export const mutations = {
[types.FULL_SEARCH_SET_UI_FLAG](state, uiFlags) { [types.FULL_SEARCH_SET_UI_FLAG](state, uiFlags) {
state.uiFlags = { ...state.uiFlags, ...uiFlags }; state.uiFlags = { ...state.uiFlags, ...uiFlags };
}, },
[types.CONTACT_SEARCH_SET_UI_FLAG](state, uiFlags) {
state.uiFlags.contact = { ...state.uiFlags.contact, ...uiFlags };
},
[types.CONVERSATION_SEARCH_SET_UI_FLAG](state, uiFlags) {
state.uiFlags.conversation = { ...state.uiFlags.conversation, ...uiFlags };
},
[types.MESSAGE_SEARCH_SET_UI_FLAG](state, uiFlags) {
state.uiFlags.message = { ...state.uiFlags.message, ...uiFlags };
},
}; };
export default { export default {

View File

@@ -269,6 +269,12 @@ export default {
// Full Search // Full Search
FULL_SEARCH_SET: 'FULL_SEARCH_SET', FULL_SEARCH_SET: 'FULL_SEARCH_SET',
CONTACT_SEARCH_SET: 'CONTACT_SEARCH_SET',
CONTACT_SEARCH_SET_UI_FLAG: 'CONTACT_SEARCH_SET_UI_FLAG',
CONVERSATION_SEARCH_SET: 'CONVERSATION_SEARCH_SET',
CONVERSATION_SEARCH_SET_UI_FLAG: 'CONVERSATION_SEARCH_SET_UI_FLAG',
MESSAGE_SEARCH_SET: 'MESSAGE_SEARCH_SET',
MESSAGE_SEARCH_SET_UI_FLAG: 'MESSAGE_SEARCH_SET_UI_FLAG',
FULL_SEARCH_SET_UI_FLAG: 'FULL_SEARCH_SET_UI_FLAG', FULL_SEARCH_SET_UI_FLAG: 'FULL_SEARCH_SET_UI_FLAG',
SET_CONVERSATION_PARTICIPANTS_UI_FLAG: SET_CONVERSATION_PARTICIPANTS_UI_FLAG:
'SET_CONVERSATION_PARTICIPANTS_UI_FLAG', 'SET_CONVERSATION_PARTICIPANTS_UI_FLAG',