diff --git a/app/javascript/dashboard/api/bulkActions.js b/app/javascript/dashboard/api/bulkActions.js new file mode 100644 index 000000000..a606c56ac --- /dev/null +++ b/app/javascript/dashboard/api/bulkActions.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class BulkActionsAPI extends ApiClient { + constructor() { + super('bulk_actions', { accountScoped: true }); + } +} + +export default new BulkActionsAPI(); diff --git a/app/javascript/dashboard/api/specs/bulkAction.spec.js b/app/javascript/dashboard/api/specs/bulkAction.spec.js new file mode 100644 index 000000000..aec0b1e3e --- /dev/null +++ b/app/javascript/dashboard/api/specs/bulkAction.spec.js @@ -0,0 +1,9 @@ +import bulkActions from '../bulkActions'; +import ApiClient from '../ApiClient'; + +describe('#BulkActionsAPI', () => { + it('creates correct instance', () => { + expect(bulkActions).toBeInstanceOf(ApiClient); + expect(bulkActions).toHaveProperty('create'); + }); +}); diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 2887e4a08..4a90bf7a1 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -96,6 +96,9 @@ :chat="chat" :conversation-type="conversationType" :show-assignee="showAssigneeInConversationCard" + :selected="isConversationSelected(chat.id)" + @select-conversation="selectConversation" + @de-select-conversation="deSelectConversation" />
@@ -134,6 +137,16 @@ @applyFilter="onApplyFilter" /> + +
@@ -152,6 +165,8 @@ import advancedFilterTypes from './widgets/conversation/advancedFilterItems'; import filterQueryGenerator from '../helper/filterQueryGenerator.js'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; +import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Actions.vue'; +import alertMixin from 'shared/mixins/alertMixin'; import { hasPressedAltAndJKey, @@ -166,8 +181,9 @@ export default { ChatFilter, ConversationAdvancedFilter, DeleteCustomViews, + ConversationBulkActions, }, - mixins: [timeMixin, conversationMixin, eventListenerMixins], + mixins: [timeMixin, conversationMixin, eventListenerMixins, alertMixin], props: { conversationInbox: { type: [String, Number], @@ -202,6 +218,8 @@ export default { foldersQuery: {}, showAddFoldersModal: false, showDeleteFoldersModal: false, + selectedConversations: [], + selectedInboxes: [], }; }, computed: { @@ -217,6 +235,7 @@ export default { conversationStats: 'conversationStats/getStats', appliedFilters: 'getAppliedConversationFilters', folders: 'customViews/getCustomViews', + inboxes: 'inboxes/getInboxes', }), hasAppliedFilters() { return this.appliedFilters.length !== 0; @@ -343,6 +362,15 @@ export default { } return {}; }, + allConversationsSelected() { + return ( + JSON.stringify(this.selectedConversations) === + JSON.stringify(this.conversationList.map(item => item.id)) + ); + }, + uniqueInboxes() { + return [...new Set(this.selectedInboxes)]; + }, }, watch: { activeTeam() { @@ -376,6 +404,7 @@ export default { if (this.$route.name !== 'home') { this.$router.push({ name: 'home' }); } + this.resetBulkActions(); this.foldersQuery = filterQueryGenerator(payload); this.$store.dispatch('conversationPage/reset'); this.$store.dispatch('emptyAllConversations'); @@ -441,6 +470,7 @@ export default { } }, resetAndFetchData() { + this.resetBulkActions(); this.$store.dispatch('conversationPage/reset'); this.$store.dispatch('emptyAllConversations'); this.$store.dispatch('clearConversationFilters'); @@ -491,6 +521,7 @@ export default { }, updateAssigneeTab(selectedTab) { if (this.activeAssigneeTab !== selectedTab) { + this.resetBulkActions(); bus.$emit('clearSearchInput'); this.activeAssigneeTab = selectedTab; if (!this.currentPage) { @@ -498,6 +529,10 @@ export default { } } }, + resetBulkActions() { + this.selectedConversations = []; + this.selectedInboxes = []; + }, updateStatusType(index) { if (this.activeStatus !== index) { this.activeStatus = index; @@ -520,6 +555,59 @@ export default { this.fetchConversations(); } }, + isConversationSelected(id) { + return this.selectedConversations.includes(id); + }, + selectConversation(conversationId, inboxId) { + this.selectedConversations.push(conversationId); + this.selectedInboxes.push(inboxId); + }, + deSelectConversation(conversationId, inboxId) { + this.selectedConversations = this.selectedConversations.filter( + item => item !== conversationId + ); + this.selectedInboxes = this.selectedInboxes.filter( + item => item !== inboxId + ); + }, + selectAllConversations(check) { + if (check) { + this.selectedConversations = this.conversationList.map(item => item.id); + this.selectedInboxes = this.conversationList.map(item => item.inbox_id); + } else { + this.resetBulkActions(); + } + }, + async onAssignAgent(agent) { + try { + await this.$store.dispatch('bulkActions/process', { + type: 'Conversation', + ids: this.selectedConversations, + fields: { + assignee_id: agent.id, + }, + }); + this.selectedConversations = []; + this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL')); + } catch (err) { + this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED')); + } + }, + async onResolveConversations() { + try { + await this.$store.dispatch('bulkActions/process', { + type: 'Conversation', + ids: this.selectedConversations, + fields: { + status: 'resolved', + }, + }); + this.selectedConversations = []; + this.showAlert(this.$t('BULK_ACTION.RESOLVE_SUCCESFUL')); + } catch (error) { + this.showAlert(this.$t('BULK_ACTION.RESOLVE_FAILED')); + } + }, }, }; @@ -535,7 +623,7 @@ export default { .conversations-list-wrap { flex-shrink: 0; width: 34rem; - + overflow: hidden; @include breakpoint(large up) { width: 36rem; } diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index 494bd5c82..d2eb8a4bd 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -99,7 +99,7 @@ export default { watch: { 'currentChat.inbox_id'(inboxId) { if (inboxId) { - this.$store.dispatch('inboxAssignableAgents/fetch', { inboxId }); + this.$store.dispatch('inboxAssignableAgents/fetch', [inboxId]); } }, 'currentChat.id'() { diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 7e6c89bac..ee12b989a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -5,11 +5,24 @@ active: isActiveChat, 'unread-chat': hasUnread, 'has-inbox-name': showInboxName, + 'conversation-selected': selected, }" + @mouseenter="onCardHover" + @mouseleave="onCardLeave" @click="cardClick(chat)" > + @@ -272,6 +305,10 @@ export default { } } +.conversation-selected { + background: var(--color-background-light); +} + .has-inbox-name { &::v-deep .user-thumbnail-box { margin-top: var(--space-normal); @@ -320,4 +357,22 @@ export default { margin-top: var(--space-minus-micro); vertical-align: middle; } +.checkbox-wrapper { + height: 40px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + margin-top: var(--space-normal); + cursor: pointer; + &:hover { + background-color: var(--w-100); + } + + input[type='checkbox'] { + margin: var(--space-zero); + cursor: pointer; + } +} diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Actions.vue new file mode 100644 index 000000000..5466f559d --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Actions.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue new file mode 100644 index 000000000..1d7fe6ec0 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/bulkActions.json b/app/javascript/dashboard/i18n/locale/en/bulkActions.json new file mode 100644 index 000000000..bfd688bef --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/bulkActions.json @@ -0,0 +1,17 @@ +{ + "BULK_ACTION": { + "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", + "AGENT_SELECT_LABEL": "Select Agent", + "ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", + "GO_BACK_LABEL": "Go back", + "ASSIGN_LABEL": "Assign", + "ASSIGN_AGENT_TOOLTIP": "Assign Agent", + "RESOLVE_TOOLTIP": "Resolve", + "ASSIGN_SUCCESFUL": "Conversations assigned successfully", + "ASSIGN_FAILED": "Failed to assign conversations, please try again", + "RESOLVE_SUCCESFUL": "Conversations resolved successfully", + "RESOLVE_FAILED": "Failed to resolve conversations, please try again", + "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", + "AGENT_LIST_LOADING": "Loading Agents" + } +} diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index 5c1449fab..b21009322 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -21,6 +21,7 @@ import { default as _setNewPassword } from './setNewPassword.json'; import { default as _settings } from './settings.json'; import { default as _signup } from './signup.json'; import { default as _teamsSettings } from './teamsSettings.json'; +import { default as _bulkActions } from './bulkActions.json'; export default { ..._advancedFilters, @@ -46,4 +47,5 @@ export default { ..._settings, ..._signup, ..._teamsSettings, + ..._bulkActions, }; diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index d51ebe6eb..e8709c62f 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -21,7 +21,7 @@ }, "MESSAGE_SIGNATURE_SECTION": { "TITLE": "Personal message signature", - "NOTE": "Create a personal message signature that would be added to all the messages you send from the platform. Use the rich content editor to create a highly personalised signature.", + "NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.", "BTN_TEXT": "Save message signature", "API_ERROR": "Couldn't save signature! Try again", "API_SUCCESS": "Signature saved successfully" diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 5cd1a0112..4957a27cc 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -6,6 +6,7 @@ import agents from './modules/agents'; import attributes from './modules/attributes'; import auth from './modules/auth'; import automations from './modules/automations'; +import bulkActions from './modules/bulkActions'; import campaigns from './modules/campaigns'; import cannedResponse from './modules/cannedResponse'; import contactConversations from './modules/contactConversations'; @@ -43,6 +44,7 @@ export default new Vuex.Store({ attributes, auth, automations, + bulkActions, campaigns, cannedResponse, contactConversations, diff --git a/app/javascript/dashboard/store/modules/bulkActions.js b/app/javascript/dashboard/store/modules/bulkActions.js new file mode 100644 index 000000000..709f4875a --- /dev/null +++ b/app/javascript/dashboard/store/modules/bulkActions.js @@ -0,0 +1,44 @@ +import types from '../mutation-types'; +import BulkActionsAPI from '../../api/bulkActions'; + +export const state = { + uiFlags: { + isUpdating: false, + }, +}; + +export const getters = { + getUIFlags(_state) { + return _state.uiFlags; + }, +}; + +export const actions = { + process: async function processAction({ commit }, payload) { + commit(types.SET_BULK_ACTIONS_FLAG, { isUpdating: true }); + try { + await BulkActionsAPI.create(payload); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_BULK_ACTIONS_FLAG, { isUpdating: false }); + } + }, +}; + +export const mutations = { + [types.SET_BULK_ACTIONS_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, +}; + +export default { + namespaced: true, + actions, + state, + getters, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/inboxAssignableAgents.js b/app/javascript/dashboard/store/modules/inboxAssignableAgents.js index 1acb6897a..08baeb536 100644 --- a/app/javascript/dashboard/store/modules/inboxAssignableAgents.js +++ b/app/javascript/dashboard/store/modules/inboxAssignableAgents.js @@ -26,13 +26,16 @@ export const getters = { }; export const actions = { - async fetch({ commit }, { inboxId }) { + async fetch({ commit }, inboxIds) { commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }); try { const { data: { payload }, - } = await AssignableAgentsAPI.get([inboxId]); - commit(types.SET_INBOX_ASSIGNABLE_AGENTS, { inboxId, members: payload }); + } = await AssignableAgentsAPI.get(inboxIds); + commit(types.SET_INBOX_ASSIGNABLE_AGENTS, { + inboxId: inboxIds.join(','), + members: payload, + }); } catch (error) { throw new Error(error); } finally { diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/actions.spec.js b/app/javascript/dashboard/store/modules/specs/bulkActions/actions.spec.js new file mode 100644 index 000000000..5342c46fd --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/actions.spec.js @@ -0,0 +1,28 @@ +import axios from 'axios'; +import { actions } from '../../bulkActions'; +import * as types from '../../../mutation-types'; +import payload from './fixtures'; +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: payload }); + await actions.process({ commit }, payload); + expect(commit.mock.calls).toEqual([ + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: true }], + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.process({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: true }], + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/fixtures.js b/app/javascript/dashboard/store/modules/specs/bulkActions/fixtures.js new file mode 100644 index 000000000..3b3606e36 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/fixtures.js @@ -0,0 +1,5 @@ +export default { + type: 'Conversation', + ids: [64, 39], + fields: { assignee_id: 6 }, +}; diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/getters.spec.js b/app/javascript/dashboard/store/modules/specs/bulkActions/getters.spec.js new file mode 100644 index 000000000..2349e2efb --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/getters.spec.js @@ -0,0 +1,14 @@ +import { getters } from '../../bulkActions'; + +describe('#getters', () => { + it('getUIFlags', () => { + const state = { + uiFlags: { + isUpdating: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isUpdating: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/bulkActions/mutations.spec.js new file mode 100644 index 000000000..29278c72e --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/mutations.spec.js @@ -0,0 +1,12 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../bulkActions'; + +describe('#mutations', () => { + describe('#toggleUiFlag', () => { + it('set update flags', () => { + const state = { uiFlags: { isUpdating: false } }; + mutations[types.SET_BULK_ACTIONS_FLAG](state, { isUpdating: true }); + expect(state.uiFlags.isUpdating).toEqual(true); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js index 480ee657f..39b21b3c0 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js @@ -12,12 +12,12 @@ describe('#actions', () => { axios.get.mockResolvedValue({ data: { payload: agentsData }, }); - await actions.fetch({ commit }, { inboxId: 1 }); + await actions.fetch({ commit }, [1]); expect(commit.mock.calls).toEqual([ [types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }], [ types.SET_INBOX_ASSIGNABLE_AGENTS, - { inboxId: 1, members: agentsData }, + { inboxId: '1', members: agentsData }, ], [types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }], ]); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index b78e468a1..e20efbf78 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -211,6 +211,9 @@ export default { ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW', DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW', + // Bulk Actions + SET_BULK_ACTIONS_FLAG: 'SET_BULK_ACTIONS_FLAG', + // Dashboard Apps SET_DASHBOARD_APPS_UI_FLAG: 'SET_DASHBOARD_APPS_UI_FLAG', SET_DASHBOARD_APPS: 'SET_DASHBOARD_APPS', diff --git a/app/javascript/shared/assets/stylesheets/colors.scss b/app/javascript/shared/assets/stylesheets/colors.scss index 3f64ce58b..82a700929 100644 --- a/app/javascript/shared/assets/stylesheets/colors.scss +++ b/app/javascript/shared/assets/stylesheets/colors.scss @@ -7,7 +7,7 @@ --w-75: #D6EBFF; --w-100: #C2E1FF; --w-200: #99CEFF; - --w-300: ##70BAFF; + --w-300: #70BAFF; --w-400: #47A6FF; --w-500: #1F93FF; --w-600: #1976CC; diff --git a/app/javascript/shared/assets/stylesheets/shadows.scss b/app/javascript/shared/assets/stylesheets/shadows.scss index 0e44e1d26..6b11a427b 100644 --- a/app/javascript/shared/assets/stylesheets/shadows.scss +++ b/app/javascript/shared/assets/stylesheets/shadows.scss @@ -8,4 +8,9 @@ 0 4px 6px -2px rgba(0, 0, 0, 0.05); --shadow-larger: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --shadow-dropdown-pane: + 0 0.8rem 1.6rem rgb(50 50 93 / 8%), + 0 0.4rem 1.2rem rgb(0 0 0 / 7%); + --shadow-bulk-action-container: + 6px 3px 22px 9px rgb(181 181 181 / 25%); } diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index cd52e47f0..d235ffdec 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -98,6 +98,7 @@ "people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z", "people-team-outline": "M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75h5.502Zm0 1.5H9.252a.25.25 0 0 0-.25.25v4.749a3.001 3.001 0 0 0 6.002 0V11.75a.25.25 0 0 0-.25-.25ZM3.75 10h3.381a2.738 2.738 0 0 0-.618 1.5H3.75a.25.25 0 0 0-.25.25v3.249a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V11.75c0-.966.784-1.75 1.75-1.75Zm13.125 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.214-.448.369-.929.455-1.433A2.5 2.5 0 0 0 20.5 15v-3.25a.25.25 0 0 0-.25-.25h-2.757a2.738 2.738 0 0 0-.618-1.5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm6.5 1a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-13 0a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm6.5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-13 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z", "person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z", + "person-assign-outline": "M11.313 15.5a6.471 6.471 0 0 1 .709-1.5h-7.77a2.249 2.249 0 0 0-2.249 2.25v.577c0 .892.319 1.756.899 2.435c1.566 1.834 3.952 2.74 7.098 2.74c.931 0 1.796-.08 2.592-.24a6.51 6.51 0 0 1-.913-1.366c-.524.07-1.083.105-1.68.105c-2.737 0-4.703-.745-5.957-2.213a2.25 2.25 0 0 1-.539-1.461v-.578a.75.75 0 0 1 .75-.749h7.06ZM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM23 17.5a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0Zm-4.647-2.853a.5.5 0 0 0-.707.707L19.293 17H15a.5.5 0 1 0 0 1h4.293l-1.647 1.647a.5.5 0 0 0 .707.707l2.5-2.5a.497.497 0 0 0 .147-.345V17.5a.498.498 0 0 0-.15-.357l-2.497-2.496Z", "person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z", "power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z", "quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z", diff --git a/app/jobs/inboxes/fetch_imap_emails_job.rb b/app/jobs/inboxes/fetch_imap_emails_job.rb index eab5e4701..2b77d068c 100644 --- a/app/jobs/inboxes/fetch_imap_emails_job.rb +++ b/app/jobs/inboxes/fetch_imap_emails_job.rb @@ -6,13 +6,12 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob def perform(channel) return unless should_fetch_email?(channel) - process_mail_for_channel(channel) + fetch_mail_for_channel(channel) # clearing old failures like timeouts since the mail is now successfully processed channel.reauthorized! rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::IMAP::NoResponseError channel.authorization_error! rescue StandardError => e - channel.authorization_error! ChatwootExceptionTracker.new(e, account: channel.account).capture_exception end @@ -22,7 +21,7 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob channel.imap_enabled? && !channel.reauthorization_required? end - def process_mail_for_channel(channel) + def fetch_mail_for_channel(channel) # TODO: rather than setting this as default method for all mail objects, lets if can do new mail object # using Mail.retriever_method.new(params) Mail.defaults do @@ -36,12 +35,18 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob new_mails = false Mail.find(what: :last, count: 10, order: :desc).each do |inbound_mail| - if inbound_mail.date.utc >= channel.imap_inbox_synced_at - Imap::ImapMailbox.new.process(inbound_mail, channel) - new_mails = true - end + next unless inbound_mail.date.utc >= channel.imap_inbox_synced_at + + process_mail(inbound_mail, channel) + new_mails = true end channel.update(imap_inbox_synced_at: Time.now.utc) if new_mails end + + def process_mail(inbound_mail, channel) + Imap::ImapMailbox.new.process(inbound_mail, channel) + rescue StandardError => e + ChatwootExceptionTracker.new(e, account: channel.account).capture_exception + end end diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index 6f9512f39..15f538846 100644 --- a/deployment/setup_20.04.sh +++ b/deployment/setup_20.04.sh @@ -179,7 +179,8 @@ function main() { Chatwoot Installation (latest) *************************************************************************** -For more verbose logs, open up a second terminal and follow along using, `tail -f /var/log/chatwoot`. +For more verbose logs, open up a second terminal and follow along using, +'tail -f /var/log/chatwoot'. EOF @@ -199,7 +200,7 @@ EOF echo "***** Skipping Postgres and Redis installation. ****" fi - echo -en "\n➥ 1/9 Installing dependencies. This takes a while." + echo -en "\n➥ 1/9 Installing dependencies. This takes a while.\n" install_dependencies &>> "${LOG_FILE}" if [ "$install_pg_redis" != "no" ] @@ -273,7 +274,7 @@ The database migrations had not run as Postgres and Redis were not installed as part of the installation process. After modifying the environment variables (in the .env file) with your external database credentials, run the database migrations using the below command. -`RAILS_ENV=production bundle exec rails db:chatwoot_prepare`. +'RAILS_ENV=production bundle exec rails db:chatwoot_prepare'. *************************************************************************** EOF fi diff --git a/lib/chatwoot_exception_tracker.rb b/lib/chatwoot_exception_tracker.rb index 5e08d2dad..e525f3e89 100644 --- a/lib/chatwoot_exception_tracker.rb +++ b/lib/chatwoot_exception_tracker.rb @@ -12,8 +12,11 @@ class ChatwootExceptionTracker end def capture_exception - capture_exception_with_sentry if ENV['SENTRY_DSN'].present? - # Implement other providers like honeybadger, rollbar etc in future + if ENV['SENTRY_DSN'].present? + capture_exception_with_sentry + else + Rails.logger.error @exception + end end private diff --git a/spec/lib/chatwoot_exception_tracker_spec.rb b/spec/lib/chatwoot_exception_tracker_spec.rb index 5e8164d6d..d0d1b37a3 100644 --- a/spec/lib/chatwoot_exception_tracker_spec.rb +++ b/spec/lib/chatwoot_exception_tracker_spec.rb @@ -1,8 +1,9 @@ require 'rails_helper' describe ChatwootExceptionTracker do - it 'returns nil if no tracker is configured' do - expect(described_class.new('random').capture_exception).to eq(nil) + it 'use rails logger if no tracker is configured' do + expect(Rails.logger).to receive(:error).with('random') + described_class.new('random').capture_exception end context 'with sentry DSN' do