diff --git a/app/javascript/dashboard/modules/search/components/SearchTabs.vue b/app/javascript/dashboard/modules/search/components/SearchTabs.vue
index 656f5b7df..6e5174b5c 100644
--- a/app/javascript/dashboard/modules/search/components/SearchTabs.vue
+++ b/app/javascript/dashboard/modules/search/components/SearchTabs.vue
@@ -1,39 +1,38 @@
-
-
+
-
-
diff --git a/app/javascript/dashboard/modules/search/components/SearchView.vue b/app/javascript/dashboard/modules/search/components/SearchView.vue
index aa9a583ee..1b0a9e4d7 100644
--- a/app/javascript/dashboard/modules/search/components/SearchView.vue
+++ b/app/javascript/dashboard/modules/search/components/SearchView.vue
@@ -1,12 +1,9 @@
-
-
-
-
+
+
-
-
-
- (selectedTab = tab)"
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ (selectedTab = tab)"
+ />
-
-
-
- {{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
-
-
-
-
-
-
-
- {{ $t('SEARCH.EMPTY_STATE_DEFAULT') }}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('SEARCH.EMPTY_STATE_FULL', { query }) }}
+
+
+
+
+
+
+
+ {{ t('SEARCH.EMPTY_STATE_DEFAULT') }}
+
+
diff --git a/app/javascript/dashboard/store/modules/conversationSearch.js b/app/javascript/dashboard/store/modules/conversationSearch.js
index 210081fa1..b4d540fbe 100644
--- a/app/javascript/dashboard/store/modules/conversationSearch.js
+++ b/app/javascript/dashboard/store/modules/conversationSearch.js
@@ -75,11 +75,10 @@ export const actions = {
});
}
},
- async contactSearch({ commit }, { q }) {
- commit(types.CONTACT_SEARCH_SET, []);
+ async contactSearch({ commit }, { q, page = 1 }) {
commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true });
try {
- const { data } = await SearchAPI.contacts({ q });
+ const { data } = await SearchAPI.contacts({ q, page });
commit(types.CONTACT_SEARCH_SET, data.payload.contacts);
} catch (error) {
// Ignore error
@@ -87,11 +86,10 @@ export const actions = {
commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false });
}
},
- async conversationSearch({ commit }, { q }) {
- commit(types.CONVERSATION_SEARCH_SET, []);
+ async conversationSearch({ commit }, { q, page = 1 }) {
commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true });
try {
- const { data } = await SearchAPI.conversations({ q });
+ const { data } = await SearchAPI.conversations({ q, page });
commit(types.CONVERSATION_SEARCH_SET, data.payload.conversations);
} catch (error) {
// Ignore error
@@ -99,11 +97,10 @@ export const actions = {
commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false });
}
},
- async messageSearch({ commit }, { q }) {
- commit(types.MESSAGE_SEARCH_SET, []);
+ async messageSearch({ commit }, { q, page = 1 }) {
commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true });
try {
- const { data } = await SearchAPI.messages({ q });
+ const { data } = await SearchAPI.messages({ q, page });
commit(types.MESSAGE_SEARCH_SET, data.payload.messages);
} catch (error) {
// Ignore error
@@ -112,9 +109,7 @@ export const actions = {
}
},
async clearSearchResults({ commit }) {
- commit(types.MESSAGE_SEARCH_SET, []);
- commit(types.CONVERSATION_SEARCH_SET, []);
- commit(types.CONTACT_SEARCH_SET, []);
+ commit(types.CLEAR_SEARCH_RESULTS);
},
};
@@ -123,13 +118,13 @@ export const mutations = {
state.records = records;
},
[types.CONTACT_SEARCH_SET](state, records) {
- state.contactRecords = records;
+ state.contactRecords = [...state.contactRecords, ...records];
},
[types.CONVERSATION_SEARCH_SET](state, records) {
- state.conversationRecords = records;
+ state.conversationRecords = [...state.conversationRecords, ...records];
},
[types.MESSAGE_SEARCH_SET](state, records) {
- state.messageRecords = records;
+ state.messageRecords = [...state.messageRecords, ...records];
},
[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) {
state.uiFlags = { ...state.uiFlags, ...uiFlags };
@@ -146,6 +141,11 @@ export const mutations = {
[types.MESSAGE_SEARCH_SET_UI_FLAG](state, uiFlags) {
state.uiFlags.message = { ...state.uiFlags.message, ...uiFlags };
},
+ [types.CLEAR_SEARCH_RESULTS](state) {
+ state.contactRecords = [];
+ state.conversationRecords = [];
+ state.messageRecords = [];
+ },
};
export default {
diff --git a/app/javascript/dashboard/store/modules/specs/conversationSearch/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversationSearch/actions.spec.js
index 0fdcae458..ebf6c0557 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationSearch/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/actions.spec.js
@@ -1,11 +1,19 @@
import { actions } from '../../conversationSearch';
import types from '../../../mutation-types';
import axios from 'axios';
+
const commit = vi.fn();
+const dispatch = vi.fn();
global.axios = axios;
vi.mock('axios');
describe('#actions', () => {
+ beforeEach(() => {
+ commit.mockClear();
+ dispatch.mockClear();
+ axios.get.mockClear();
+ });
+
describe('#get', () => {
it('sends correct actions if no query param is provided', () => {
actions.get({ commit }, { q: '' });
@@ -41,4 +49,111 @@ describe('#actions', () => {
]);
});
});
+
+ describe('#fullSearch', () => {
+ it('should not dispatch any actions if no query provided', async () => {
+ await actions.fullSearch({ commit, dispatch }, { q: '' });
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('should dispatch all search actions and set UI flags correctly', async () => {
+ await actions.fullSearch({ commit, dispatch }, { q: 'test' });
+
+ expect(commit.mock.calls).toEqual([
+ [
+ types.FULL_SEARCH_SET_UI_FLAG,
+ { isFetching: true, isSearchCompleted: false },
+ ],
+ [
+ types.FULL_SEARCH_SET_UI_FLAG,
+ { isFetching: false, isSearchCompleted: true },
+ ],
+ ]);
+
+ expect(dispatch).toHaveBeenCalledWith('contactSearch', { q: 'test' });
+ expect(dispatch).toHaveBeenCalledWith('conversationSearch', {
+ q: 'test',
+ });
+ expect(dispatch).toHaveBeenCalledWith('messageSearch', { q: 'test' });
+ });
+ });
+
+ describe('#contactSearch', () => {
+ it('should handle successful contact search', async () => {
+ axios.get.mockResolvedValue({
+ data: { payload: { contacts: [{ id: 1 }] } },
+ });
+
+ await actions.contactSearch({ commit }, { q: 'test', page: 1 });
+ expect(commit.mock.calls).toEqual([
+ [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.CONTACT_SEARCH_SET, [{ id: 1 }]],
+ [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+
+ it('should handle failed contact search', async () => {
+ axios.get.mockRejectedValue({});
+ await actions.contactSearch({ commit }, { q: 'test' });
+ expect(commit.mock.calls).toEqual([
+ [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+
+ describe('#conversationSearch', () => {
+ it('should handle successful conversation search', async () => {
+ axios.get.mockResolvedValue({
+ data: { payload: { conversations: [{ id: 1 }] } },
+ });
+
+ await actions.conversationSearch({ commit }, { q: 'test', page: 1 });
+ expect(commit.mock.calls).toEqual([
+ [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.CONVERSATION_SEARCH_SET, [{ id: 1 }]],
+ [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+
+ it('should handle failed conversation search', async () => {
+ axios.get.mockRejectedValue({});
+ await actions.conversationSearch({ commit }, { q: 'test' });
+ expect(commit.mock.calls).toEqual([
+ [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+
+ describe('#messageSearch', () => {
+ it('should handle successful message search', async () => {
+ axios.get.mockResolvedValue({
+ data: { payload: { messages: [{ id: 1 }] } },
+ });
+
+ await actions.messageSearch({ commit }, { q: 'test', page: 1 });
+ expect(commit.mock.calls).toEqual([
+ [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.MESSAGE_SEARCH_SET, [{ id: 1 }]],
+ [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+
+ it('should handle failed message search', async () => {
+ axios.get.mockRejectedValue({});
+ await actions.messageSearch({ commit }, { q: 'test' });
+ expect(commit.mock.calls).toEqual([
+ [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+
+ describe('#clearSearchResults', () => {
+ it('should commit clear search results mutation', () => {
+ actions.clearSearchResults({ commit });
+ expect(commit).toHaveBeenCalledWith(types.CLEAR_SEARCH_RESULTS);
+ });
+ });
});
diff --git a/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js
index 489a83fd2..ea3ca7048 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js
@@ -10,10 +10,49 @@ describe('#getters', () => {
]);
});
+ it('getContactRecords', () => {
+ const state = {
+ contactRecords: [{ id: 1, name: 'Contact 1' }],
+ };
+ expect(getters.getContactRecords(state)).toEqual([
+ { id: 1, name: 'Contact 1' },
+ ]);
+ });
+
+ it('getConversationRecords', () => {
+ const state = {
+ conversationRecords: [{ id: 1, title: 'Conversation 1' }],
+ };
+ expect(getters.getConversationRecords(state)).toEqual([
+ { id: 1, title: 'Conversation 1' },
+ ]);
+ });
+
+ it('getMessageRecords', () => {
+ const state = {
+ messageRecords: [{ id: 1, content: 'Message 1' }],
+ };
+ expect(getters.getMessageRecords(state)).toEqual([
+ { id: 1, content: 'Message 1' },
+ ]);
+ });
+
it('getUIFlags', () => {
const state = {
- uiFlags: { isFetching: false },
+ uiFlags: {
+ isFetching: false,
+ isSearchCompleted: true,
+ contact: { isFetching: true },
+ message: { isFetching: false },
+ conversation: { isFetching: false },
+ },
};
- expect(getters.getUIFlags(state)).toEqual({ isFetching: false });
+ expect(getters.getUIFlags(state)).toEqual({
+ isFetching: false,
+ isSearchCompleted: true,
+ contact: { isFetching: true },
+ message: { isFetching: false },
+ conversation: { isFetching: false },
+ });
});
});
diff --git a/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js
index 770129655..7bef2e527 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js
@@ -10,7 +10,7 @@ describe('#mutations', () => {
});
});
- describe('#SEARCH_CONVERSATIONS_SET', () => {
+ describe('#SEARCH_CONVERSATIONS_SET_UI_FLAG', () => {
it('set uiFlags correctly', () => {
const state = { uiFlags: { isFetching: true } };
mutations[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, {
@@ -19,4 +19,99 @@ describe('#mutations', () => {
expect(state.uiFlags).toEqual({ isFetching: false });
});
});
+
+ describe('#CONTACT_SEARCH_SET', () => {
+ it('should append new contact records to existing ones', () => {
+ const state = { contactRecords: [{ id: 1 }] };
+ mutations[types.CONTACT_SEARCH_SET](state, [{ id: 2 }]);
+ expect(state.contactRecords).toEqual([{ id: 1 }, { id: 2 }]);
+ });
+ });
+
+ describe('#CONVERSATION_SEARCH_SET', () => {
+ it('should append new conversation records to existing ones', () => {
+ const state = { conversationRecords: [{ id: 1 }] };
+ mutations[types.CONVERSATION_SEARCH_SET](state, [{ id: 2 }]);
+ expect(state.conversationRecords).toEqual([{ id: 1 }, { id: 2 }]);
+ });
+ });
+
+ describe('#MESSAGE_SEARCH_SET', () => {
+ it('should append new message records to existing ones', () => {
+ const state = { messageRecords: [{ id: 1 }] };
+ mutations[types.MESSAGE_SEARCH_SET](state, [{ id: 2 }]);
+ expect(state.messageRecords).toEqual([{ id: 1 }, { id: 2 }]);
+ });
+ });
+
+ describe('#FULL_SEARCH_SET_UI_FLAG', () => {
+ it('set full search UI flags correctly', () => {
+ const state = {
+ uiFlags: {
+ isFetching: true,
+ isSearchCompleted: false,
+ },
+ };
+ mutations[types.FULL_SEARCH_SET_UI_FLAG](state, {
+ isFetching: false,
+ isSearchCompleted: true,
+ });
+ expect(state.uiFlags).toEqual({
+ isFetching: false,
+ isSearchCompleted: true,
+ });
+ });
+ });
+
+ describe('#CONTACT_SEARCH_SET_UI_FLAG', () => {
+ it('set contact search UI flags correctly', () => {
+ const state = {
+ uiFlags: {
+ contact: { isFetching: true },
+ },
+ };
+ mutations[types.CONTACT_SEARCH_SET_UI_FLAG](state, { isFetching: false });
+ expect(state.uiFlags.contact).toEqual({ isFetching: false });
+ });
+ });
+
+ describe('#CONVERSATION_SEARCH_SET_UI_FLAG', () => {
+ it('set conversation search UI flags correctly', () => {
+ const state = {
+ uiFlags: {
+ conversation: { isFetching: true },
+ },
+ };
+ mutations[types.CONVERSATION_SEARCH_SET_UI_FLAG](state, {
+ isFetching: false,
+ });
+ expect(state.uiFlags.conversation).toEqual({ isFetching: false });
+ });
+ });
+
+ describe('#MESSAGE_SEARCH_SET_UI_FLAG', () => {
+ it('set message search UI flags correctly', () => {
+ const state = {
+ uiFlags: {
+ message: { isFetching: true },
+ },
+ };
+ mutations[types.MESSAGE_SEARCH_SET_UI_FLAG](state, { isFetching: false });
+ expect(state.uiFlags.message).toEqual({ isFetching: false });
+ });
+ });
+
+ describe('#CLEAR_SEARCH_RESULTS', () => {
+ it('should clear all search records', () => {
+ const state = {
+ contactRecords: [{ id: 1 }],
+ conversationRecords: [{ id: 1 }],
+ messageRecords: [{ id: 1 }],
+ };
+ mutations[types.CLEAR_SEARCH_RESULTS](state);
+ expect(state.contactRecords).toEqual([]);
+ expect(state.conversationRecords).toEqual([]);
+ expect(state.messageRecords).toEqual([]);
+ });
+ });
});
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index 7cc5223a0..7416ca9a3 100644
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -311,6 +311,7 @@ export default {
CONVERSATION_SEARCH_SET: 'CONVERSATION_SEARCH_SET',
CONVERSATION_SEARCH_SET_UI_FLAG: 'CONVERSATION_SEARCH_SET_UI_FLAG',
MESSAGE_SEARCH_SET: 'MESSAGE_SEARCH_SET',
+ CLEAR_SEARCH_RESULTS: 'CLEAR_SEARCH_RESULTS',
MESSAGE_SEARCH_SET_UI_FLAG: 'MESSAGE_SEARCH_SET_UI_FLAG',
FULL_SEARCH_SET_UI_FLAG: 'FULL_SEARCH_SET_UI_FLAG',
SET_CONVERSATION_PARTICIPANTS_UI_FLAG:
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index add7938a0..9c7a6ceef 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -30,7 +30,8 @@ class SearchService
.where("cast(conversations.display_id as text) ILIKE :search OR contacts.name ILIKE :search OR contacts.email
ILIKE :search OR contacts.phone_number ILIKE :search OR contacts.identifier ILIKE :search", search: "%#{search_query}%")
.order('conversations.created_at DESC')
- .limit(10)
+ .page(params[:page])
+ .per(15)
end
def filter_messages
@@ -38,13 +39,14 @@ class SearchService
.where('messages.content ILIKE :search', search: "%#{search_query}%")
.where('created_at >= ?', 3.months.ago)
.reorder('created_at DESC')
- .limit(10)
+ .page(params[:page])
+ .per(15)
end
def filter_contacts
@contacts = current_account.contacts.where(
"name ILIKE :search OR email ILIKE :search OR phone_number
ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
- ).resolved_contacts.order_on_last_activity_at('desc').limit(10)
+ ).resolved_contacts.order_on_last_activity_at('desc').page(params[:page]).per(15)
end
end