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"