diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue index 637424815..74e78c9cf 100644 --- a/app/javascript/dashboard/components/buttons/ResolveAction.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -8,7 +8,7 @@ icon="ion-checkmark" emoji="✅" :is-loading="isLoading" - @click="() => toggleStatus(STATUS_TYPE.RESOLVED)" + @click="onCmdResolveConversation" > {{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }} @@ -19,7 +19,7 @@ icon="ion-refresh" emoji="👀" :is-loading="isLoading" - @click="() => toggleStatus(STATUS_TYPE.OPEN)" + @click="onCmdOpenConversation" > {{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }} @@ -29,7 +29,7 @@ color-scheme="primary" icon="ion-person" :is-loading="isLoading" - @click="() => toggleStatus(STATUS_TYPE.OPEN)" + @click="onCmdOpenConversation" > {{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }} @@ -118,6 +118,11 @@ import { startOfTomorrow, startOfWeek, } from 'date-fns'; +import { + CMD_REOPEN_CONVERSATION, + CMD_RESOLVE_CONVERSATION, + CMD_SNOOZE_CONVERSATION, +} from '../../routes/dashboard/commands/commandBarBusEvents'; export default { components: { @@ -135,9 +140,7 @@ export default { }; }, computed: { - ...mapGetters({ - currentChat: 'getSelectedChat', - }), + ...mapGetters({ currentChat: 'getSelectedChat' }), isOpen() { return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN; }, @@ -170,6 +173,16 @@ export default { }; }, }, + mounted() { + bus.$on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation); + bus.$on(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation); + bus.$on(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation); + }, + destroyed() { + bus.$off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation); + bus.$off(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation); + bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation); + }, methods: { async handleKeyEvents(e) { const allConversations = document.querySelectorAll( @@ -204,6 +217,18 @@ export default { } } }, + onCmdSnoozeConversation(snoozeType) { + this.toggleStatus( + this.STATUS_TYPE.SNOOZED, + this.snoozeTimes[snoozeType] || null + ); + }, + onCmdOpenConversation() { + this.toggleStatus(this.STATUS_TYPE.OPEN); + }, + onCmdResolveConversation() { + this.toggleStatus(this.STATUS_TYPE.RESOLVED); + }, showOpenButton() { return this.isResolved || this.isSnoozed; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index 423e64280..ea4055d08 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -64,8 +64,20 @@ export default { this.$store.dispatch('inboxAssignableAgents/fetch', { inboxId }); } }, + 'currentChat.id'() { + this.fetchLabels(); + }, + }, + mounted() { + this.fetchLabels(); }, methods: { + fetchLabels() { + if (!this.currentChat.id) { + return; + } + this.$store.dispatch('conversationLabels/get', this.currentChat.id); + }, onToggleContactPanel() { this.$emit('contact-panel-toggle'); }, diff --git a/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue b/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue index f7fe31bd2..502819b05 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue @@ -38,6 +38,11 @@ import { mixin as clickaway } from 'vue-clickaway'; import alertMixin from 'shared/mixins/alertMixin'; import EmailTranscriptModal from './EmailTranscriptModal'; import ResolveAction from '../../buttons/ResolveAction'; +import { + CMD_MUTE_CONVERSATION, + CMD_SEND_TRANSCRIPT, + CMD_UNMUTE_CONVERSATION, +} from '../../../routes/dashboard/commands/commandBarBusEvents'; export default { components: { @@ -47,36 +52,35 @@ export default { mixins: [alertMixin, clickaway], data() { return { - showConversationActions: false, showEmailActionsModal: false, }; }, computed: { - ...mapGetters({ - currentChat: 'getSelectedChat', - }), + ...mapGetters({ currentChat: 'getSelectedChat' }), + }, + mounted() { + bus.$on(CMD_MUTE_CONVERSATION, this.mute); + bus.$on(CMD_UNMUTE_CONVERSATION, this.unmute); + bus.$on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal); + }, + destroyed() { + bus.$off(CMD_MUTE_CONVERSATION, this.mute); + bus.$off(CMD_UNMUTE_CONVERSATION, this.unmute); + bus.$off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal); }, methods: { mute() { this.$store.dispatch('muteConversation', this.currentChat.id); this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS')); - this.toggleConversationActions(); }, unmute() { this.$store.dispatch('unmuteConversation', this.currentChat.id); this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS')); - this.toggleConversationActions(); }, toggleEmailActionsModal() { this.showEmailActionsModal = !this.showEmailActionsModal; this.hideConversationActions(); }, - toggleConversationActions() { - this.showConversationActions = !this.showConversationActions; - }, - hideConversationActions() { - this.showConversationActions = false; - }, }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js b/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js index 558b021d1..ad20a82b1 100644 --- a/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js +++ b/app/javascript/dashboard/components/widgets/conversation/specs/MoreActions.spec.js @@ -33,6 +33,8 @@ describe('MoveActions', () => { beforeEach(() => { window.bus = { $emit: jest.fn(), + $on: jest.fn(), + $off: jest.fn(), }; state = { diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index 223a4688e..bbeb72f0f 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -79,5 +79,50 @@ "BUTTON": { "REFRESH": "Refresh" } + }, + "COMMAND_BAR": { + "SEARCH_PLACEHOLDER": "Search or jump to", + "SECTIONS": { + "GENERAL": "General", + "REPORTS": "Reports", + "CONVERSATION": "Conversation", + "CHANGE_ASSIGNEE": "Change Assignee", + "CHANGE_TEAM": "Change Team", + "ADD_LABEL": "Add label to the conversation", + "REMOVE_LABEL": "Remove label from the conversation", + "SETTINGS": "Settings" + }, + "COMMANDS": { + "GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard", + "GO_TO_CONTACTS_DASHBOARD": "Go to Contacts Dashboard", + "GO_TO_REPORTS_OVERVIEW": "Go to Reports Overview", + "GO_TO_AGENT_REPORTS": "Go to Agent Reports", + "GO_TO_LABEL_REPORTS": "Go to Label Reports", + "GO_TO_INBOX_REPORTS": "Go to Inbox Reports", + "GO_TO_TEAM_REPORTS": "Go to Team Reports", + "GO_TO_SETTINGS_AGENTS": "Go to Agent Settings", + "GO_TO_SETTINGS_TEAMS": "Go to Team Settings", + "GO_TO_SETTINGS_INBOXES": "Go to Inbox Settings", + "GO_TO_SETTINGS_LABELS": "Go to Label Settings", + "GO_TO_SETTINGS_CANNED_RESPONSES": "Go to Canned Response Settings", + "GO_TO_SETTINGS_APPLICATIONS": "Go to Application Settings", + "GO_TO_SETTINGS_ACCOUNT": "Go to Account Settings", + "GO_TO_SETTINGS_PROFILE": "Go to Profile Settings", + "GO_TO_NOTIFICATIONS": "Go to Notifications", + + "ADD_LABELS_TO_CONVERSATION": "Add label to the conversation", + "ASSIGN_AN_AGENT": "Assign an agent", + "ASSIGN_A_TEAM": "Assign a team", + "MUTE_CONVERSATION": "Mute conversation", + "UNMUTE_CONVERSATION": "Unmute conversation", + "REMOVE_LABEL_FROM_CONVERSATION": "Remove label from the conversation", + "REOPEN_CONVERSATION": "Reopen conversation", + "RESOLVE_CONVERSATION": "Resolve conversation", + "SEND_TRANSCRIPT": "Send an email transcript", + "SNOOZE_CONVERSATION": "Snooze Conversation", + "UNTIL_NEXT_REPLY": "Until next reply", + "UNTIL_NEXT_WEEK": "Until next week", + "UNTIL_TOMORROW": "Until tomorrow" + } } } diff --git a/app/javascript/dashboard/mixins/agentMixin.js b/app/javascript/dashboard/mixins/agentMixin.js index 295dafa8a..14c5bb043 100644 --- a/app/javascript/dashboard/mixins/agentMixin.js +++ b/app/javascript/dashboard/mixins/agentMixin.js @@ -7,9 +7,7 @@ export default { this.inboxId ); }, - ...mapGetters({ - currentUser: 'getCurrentUser', - }), + ...mapGetters({ currentUser: 'getCurrentUser' }), isAgentSelected() { return this.currentChat?.meta?.assignee; }, diff --git a/app/javascript/dashboard/mixins/conversation/labelMixin.js b/app/javascript/dashboard/mixins/conversation/labelMixin.js new file mode 100644 index 000000000..622bd3519 --- /dev/null +++ b/app/javascript/dashboard/mixins/conversation/labelMixin.js @@ -0,0 +1,41 @@ +import { mapGetters } from 'vuex'; + +export default { + computed: { + ...mapGetters({ accountLabels: 'labels/getLabels' }), + savedLabels() { + return this.$store.getters['conversationLabels/getConversationLabels']( + this.conversationId + ); + }, + activeLabels() { + return this.accountLabels.filter(({ title }) => + this.savedLabels.includes(title) + ); + }, + inactiveLabels() { + return this.accountLabels.filter( + ({ title }) => !this.savedLabels.includes(title) + ); + }, + }, + methods: { + addLabelToConversation(value) { + const result = this.activeLabels.map(item => item.title); + result.push(value.title); + this.onUpdateLabels(result); + }, + removeLabelFromConversation(value) { + const result = this.activeLabels + .map(label => label.title) + .filter(label => label !== value); + this.onUpdateLabels(result); + }, + async onUpdateLabels(selectedLabels) { + this.$store.dispatch('conversationLabels/update', { + conversationId: this.conversationId, + labels: selectedLabels, + }); + }, + }, +}; diff --git a/app/javascript/dashboard/mixins/conversation/teamMixin.js b/app/javascript/dashboard/mixins/conversation/teamMixin.js new file mode 100644 index 000000000..745f91589 --- /dev/null +++ b/app/javascript/dashboard/mixins/conversation/teamMixin.js @@ -0,0 +1,22 @@ +import { mapGetters } from 'vuex'; + +export default { + computed: { + ...mapGetters({ teams: 'teams/getTeams' }), + hasAnAssignedTeam() { + return !!this.currentChat?.meta?.team; + }, + teamsList() { + if (this.hasAnAssignedTeam) { + return [ + { + id: 0, + name: 'None', + }, + ...this.teams, + ]; + } + return this.teams; + }, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/Dashboard.vue b/app/javascript/dashboard/routes/dashboard/Dashboard.vue index 1b0744bbd..123863eb7 100644 --- a/app/javascript/dashboard/routes/dashboard/Dashboard.vue +++ b/app/javascript/dashboard/routes/dashboard/Dashboard.vue @@ -3,16 +3,19 @@
+
+ + diff --git a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js new file mode 100644 index 000000000..4d1e1cfd7 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js @@ -0,0 +1,277 @@ +import { mapGetters } from 'vuex'; +import wootConstants from '../../../constants'; +import { + CMD_MUTE_CONVERSATION, + CMD_REOPEN_CONVERSATION, + CMD_RESOLVE_CONVERSATION, + CMD_SEND_TRANSCRIPT, + CMD_SNOOZE_CONVERSATION, + CMD_UNMUTE_CONVERSATION, +} from './commandBarBusEvents'; + +import { + ICON_ADD_LABEL, + ICON_ASSIGN_AGENT, + ICON_ASSIGN_TEAM, + ICON_MUTE_CONVERSATION, + ICON_REMOVE_LABEL, + ICON_REOPEN_CONVERSATION, + ICON_RESOLVE_CONVERSATION, + ICON_SEND_TRANSCRIPT, + ICON_SNOOZE_CONVERSATION, + ICON_SNOOZE_UNTIL_NEXT_REPLY, + ICON_SNOOZE_UNTIL_NEXT_WEEK, + ICON_SNOOZE_UNTIL_TOMORRROW, + ICON_UNMUTE_CONVERSATION, +} from './CommandBarIcons'; + +const OPEN_CONVERSATION_ACTIONS = [ + { + id: 'resolve_conversation', + title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.CONVERSATION', + icon: ICON_RESOLVE_CONVERSATION, + handler: () => bus.$emit(CMD_RESOLVE_CONVERSATION), + }, + { + id: 'snooze_conversation', + title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION', + icon: ICON_SNOOZE_CONVERSATION, + children: ['until_next_reply', 'until_tomorrow', 'until_next_week'], + }, + { + id: 'until_next_reply', + title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_REPLY', + parent: 'snooze_conversation', + icon: ICON_SNOOZE_UNTIL_NEXT_REPLY, + handler: () => bus.$emit(CMD_SNOOZE_CONVERSATION, 'nextReply'), + }, + { + id: 'until_tomorrow', + title: 'COMMAND_BAR.COMMANDS.UNTIL_TOMORROW', + parent: 'snooze_conversation', + icon: ICON_SNOOZE_UNTIL_TOMORRROW, + handler: () => bus.$emit(CMD_SNOOZE_CONVERSATION, 'tomorrow'), + }, + { + id: 'until_next_week', + title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_WEEK', + parent: 'snooze_conversation', + icon: ICON_SNOOZE_UNTIL_NEXT_WEEK, + handler: () => bus.$emit(CMD_SNOOZE_CONVERSATION, 'nextWeek'), + }, +]; + +const RESOLVED_CONVERSATION_ACTIONS = [ + { + id: 'reopen_conversation', + title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.CONVERSATION', + icon: ICON_REOPEN_CONVERSATION, + handler: () => bus.$emit(CMD_REOPEN_CONVERSATION), + }, +]; + +const SEND_TRANSCRIPT_ACTION = { + id: 'send_transcript', + title: 'COMMAND_BAR.COMMANDS.SEND_TRANSCRIPT', + section: 'COMMAND_BAR.SECTIONS.CONVERSATION', + icon: ICON_SEND_TRANSCRIPT, + handler: () => bus.$emit(CMD_SEND_TRANSCRIPT), +}; + +const UNMUTE_ACTION = { + id: 'unmute_conversation', + title: 'COMMAND_BAR.COMMANDS.UNMUTE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.CONVERSATION', + icon: ICON_UNMUTE_CONVERSATION, + handler: () => bus.$emit(CMD_UNMUTE_CONVERSATION), +}; + +const MUTE_ACTION = { + id: 'mute_conversation', + title: 'COMMAND_BAR.COMMANDS.MUTE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.CONVERSATION', + icon: ICON_MUTE_CONVERSATION, + handler: () => bus.$emit(CMD_MUTE_CONVERSATION), +}; + +export const isAConversationRoute = routeName => + [ + 'inbox_conversation', + 'conversation_through_inbox', + 'conversations_through_label', + 'conversations_through_team', + ].includes(routeName); + +export default { + watch: { + assignableAgents() { + this.setCommandbarData(); + }, + currentChat() { + this.setCommandbarData(); + }, + teamsList() { + this.setCommandbarData(); + }, + activeLabels() { + this.setCommandbarData(); + }, + }, + computed: { + ...mapGetters({ currentChat: 'getSelectedChat' }), + inboxId() { + return this.currentChat?.inbox_id; + }, + conversationId() { + return this.currentChat?.id; + }, + statusActions() { + const isOpen = + this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN; + const isSnoozed = + this.currentChat?.status === wootConstants.STATUS_TYPE.SNOOZED; + const isResolved = + this.currentChat?.status === wootConstants.STATUS_TYPE.RESOLVED; + + let actions = []; + if (isOpen) { + actions = OPEN_CONVERSATION_ACTIONS; + } else if (isResolved || isSnoozed) { + actions = RESOLVED_CONVERSATION_ACTIONS; + } + return this.prepareActions(actions); + }, + assignAgentActions() { + const agentOptions = this.agentsList.map(agent => ({ + id: `agent-${agent.id}`, + title: agent.name, + parent: 'assign_an_agent', + section: this.$t('COMMAND_BAR.SECTIONS.CHANGE_ASSIGNEE'), + agentInfo: agent, + icon: ICON_ASSIGN_AGENT, + handler: this.onChangeAssignee, + })); + return [ + { + id: 'assign_an_agent', + title: this.$t('COMMAND_BAR.COMMANDS.ASSIGN_AN_AGENT'), + section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_ASSIGN_AGENT, + children: agentOptions.map(option => option.id), + }, + ...agentOptions, + ]; + }, + assignTeamActions() { + const teamOptions = this.teamsList.map(team => ({ + id: `team-${team.id}`, + title: team.name, + parent: 'assign_a_team', + section: this.$t('COMMAND_BAR.SECTIONS.CHANGE_TEAM'), + teamInfo: team, + icon: ICON_ASSIGN_TEAM, + handler: this.onChangeTeam, + })); + return [ + { + id: 'assign_a_team', + title: this.$t('COMMAND_BAR.COMMANDS.ASSIGN_A_TEAM'), + section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_ASSIGN_TEAM, + children: teamOptions.map(option => option.id), + }, + ...teamOptions, + ]; + }, + + addLabelActions() { + const availableLabels = this.inactiveLabels.map(label => ({ + id: label.title, + title: `#${label.title}`, + parent: 'add_a_label_to_the_conversation', + section: this.$t('COMMAND_BAR.SECTIONS.ADD_LABEL'), + icon: ICON_ADD_LABEL, + handler: action => this.addLabelToConversation({ title: action.id }), + })); + return [ + ...availableLabels, + { + id: 'add_a_label_to_the_conversation', + title: this.$t('COMMAND_BAR.COMMANDS.ADD_LABELS_TO_CONVERSATION'), + section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_ADD_LABEL, + children: this.inactiveLabels.map(label => label.title), + }, + ]; + }, + removeLabelActions() { + const activeLabels = this.activeLabels.map(label => ({ + id: label.title, + title: `#${label.title}`, + parent: 'remove_a_label_to_the_conversation', + section: this.$t('COMMAND_BAR.SECTIONS.REMOVE_LABEL'), + icon: ICON_REMOVE_LABEL, + handler: action => this.removeLabelFromConversation(action.id), + })); + return [ + ...activeLabels, + { + id: 'remove_a_label_to_the_conversation', + title: this.$t('COMMAND_BAR.COMMANDS.REMOVE_LABEL_FROM_CONVERSATION'), + section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_REMOVE_LABEL, + children: this.activeLabels.map(label => label.title), + }, + ]; + }, + labelActions() { + if (this.activeLabels.length) { + return [...this.addLabelActions, ...this.removeLabelActions]; + } + return this.addLabelActions; + }, + conversationAdditionalActions() { + return this.prepareActions([ + this.currentChat.muted ? UNMUTE_ACTION : MUTE_ACTION, + SEND_TRANSCRIPT_ACTION, + ]); + }, + conversationHotKeys() { + if (isAConversationRoute(this.$route.name)) { + return [ + ...this.statusActions, + ...this.conversationAdditionalActions, + ...this.assignAgentActions, + ...this.assignTeamActions, + ...this.labelActions, + ]; + } + + return []; + }, + }, + + methods: { + onChangeAssignee(action) { + this.$store.dispatch('assignAgent', { + conversationId: this.currentChat.id, + agentId: action.agentInfo.id, + }); + }, + onChangeTeam(action) { + this.$store.dispatch('assignTeam', { + conversationId: this.currentChat.id, + teamId: action.teamInfo.id, + }); + }, + prepareActions(actions) { + return actions.map(action => ({ + ...action, + title: this.$t(action.title), + section: this.$t(action.section), + })); + }, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js new file mode 100644 index 000000000..5fee0f6f2 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js @@ -0,0 +1,173 @@ +import { + ICON_ACCOUNT_SETTINGS, + ICON_AGENT_REPORTS, + ICON_APPS, + ICON_CANNED_RESPONSE, + ICON_CONTACT_DASHBOARD, + ICON_CONVERSATION_DASHBOARD, + ICON_INBOXES, + ICON_INBOX_REPORTS, + ICON_LABELS, + ICON_LABEL_REPORTS, + ICON_NOTIFICATION, + ICON_REPORTS_OVERVIEW, + ICON_TEAM_REPORTS, + ICON_USER_PROFILE, +} from './CommandBarIcons'; +import { frontendURL } from '../../../helper/URLHelper'; + +const GO_TO_COMMANDS = [ + { + id: 'goto_conversation_dashboard', + title: 'COMMAND_BAR.COMMANDS.GO_TO_CONVERSATION_DASHBOARD', + section: 'COMMAND_BAR.SECTIONS.GENERAL', + icon: ICON_CONVERSATION_DASHBOARD, + path: accountId => `accounts/${accountId}/dashboard`, + role: ['administrator', 'agent'], + }, + { + id: 'goto_contacts_dashboard', + title: 'COMMAND_BAR.COMMANDS.GO_TO_CONTACTS_DASHBOARD', + section: 'COMMAND_BAR.SECTIONS.GENERAL', + icon: ICON_CONTACT_DASHBOARD, + path: accountId => `accounts/${accountId}/contacts`, + role: ['administrator', 'agent'], + }, + { + id: 'open_reports_overview', + section: 'COMMAND_BAR.SECTIONS.REPORTS', + title: 'COMMAND_BAR.COMMANDS.GO_TO_REPORTS_OVERVIEW', + icon: ICON_REPORTS_OVERVIEW, + path: accountId => `accounts/${accountId}/reports/overview`, + role: ['administrator'], + }, + { + id: 'open_agent_reports', + section: 'COMMAND_BAR.SECTIONS.REPORTS', + title: 'COMMAND_BAR.COMMANDS.GO_TO_AGENT_REPORTS', + icon: ICON_AGENT_REPORTS, + path: accountId => `accounts/${accountId}/reports/agent`, + role: ['administrator'], + }, + { + id: 'open_label_reports', + section: 'COMMAND_BAR.SECTIONS.REPORTS', + title: 'COMMAND_BAR.COMMANDS.GO_TO_LABEL_REPORTS', + icon: ICON_LABEL_REPORTS, + path: accountId => `accounts/${accountId}/reports/label`, + role: ['administrator'], + }, + { + id: 'open_inbox_reports', + section: 'COMMAND_BAR.SECTIONS.REPORTS', + title: 'COMMAND_BAR.COMMANDS.GO_TO_INBOX_REPORTS', + icon: ICON_INBOX_REPORTS, + path: accountId => `accounts/${accountId}/reports/inboxes`, + role: ['administrator'], + }, + { + id: 'open_team_reports', + section: 'COMMAND_BAR.SECTIONS.REPORTS', + title: 'COMMAND_BAR.COMMANDS.GO_TO_TEAM_REPORTS', + icon: ICON_TEAM_REPORTS, + path: accountId => `accounts/${accountId}/reports/teams`, + role: ['administrator'], + }, + { + id: 'open_agent_settings', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_AGENTS', + icon: ICON_AGENT_REPORTS, + path: accountId => `accounts/${accountId}/settings/agents/list`, + role: ['administrator'], + }, + { + id: 'open_team_settings', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_TEAMS', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_TEAM_REPORTS, + path: accountId => `accounts/${accountId}/settings/teams/list`, + role: ['administrator'], + }, + { + id: 'open_inbox_settings', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_INBOXES', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_INBOXES, + path: accountId => `accounts/${accountId}/settings/inboxes/list`, + role: ['administrator'], + }, + { + id: 'open_label_settings', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_LABELS', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_LABELS, + path: accountId => `accounts/${accountId}/settings/labels/list`, + role: ['administrator'], + }, + { + id: 'open_canned_response_settings', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_CANNED_RESPONSES', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_CANNED_RESPONSE, + path: accountId => `accounts/${accountId}/settings/canned-response/list`, + role: ['administrator', 'agent'], + }, + { + id: 'open_applications_settings', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_APPLICATIONS', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_APPS, + path: accountId => `accounts/${accountId}/settings/applications`, + role: ['administrator'], + }, + { + id: 'open_account_settings', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_ACCOUNT', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_ACCOUNT_SETTINGS, + path: accountId => `accounts/${accountId}/settings/general`, + role: ['administrator'], + }, + { + id: 'open_profile_settings', + title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_PROFILE', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_USER_PROFILE, + path: accountId => `accounts/${accountId}/profile/settings`, + role: ['administrator', 'agent'], + }, + { + id: 'open_notifications', + title: 'COMMAND_BAR.COMMANDS.GO_TO_NOTIFICATIONS', + section: 'COMMAND_BAR.SECTIONS.SETTINGS', + icon: ICON_NOTIFICATION, + path: accountId => `accounts/${accountId}/notifications`, + role: ['administrator', 'agent'], + }, +]; + +export default { + computed: { + goToCommandHotKeys() { + let commands = GO_TO_COMMANDS; + + if (!this.isAdmin) { + commands = commands.filter(command => command.role.includes('agent')); + } + + return commands.map(command => ({ + id: command.id, + section: this.$t(command.section), + title: this.$t(command.title), + icon: command.icon, + handler: () => this.openRoute(command.path(this.accountId)), + })); + }, + }, + methods: { + openRoute(url) { + this.$router.push(frontendURL(url)); + }, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/commands/specs/conversationHotKeys.spec.js b/app/javascript/dashboard/routes/dashboard/commands/specs/conversationHotKeys.spec.js new file mode 100644 index 000000000..a7dd197fd --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/commands/specs/conversationHotKeys.spec.js @@ -0,0 +1,11 @@ +import { isAConversationRoute } from '../conversationHotKeys'; + +describe('isAConversationRoute', () => { + it('returns true if conversation route name is provided', () => { + expect(isAConversationRoute('inbox_conversation')).toBe(true); + expect(isAConversationRoute('conversation_through_inbox')).toBe(true); + expect(isAConversationRoute('conversations_through_label')).toBe(true); + expect(isAConversationRoute('conversations_through_team')).toBe(true); + expect(isAConversationRoute('dashboard')).toBe(false); + }); +}); diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue index f5a540834..b5bfa487a 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue @@ -99,8 +99,6 @@ diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index c8483f4b9..208ce6648 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -1,5 +1,3 @@ -/* eslint no-console: 0 */ -/* eslint no-param-reassign: 0 */ import Vue from 'vue'; import * as types from '../../mutation-types'; import getters, { getSelectedChatConversation } from './getters'; @@ -76,7 +74,7 @@ export const mutations = { const [chat] = getSelectedChatConversation(_state); Vue.set(chat, 'custom_attributes', custom_attributes); }, - + [types.default.CHANGE_CONVERSATION_STATUS]( _state, { conversationId, status, snoozedUntil } @@ -89,12 +87,12 @@ export const mutations = { [types.default.MUTE_CONVERSATION](_state) { const [chat] = getSelectedChatConversation(_state); - chat.muted = true; + Vue.set(chat, 'muted', true); }, [types.default.UNMUTE_CONVERSATION](_state) { const [chat] = getSelectedChatConversation(_state); - chat.muted = false; + Vue.set(chat, 'muted', false); }, [types.default.ADD_MESSAGE]({ allConversations, selectedChatId }, message) { diff --git a/package.json b/package.json index d74e1f537..f76defa78 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "lodash.groupby": "^4.6.0", "marked": "2.0.3", "md5": "^2.3.0", + "ninja-keys": "https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902", "posthog-js": "^1.13.7", "prosemirror-markdown": "1.5.1", "prosemirror-state": "1.3.4", diff --git a/yarn.lock b/yarn.lock index aa0ae5567..a0a662bac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1511,6 +1511,19 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@lit/reactive-element@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294" + integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ== + +"@material/mwc-icon@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@material/mwc-icon/-/mwc-icon-0.25.3.tgz#8b646e45f16a449553e89901684c026ff4f465a0" + integrity sha512-36076AWZIRSr8qYOLjuDDkxej/HA0XAosrj7TS1ZeLlUBnLUtbDtvc1S7KSa0hqez7ouzOqGaWK24yoNnTa2OA== + dependencies: + lit "^2.0.0" + tslib "^2.0.1" + "@mdx-js/loader@^1.6.22": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4" @@ -2712,6 +2725,11 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4" integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ== +"@types/trusted-types@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + "@types/uglify-js@*": version "3.13.0" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124" @@ -7623,6 +7641,11 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +hotkeys-js@3.8.7: + version "3.8.7" + resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.8.7.tgz#c16cab978b53d7242f860ca3932e976b92399981" + integrity sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg== + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -9482,6 +9505,30 @@ listr@^0.14.3: p-map "^2.0.0" rxjs "^6.3.3" +lit-element@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4" + integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew== + dependencies: + "@lit/reactive-element" "^1.0.0" + lit-html "^2.0.0" + +lit-html@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95" + integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w== + dependencies: + "@types/trusted-types" "^2.0.2" + +lit@2.0.2, lit@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c" + integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw== + dependencies: + "@lit/reactive-element" "^1.0.0" + lit-element "^3.0.0" + lit-html "^2.0.0" + load-json-file@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" @@ -10203,6 +10250,14 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +"ninja-keys@https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902": + version "1.1.6" + resolved "https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902" + dependencies: + "@material/mwc-icon" "0.25.3" + hotkeys-js "3.8.7" + lit "2.0.2" + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -14456,6 +14511,11 @@ tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tslib@^2.0.3: version "2.2.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"