diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index f4bbcc149..051b25679 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -25,7 +25,7 @@ class ConversationFinder set_assignee_type find_all_conversations - filter_by_status + filter_by_status unless params[:q] filter_by_labels if params[:labels] filter_by_query if params[:q] @@ -78,9 +78,11 @@ class ConversationFinder end def filter_by_query - @conversations = @conversations.joins(:messages).where('messages.content LIKE :search', - search: "%#{params[:q]}%").includes(:messages).where('messages.content LIKE :search', - search: "%#{params[:q]}%") + allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]] + @conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%") + .where(messages: { message_type: allowed_message_types }).includes(:messages) + .where('messages.content ILIKE :search', search: "%#{params[:q]}%") + .where(messages: { message_type: allowed_message_types }) end def filter_by_status diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 6e5c72ca0..9f87d2aa5 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -18,6 +18,15 @@ class ConversationApi extends ApiClient { }); } + search({ q }) { + return axios.get(`${this.url}/search`, { + params: { + q, + page: 1, + }, + }); + } + toggleStatus(conversationId) { return axios.post(`${this.url}/${conversationId}/toggle_status`, {}); } diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index c511ee243..ae89fe9a2 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -72,7 +72,7 @@ .chat-list__top { @include flex; - @include padding($space-normal $zero $space-small $zero); + @include padding($zero $zero $space-small $zero); align-items: center; justify-content: space-between; diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 5ca82a407..8a8c59551 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -1,5 +1,6 @@ + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/Search.vue b/app/javascript/dashboard/routes/dashboard/conversation/search/Search.vue new file mode 100644 index 000000000..78242286a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/search/Search.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 3da923b76..ab6103ab7 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -11,6 +11,7 @@ import conversationLabels from './modules/conversationLabels'; import conversationMetadata from './modules/conversationMetadata'; import conversationPage from './modules/conversationPage'; import conversations from './modules/conversations'; +import conversationSearch from './modules/conversationSearch'; import conversationStats from './modules/conversationStats'; import conversationTypingStatus from './modules/conversationTypingStatus'; import globalConfig from 'shared/store/globalConfig'; @@ -35,6 +36,7 @@ export default new Vuex.Store({ conversationMetadata, conversationPage, conversations, + conversationSearch, conversationStats, conversationTypingStatus, globalConfig, diff --git a/app/javascript/dashboard/store/modules/conversationSearch.js b/app/javascript/dashboard/store/modules/conversationSearch.js new file mode 100644 index 000000000..c289de62a --- /dev/null +++ b/app/javascript/dashboard/store/modules/conversationSearch.js @@ -0,0 +1,54 @@ +import ConversationAPI from '../../api/inbox/conversation'; +import types from '../mutation-types'; +export const initialState = { + records: [], + uiFlags: { + isFetching: false, + }, +}; + +export const getters = { + getConversations(state) { + return state.records; + }, + getUIFlags(state) { + return state.uiFlags; + }, +}; + +export const actions = { + async get({ commit }, { q }) { + commit(types.SEARCH_CONVERSATIONS_SET, []); + if (!q) { + return; + } + commit(types.SEARCH_CONVERSATIONS_SET_UI_FLAG, { isFetching: true }); + try { + const { + data: { payload }, + } = await ConversationAPI.search({ q }); + commit(types.SEARCH_CONVERSATIONS_SET, payload); + } catch (error) { + // Ignore error + } finally { + commit(types.SEARCH_CONVERSATIONS_SET_UI_FLAG, { isFetching: false }); + } + }, +}; + +export const mutations = { + [types.SEARCH_CONVERSATIONS_SET](state, records) { + state.records = records; + }, + [types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) { + state.uiFlags = { ...state.uiFlags, ...uiFlags }; + }, +}; + +export default { + namespaced: true, + state: initialState, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/conversationSearch/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversationSearch/actions.spec.js new file mode 100644 index 000000000..449e699c2 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/actions.spec.js @@ -0,0 +1,44 @@ +import { actions } from '../../conversationSearch'; +import types from '../../../mutation-types'; +import axios from 'axios'; +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct actions if no query param is provided', () => { + actions.get({ commit }, { q: '' }); + expect(commit.mock.calls).toEqual([[types.SEARCH_CONVERSATIONS_SET, []]]); + }); + + it('sends correct actions if query param is provided and API call is success', async () => { + axios.get.mockResolvedValue({ + data: { + payload: [{ messages: [{ id: 1, content: 'value testing' }], id: 1 }], + }, + }); + + await actions.get({ commit }, { q: 'value' }); + expect(commit.mock.calls).toEqual([ + [types.SEARCH_CONVERSATIONS_SET, []], + [types.SEARCH_CONVERSATIONS_SET_UI_FLAG, { isFetching: true }], + [ + types.SEARCH_CONVERSATIONS_SET, + [{ messages: [{ id: 1, content: 'value testing' }], id: 1 }], + ], + [types.SEARCH_CONVERSATIONS_SET_UI_FLAG, { isFetching: false }], + ]); + }); + + it('sends correct actions if query param is provided and API call is errored', async () => { + axios.get.mockRejectedValue({}); + await actions.get({ commit }, { q: 'value' }); + expect(commit.mock.calls).toEqual([ + [types.SEARCH_CONVERSATIONS_SET, []], + [types.SEARCH_CONVERSATIONS_SET_UI_FLAG, { isFetching: true }], + [types.SEARCH_CONVERSATIONS_SET_UI_FLAG, { isFetching: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js new file mode 100644 index 000000000..489a83fd2 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js @@ -0,0 +1,19 @@ +import { getters } from '../../conversationSearch'; + +describe('#getters', () => { + it('getConversations', () => { + const state = { + records: [{ id: 1, messages: [{ id: 1, content: 'value' }] }], + }; + expect(getters.getConversations(state)).toEqual([ + { id: 1, messages: [{ id: 1, content: 'value' }] }, + ]); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { isFetching: false }, + }; + expect(getters.getUIFlags(state)).toEqual({ 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 new file mode 100644 index 000000000..770129655 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js @@ -0,0 +1,22 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../conversationSearch'; + +describe('#mutations', () => { + describe('#SEARCH_CONVERSATIONS_SET', () => { + it('set records correctly', () => { + const state = { records: [] }; + mutations[types.SEARCH_CONVERSATIONS_SET](state, [{ id: 1 }]); + expect(state.records).toEqual([{ id: 1 }]); + }); + }); + + describe('#SEARCH_CONVERSATIONS_SET', () => { + it('set uiFlags correctly', () => { + const state = { uiFlags: { isFetching: true } }; + mutations[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, { + isFetching: false, + }); + expect(state.uiFlags).toEqual({ isFetching: false }); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 2feb845b0..98e710318 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -123,4 +123,8 @@ export default { // User Typing ADD_USER_TYPING_TO_CONVERSATION: 'ADD_USER_TYPING_TO_CONVERSATION', REMOVE_USER_TYPING_FROM_CONVERSATION: 'REMOVE_USER_TYPING_FROM_CONVERSATION', + + // Conversation Search + SEARCH_CONVERSATIONS_SET: 'SEARCH_CONVERSATIONS_SET', + SEARCH_CONVERSATIONS_SET_UI_FLAG: 'SEARCH_CONVERSATIONS_SET_UI_FLAG', }; diff --git a/app/views/api/v1/accounts/conversations/search.json.jbuilder b/app/views/api/v1/accounts/conversations/search.json.jbuilder index ca8bb58eb..9193b0e48 100644 --- a/app/views/api/v1/accounts/conversations/search.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/search.json.jbuilder @@ -1,22 +1,17 @@ -json.data do - json.meta do - json.mine_count @conversations_count[:mine_count] - json.unassigned_count @conversations_count[:unassigned_count] - json.all_count @conversations_count[:all_count] - end - json.payload do - json.array! @conversations do |conversation| - json.inbox_id conversation.inbox_id - json.messages conversation.messages - json.status conversation.status - json.muted conversation.muted? - json.can_reply conversation.can_reply? - json.timestamp conversation.last_activity_at.to_i - json.contact_last_seen_at conversation.contact_last_seen_at.to_i - json.agent_last_seen_at conversation.agent_last_seen_at.to_i - json.additional_attributes conversation.additional_attributes - json.account_id conversation.account_id - json.labels conversation.label_list +json.meta do + json.mine_count @conversations_count[:mine_count] + json.unassigned_count @conversations_count[:unassigned_count] + json.all_count @conversations_count[:all_count] +end +json.payload do + json.array! @conversations do |conversation| + json.id conversation.display_id + json.messages do + json.array! conversation.messages do |message| + json.content message.content + json.created_at message.created_at.to_i + end end + json.account_id conversation.account_id end end diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 87ff6d592..fbefe25a1 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -85,7 +85,7 @@ RSpec.describe 'Conversations API', type: :request do as: :json expect(response).to have_http_status(:success) - response_data = JSON.parse(response.body, symbolize_names: true)[:data] + response_data = JSON.parse(response.body, symbolize_names: true) expect(response_data[:meta][:all_count]).to eq(1) expect(response_data[:payload].first[:messages].first[:content]).to eq 'test1' end