diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue index 0626ced59..0ff363874 100644 --- a/app/javascript/dashboard/components/buttons/ResolveAction.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -13,7 +13,7 @@ import wootConstants from 'dashboard/constants/globals'; import { CMD_REOPEN_CONVERSATION, CMD_RESOLVE_CONVERSATION, -} from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; +} from 'dashboard/helper/commandbar/events'; const store = useStore(); const getters = useStoreGetters(); diff --git a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue index 360599631..96fe2830a 100644 --- a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue +++ b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue @@ -7,7 +7,7 @@ import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; import { useAI } from 'dashboard/composables/useAI'; import AICTAModal from './AICTAModal.vue'; import AIAssistanceModal from './AIAssistanceModal.vue'; -import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; +import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events'; import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue'; export default { diff --git a/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue b/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue index dcfc51b2a..2fc6a0752 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MoreActions.vue @@ -7,7 +7,7 @@ import { CMD_MUTE_CONVERSATION, CMD_SEND_TRANSCRIPT, CMD_UNMUTE_CONVERSATION, -} from '../../../routes/dashboard/commands/commandBarBusEvents'; +} from 'dashboard/helper/commandbar/events'; export default { components: { diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue index 746b932ca..b0fcaeb4b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue @@ -6,7 +6,7 @@ import { CMD_BULK_ACTION_SNOOZE_CONVERSATION, CMD_BULK_ACTION_REOPEN_CONVERSATION, CMD_BULK_ACTION_RESOLVE_CONVERSATION, -} from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; +} from 'dashboard/helper/commandbar/events'; import AgentSelector from './AgentSelector.vue'; import UpdateActions from './UpdateActions.vue'; diff --git a/app/javascript/dashboard/composables/commands/spec/fixtures.js b/app/javascript/dashboard/composables/commands/spec/fixtures.js new file mode 100644 index 000000000..d4f011f55 --- /dev/null +++ b/app/javascript/dashboard/composables/commands/spec/fixtures.js @@ -0,0 +1,197 @@ +export const mockAssignableAgents = [ + { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@doe.com', + available_name: 'John Doe', + name: 'John Doe', + role: 'administrator', + thumbnail: '', + }, +]; + +export const mockCurrentChat = { + meta: { + sender: { + additional_attributes: {}, + availability_status: 'offline', + email: null, + id: 212, + name: 'Chatwoot', + phone_number: null, + identifier: null, + thumbnail: '', + custom_attributes: {}, + last_activity_at: 1723553344, + created_at: 1722588710, + }, + channel: 'Channel::WebWidget', + assignee: { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@doe.com', + available_name: 'John Doe', + name: 'John Doe', + role: 'administrator', + thumbnail: '', + }, + hmac_verified: false, + }, + id: 138, + messages: [ + { + id: 3348, + content: 'Hello, how can I assist you today?', + account_id: 1, + inbox_id: 1, + conversation_id: 138, + message_type: 1, + created_at: 1724398739, + updated_at: '2024-08-23T07:38:59.763Z', + private: false, + status: 'sent', + source_id: null, + content_type: 'text', + content_attributes: {}, + sender_type: 'User', + sender_id: 1, + external_source_ids: {}, + additional_attributes: {}, + processed_message_content: 'Hello, how can I assist you today?', + sentiment: {}, + conversation: { + assignee_id: 1, + unread_count: 0, + last_activity_at: 1724398739, + contact_inbox: { + source_id: '5e57317d-053b-4a72-8292-a25b9f29c401', + }, + }, + sender: { + id: 1, + name: 'John Doe', + available_name: 'John Doe', + avatar_url: '', + type: 'user', + availability_status: 'online', + thumbnail: '', + }, + }, + ], + account_id: 1, + uuid: '69dd6922-2f0c-4317-8796-bbeb3679cead', + additional_attributes: { + browser: { + device_name: 'Unknown', + browser_name: 'Chrome', + platform_name: 'macOS', + browser_version: '127.0.0.0', + platform_version: '10.15.7', + }, + referer: 'http://chatwoot.com/widget_tests?dark_mode=auto', + initiated_at: { + timestamp: 'Fri Aug 02 2024 15:21:18 GMT+0530 (India Standard Time)', + }, + browser_language: 'en', + }, + agent_last_seen_at: 1724400730, + assignee_last_seen_at: 1724400686, + can_reply: true, + contact_last_seen_at: 1723553351, + custom_attributes: {}, + inbox_id: 1, + labels: ['billing'], + muted: false, + snoozed_until: null, + status: 'open', + created_at: 1722592278, + timestamp: 1724398739, + first_reply_created_at: 1722592316, + unread_count: 0, + last_non_activity_message: {}, + last_activity_at: 1724398739, + priority: null, + waiting_since: 0, + sla_policy_id: 10, + applied_sla: { + id: 143, + sla_id: 10, + sla_status: 'missed', + created_at: 1722592279, + updated_at: 1722874214, + sla_description: '', + sla_name: 'Hacker SLA', + sla_first_response_time_threshold: 600, + sla_next_response_time_threshold: 240, + sla_only_during_business_hours: false, + sla_resolution_time_threshold: 259200, + }, + sla_events: [ + { + id: 270, + event_type: 'nrt', + meta: { + message_id: 2743, + }, + updated_at: 1722592819, + created_at: 1722592819, + }, + { + id: 275, + event_type: 'rt', + meta: {}, + updated_at: 1722852322, + created_at: 1722852322, + }, + ], + allMessagesLoaded: false, + dataFetched: true, +}; + +export const mockTeamsList = [ + { + id: 5, + name: 'design', + description: 'design team', + allow_auto_assign: true, + account_id: 1, + is_member: false, + }, +]; + +export const mockActiveLabels = [ + { + id: 16, + title: 'billing', + description: '', + color: '#D8EA19', + show_on_sidebar: true, + }, +]; + +export const mockInactiveLabels = [ + { + id: 2, + title: 'Feature Request', + description: '', + color: '#D8EA19', + show_on_sidebar: true, + }, +]; + +export const MOCK_FEATURE_FLAGS = { + CRM: 'crm', + AGENT_MANAGEMENT: 'agent_management', + TEAM_MANAGEMENT: 'team_management', + INBOX_MANAGEMENT: 'inbox_management', + REPORTS: 'reports', + LABELS: 'labels', + CANNED_RESPONSES: 'canned_responses', + INTEGRATIONS: 'integrations', +}; diff --git a/app/javascript/dashboard/composables/commands/spec/useAppearanceHotKeys.spec.js b/app/javascript/dashboard/composables/commands/spec/useAppearanceHotKeys.spec.js new file mode 100644 index 000000000..34057f6e1 --- /dev/null +++ b/app/javascript/dashboard/composables/commands/spec/useAppearanceHotKeys.spec.js @@ -0,0 +1,83 @@ +import { useAppearanceHotKeys } from '../useAppearanceHotKeys'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { LocalStorage } from 'shared/helpers/localStorage'; +import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; +import { setColorTheme } from 'dashboard/helper/themeHelper.js'; + +vi.mock('dashboard/composables/useI18n'); +vi.mock('shared/helpers/localStorage'); +vi.mock('dashboard/helper/themeHelper.js'); + +describe('useAppearanceHotKeys', () => { + beforeEach(() => { + useI18n.mockReturnValue({ + t: vi.fn(key => key), + }); + + window.matchMedia = vi.fn().mockReturnValue({ matches: false }); + }); + + it('should return goToAppearanceHotKeys computed property', () => { + const { goToAppearanceHotKeys } = useAppearanceHotKeys(); + expect(goToAppearanceHotKeys.value).toBeDefined(); + }); + + it('should have the correct number of appearance options', () => { + const { goToAppearanceHotKeys } = useAppearanceHotKeys(); + expect(goToAppearanceHotKeys.value.length).toBe(4); // 1 parent + 3 theme options + }); + + it('should have the correct parent option', () => { + const { goToAppearanceHotKeys } = useAppearanceHotKeys(); + const parentOption = goToAppearanceHotKeys.value.find( + option => option.id === 'appearance_settings' + ); + expect(parentOption).toBeDefined(); + expect(parentOption.children.length).toBe(3); + }); + + it('should have the correct theme options', () => { + const { goToAppearanceHotKeys } = useAppearanceHotKeys(); + const themeOptions = goToAppearanceHotKeys.value.filter( + option => option.parent === 'appearance_settings' + ); + expect(themeOptions.length).toBe(3); + expect(themeOptions.map(option => option.id)).toEqual([ + 'light', + 'dark', + 'auto', + ]); + }); + + it('should call setAppearance when a theme option is selected', () => { + const { goToAppearanceHotKeys } = useAppearanceHotKeys(); + const lightThemeOption = goToAppearanceHotKeys.value.find( + option => option.id === 'light' + ); + + lightThemeOption.handler(); + + expect(LocalStorage.set).toHaveBeenCalledWith( + LOCAL_STORAGE_KEYS.COLOR_SCHEME, + 'light' + ); + expect(setColorTheme).toHaveBeenCalledWith(false); + }); + + it('should handle system dark mode preference', () => { + window.matchMedia = vi.fn().mockReturnValue({ matches: true }); + + const { goToAppearanceHotKeys } = useAppearanceHotKeys(); + const autoThemeOption = goToAppearanceHotKeys.value.find( + option => option.id === 'auto' + ); + + autoThemeOption.handler(); + + expect(LocalStorage.set).toHaveBeenCalledWith( + LOCAL_STORAGE_KEYS.COLOR_SCHEME, + 'auto' + ); + expect(setColorTheme).toHaveBeenCalledWith(true); + }); +}); diff --git a/app/javascript/dashboard/composables/commands/spec/useBulkActionsHotKeys.spec.js b/app/javascript/dashboard/composables/commands/spec/useBulkActionsHotKeys.spec.js new file mode 100644 index 000000000..0a755457b --- /dev/null +++ b/app/javascript/dashboard/composables/commands/spec/useBulkActionsHotKeys.spec.js @@ -0,0 +1,102 @@ +import { useBulkActionsHotKeys } from '../useBulkActionsHotKeys'; +import { useStore, useMapGetter } from 'dashboard/composables/store'; +import { useI18n } from 'dashboard/composables/useI18n'; +import wootConstants from 'dashboard/constants/globals'; +import { emitter } from 'shared/helpers/mitt'; + +vi.mock('dashboard/composables/store'); +vi.mock('dashboard/composables/useI18n'); +vi.mock('shared/helpers/mitt'); + +describe('useBulkActionsHotKeys', () => { + let store; + + beforeEach(() => { + store = { + getters: { + 'bulkActions/getSelectedConversationIds': [], + }, + }; + + useStore.mockReturnValue(store); + useMapGetter.mockImplementation(key => ({ + value: store.getters[key], + })); + + useI18n.mockReturnValue({ t: vi.fn(key => key) }); + emitter.emit = vi.fn(); + }); + + it('should return bulk actions when conversations are selected', () => { + store.getters['bulkActions/getSelectedConversationIds'] = [1, 2, 3]; + const { bulkActionsHotKeys } = useBulkActionsHotKeys(); + + expect(bulkActionsHotKeys.value.length).toBeGreaterThan(0); + expect(bulkActionsHotKeys.value).toContainEqual( + expect.objectContaining({ + id: 'bulk_action_snooze_conversation', + title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', + }) + ); + expect(bulkActionsHotKeys.value).toContainEqual( + expect.objectContaining({ + id: 'bulk_action_reopen_conversation', + title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', + }) + ); + expect(bulkActionsHotKeys.value).toContainEqual( + expect.objectContaining({ + id: 'bulk_action_resolve_conversation', + title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', + }) + ); + }); + + it('should include snooze options in bulk actions', () => { + store.getters['bulkActions/getSelectedConversationIds'] = [1, 2, 3]; + const { bulkActionsHotKeys } = useBulkActionsHotKeys(); + + const snoozeAction = bulkActionsHotKeys.value.find( + action => action.id === 'bulk_action_snooze_conversation' + ); + expect(snoozeAction).toBeDefined(); + expect(snoozeAction.children).toEqual( + Object.values(wootConstants.SNOOZE_OPTIONS) + ); + }); + + it('should create handlers for reopen and resolve actions', () => { + store.getters['bulkActions/getSelectedConversationIds'] = [1, 2, 3]; + const { bulkActionsHotKeys } = useBulkActionsHotKeys(); + + const reopenAction = bulkActionsHotKeys.value.find( + action => action.id === 'bulk_action_reopen_conversation' + ); + const resolveAction = bulkActionsHotKeys.value.find( + action => action.id === 'bulk_action_resolve_conversation' + ); + + expect(reopenAction.handler).toBeDefined(); + expect(resolveAction.handler).toBeDefined(); + + reopenAction.handler(); + expect(emitter.emit).toHaveBeenCalledWith( + 'CMD_BULK_ACTION_REOPEN_CONVERSATION' + ); + + resolveAction.handler(); + expect(emitter.emit).toHaveBeenCalledWith( + 'CMD_BULK_ACTION_RESOLVE_CONVERSATION' + ); + }); + + it('should return an empty array when no conversations are selected', () => { + store.getters['bulkActions/getSelectedConversationIds'] = []; + const { bulkActionsHotKeys } = useBulkActionsHotKeys(); + + expect(bulkActionsHotKeys.value).toEqual([]); + }); +}); diff --git a/app/javascript/dashboard/composables/commands/spec/useConversationHotKeys.spec.js b/app/javascript/dashboard/composables/commands/spec/useConversationHotKeys.spec.js new file mode 100644 index 000000000..f0b858abf --- /dev/null +++ b/app/javascript/dashboard/composables/commands/spec/useConversationHotKeys.spec.js @@ -0,0 +1,204 @@ +import { useConversationHotKeys } from '../useConversationHotKeys'; +import { useStore, useMapGetter } from 'dashboard/composables/store'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { useRoute } from 'dashboard/composables/route'; +import { useConversationLabels } from 'dashboard/composables/useConversationLabels'; +import { useAI } from 'dashboard/composables/useAI'; +import { useAgentsList } from 'dashboard/composables/useAgentsList'; +import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; +import { + mockAssignableAgents, + mockCurrentChat, + mockTeamsList, + mockActiveLabels, + mockInactiveLabels, +} from './fixtures'; + +vi.mock('dashboard/composables/store'); +vi.mock('dashboard/composables/useI18n'); +vi.mock('dashboard/composables/route'); +vi.mock('dashboard/composables/useConversationLabels'); +vi.mock('dashboard/composables/useAI'); +vi.mock('dashboard/composables/useAgentsList'); + +describe('useConversationHotKeys', () => { + let store; + + beforeEach(() => { + store = { + dispatch: vi.fn(), + getters: { + getSelectedChat: mockCurrentChat, + 'draftMessages/getReplyEditorMode': REPLY_EDITOR_MODES.REPLY, + getContextMenuChatId: null, + 'teams/getTeams': mockTeamsList, + 'draftMessages/get': vi.fn(), + }, + }; + + useStore.mockReturnValue(store); + useMapGetter.mockImplementation(key => ({ + value: store.getters[key], + })); + + useI18n.mockReturnValue({ t: vi.fn(key => key) }); + useRoute.mockReturnValue({ name: 'inbox_conversation' }); + useConversationLabels.mockReturnValue({ + activeLabels: { value: mockActiveLabels }, + inactiveLabels: { value: mockInactiveLabels }, + addLabelToConversation: vi.fn(), + removeLabelFromConversation: vi.fn(), + }); + useAI.mockReturnValue({ isAIIntegrationEnabled: { value: true } }); + useAgentsList.mockReturnValue({ + agentsList: { value: [] }, + assignableAgents: { value: mockAssignableAgents }, + }); + }); + + it('should return the correct computed properties', () => { + const { conversationHotKeys } = useConversationHotKeys(); + + expect(conversationHotKeys.value).toBeDefined(); + }); + + it('should generate conversation hot keys', () => { + const { conversationHotKeys } = useConversationHotKeys(); + expect(conversationHotKeys.value.length).toBeGreaterThan(0); + }); + + it('should include AI assist actions when AI integration is enabled', () => { + const { conversationHotKeys } = useConversationHotKeys(); + const aiAssistAction = conversationHotKeys.value.find( + action => action.id === 'ai_assist' + ); + expect(aiAssistAction).toBeDefined(); + }); + + it('should not include AI assist actions when AI integration is disabled', () => { + useAI.mockReturnValue({ isAIIntegrationEnabled: { value: false } }); + const { conversationHotKeys } = useConversationHotKeys(); + const aiAssistAction = conversationHotKeys.value.find( + action => action.id === 'ai_assist' + ); + expect(aiAssistAction).toBeUndefined(); + }); + + it('should dispatch actions when handlers are called', () => { + const { conversationHotKeys } = useConversationHotKeys(); + const assignAgentAction = conversationHotKeys.value.find( + action => action.id === 'assign_an_agent' + ); + expect(assignAgentAction).toBeDefined(); + + if (assignAgentAction && assignAgentAction.children) { + const childAction = conversationHotKeys.value.find( + action => action.id === assignAgentAction.children[0] + ); + if (childAction && childAction.handler) { + childAction.handler({ agentInfo: { id: 2 } }); + expect(store.dispatch).toHaveBeenCalledWith('assignAgent', { + conversationId: 1, + agentId: 2, + }); + } + } + }); + + it('should return snooze actions when in snooze context', () => { + store.getters.getContextMenuChatId = 1; + useMapGetter.mockImplementation(key => ({ + value: store.getters[key], + })); + useRoute.mockReturnValue({ name: 'inbox_conversation' }); + + const { conversationHotKeys } = useConversationHotKeys(); + const snoozeAction = conversationHotKeys.value.find(action => + action.id.includes('snooze_conversation') + ); + expect(snoozeAction).toBeDefined(); + }); + + it('should return the correct label actions when there are active labels', () => { + const { conversationHotKeys } = useConversationHotKeys(); + const addLabelAction = conversationHotKeys.value.find( + action => action.id === 'add_a_label_to_the_conversation' + ); + const removeLabelAction = conversationHotKeys.value.find( + action => action.id === 'remove_a_label_to_the_conversation' + ); + + expect(addLabelAction).toBeDefined(); + expect(removeLabelAction).toBeDefined(); + }); + + it('should return only add label actions when there are no active labels', () => { + useConversationLabels.mockReturnValue({ + activeLabels: { value: [] }, + inactiveLabels: { value: [{ title: 'inactive_label' }] }, + addLabelToConversation: vi.fn(), + removeLabelFromConversation: vi.fn(), + }); + + const { conversationHotKeys } = useConversationHotKeys(); + const addLabelAction = conversationHotKeys.value.find( + action => action.id === 'add_a_label_to_the_conversation' + ); + const removeLabelAction = conversationHotKeys.value.find( + action => action.id === 'remove_a_label_to_the_conversation' + ); + + expect(addLabelAction).toBeDefined(); + expect(removeLabelAction).toBeUndefined(); + }); + + it('should return the correct team assignment actions', () => { + const { conversationHotKeys } = useConversationHotKeys(); + const assignTeamAction = conversationHotKeys.value.find( + action => action.id === 'assign_a_team' + ); + + expect(assignTeamAction).toBeDefined(); + expect(assignTeamAction.children.length).toBe(mockTeamsList.length); + }); + + it('should return the correct priority assignment actions', () => { + const { conversationHotKeys } = useConversationHotKeys(); + const assignPriorityAction = conversationHotKeys.value.find( + action => action.id === 'assign_priority' + ); + + expect(assignPriorityAction).toBeDefined(); + expect(assignPriorityAction.children.length).toBe(4); + }); + + it('should return the correct conversation additional actions', () => { + const { conversationHotKeys } = useConversationHotKeys(); + const muteAction = conversationHotKeys.value.find( + action => action.id === 'mute_conversation' + ); + const sendTranscriptAction = conversationHotKeys.value.find( + action => action.id === 'send_transcript' + ); + + expect(muteAction).toBeDefined(); + expect(sendTranscriptAction).toBeDefined(); + }); + + it('should return unmute action when conversation is muted', () => { + store.getters.getSelectedChat = { ...mockCurrentChat, muted: true }; + const { conversationHotKeys } = useConversationHotKeys(); + const unmuteAction = conversationHotKeys.value.find( + action => action.id === 'unmute_conversation' + ); + + expect(unmuteAction).toBeDefined(); + }); + + it('should not return conversation hot keys when not in conversation or inbox route', () => { + useRoute.mockReturnValue({ name: 'some_other_route' }); + const { conversationHotKeys } = useConversationHotKeys(); + + expect(conversationHotKeys.value.length).toBe(0); + }); +}); diff --git a/app/javascript/dashboard/composables/commands/spec/useGoToCommandHotKeys.spec.js b/app/javascript/dashboard/composables/commands/spec/useGoToCommandHotKeys.spec.js new file mode 100644 index 000000000..8eea16fdc --- /dev/null +++ b/app/javascript/dashboard/composables/commands/spec/useGoToCommandHotKeys.spec.js @@ -0,0 +1,188 @@ +import { useGoToCommandHotKeys } from '../useGoToCommandHotKeys'; +import { useStore, useMapGetter } from 'dashboard/composables/store'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { useRouter } from 'dashboard/composables/route'; +import { useAdmin } from 'dashboard/composables/useAdmin'; +import { frontendURL } from 'dashboard/helper/URLHelper'; +import { MOCK_FEATURE_FLAGS } from './fixtures'; + +vi.mock('dashboard/composables/store'); +vi.mock('dashboard/composables/useI18n'); +vi.mock('dashboard/composables/route'); +vi.mock('dashboard/composables/useAdmin'); +vi.mock('dashboard/helper/URLHelper'); + +const mockRoutes = [ + { path: 'accounts/:accountId/dashboard', name: 'dashboard' }, + { + path: 'accounts/:accountId/contacts', + name: 'contacts', + featureFlag: MOCK_FEATURE_FLAGS.CRM, + }, + { + path: 'accounts/:accountId/settings/agents/list', + name: 'agent_settings', + featureFlag: MOCK_FEATURE_FLAGS.AGENT_MANAGEMENT, + }, + { + path: 'accounts/:accountId/settings/teams/list', + name: 'team_settings', + featureFlag: MOCK_FEATURE_FLAGS.TEAM_MANAGEMENT, + }, + { + path: 'accounts/:accountId/settings/inboxes/list', + name: 'inbox_settings', + featureFlag: MOCK_FEATURE_FLAGS.INBOX_MANAGEMENT, + }, + { path: 'accounts/:accountId/profile/settings', name: 'profile_settings' }, + { path: 'accounts/:accountId/notifications', name: 'notifications' }, + { + path: 'accounts/:accountId/reports/overview', + name: 'reports_overview', + featureFlag: MOCK_FEATURE_FLAGS.REPORTS, + }, + { + path: 'accounts/:accountId/settings/labels/list', + name: 'label_settings', + featureFlag: MOCK_FEATURE_FLAGS.LABELS, + }, + { + path: 'accounts/:accountId/settings/canned-response/list', + name: 'canned_responses', + featureFlag: MOCK_FEATURE_FLAGS.CANNED_RESPONSES, + }, + { + path: 'accounts/:accountId/settings/applications', + name: 'applications', + featureFlag: MOCK_FEATURE_FLAGS.INTEGRATIONS, + }, +]; + +describe('useGoToCommandHotKeys', () => { + let store; + + beforeEach(() => { + store = { + getters: { + getCurrentAccountId: 1, + 'accounts/isFeatureEnabledonAccount': vi.fn().mockReturnValue(true), + }, + }; + + useStore.mockReturnValue(store); + useMapGetter.mockImplementation(key => ({ + value: store.getters[key], + })); + + useI18n.mockReturnValue({ t: vi.fn(key => key) }); + useRouter.mockReturnValue({ push: vi.fn() }); + useAdmin.mockReturnValue({ isAdmin: { value: true } }); + frontendURL.mockImplementation(url => url); + }); + + it('should return goToCommandHotKeys computed property', () => { + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + expect(goToCommandHotKeys.value).toBeDefined(); + expect(goToCommandHotKeys.value.length).toBeGreaterThan(0); + }); + + it('should filter commands based on feature flags', () => { + store.getters['accounts/isFeatureEnabledonAccount'] = vi.fn( + (accountId, flag) => flag !== MOCK_FEATURE_FLAGS.CRM + ); + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + + mockRoutes.forEach(route => { + const command = goToCommandHotKeys.value.find(cmd => + cmd.id.includes(route.name) + ); + if (route.featureFlag === MOCK_FEATURE_FLAGS.CRM) { + expect(command).toBeUndefined(); + } else if (!route.featureFlag) { + expect(command).toBeDefined(); + } + }); + }); + + it('should filter commands for non-admin users', () => { + useAdmin.mockReturnValue({ isAdmin: { value: false } }); + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + + const adminOnlyCommands = goToCommandHotKeys.value.filter( + cmd => + cmd.id.includes('agent_settings') || + cmd.id.includes('team_settings') || + cmd.id.includes('inbox_settings') + ); + expect(adminOnlyCommands.length).toBe(0); + }); + + it('should include commands for both admin and agent roles when user is admin', () => { + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + const adminCommand = goToCommandHotKeys.value.find(cmd => + cmd.id.includes('agent_settings') + ); + const agentCommand = goToCommandHotKeys.value.find(cmd => + cmd.id.includes('profile_settings') + ); + expect(adminCommand).toBeDefined(); + expect(agentCommand).toBeDefined(); + }); + + it('should translate section and title for each command', () => { + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + goToCommandHotKeys.value.forEach(command => { + expect(useI18n().t).toHaveBeenCalledWith( + expect.stringContaining('COMMAND_BAR.SECTIONS.') + ); + expect(useI18n().t).toHaveBeenCalledWith( + expect.stringContaining('COMMAND_BAR.COMMANDS.') + ); + expect(command.section).toBeDefined(); + expect(command.title).toBeDefined(); + }); + }); + + it('should call router.push with correct URL when handler is called', () => { + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + goToCommandHotKeys.value.forEach(command => { + command.handler(); + expect(useRouter().push).toHaveBeenCalledWith(expect.any(String)); + }); + }); + + it('should use current account ID in the path', () => { + store.getters.getCurrentAccountId = 42; + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + goToCommandHotKeys.value.forEach(command => { + command.handler(); + expect(useRouter().push).toHaveBeenCalledWith( + expect.stringContaining('42') + ); + }); + }); + + it('should include icon for each command', () => { + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + goToCommandHotKeys.value.forEach(command => { + expect(command.icon).toBeDefined(); + }); + }); + + it('should return commands for all enabled features', () => { + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + const enabledFeatureCommands = goToCommandHotKeys.value.filter(cmd => + mockRoutes.some(route => route.featureFlag && cmd.id.includes(route.name)) + ); + expect(enabledFeatureCommands.length).toBeGreaterThan(0); + }); + + it('should not return commands for disabled features', () => { + store.getters['accounts/isFeatureEnabledonAccount'] = vi.fn(() => false); + const { goToCommandHotKeys } = useGoToCommandHotKeys(); + const disabledFeatureCommands = goToCommandHotKeys.value.filter(cmd => + mockRoutes.some(route => route.featureFlag && cmd.id.includes(route.name)) + ); + expect(disabledFeatureCommands.length).toBe(0); + }); +}); diff --git a/app/javascript/dashboard/composables/commands/spec/useInboxHotKeys.spec.js b/app/javascript/dashboard/composables/commands/spec/useInboxHotKeys.spec.js new file mode 100644 index 000000000..95177586a --- /dev/null +++ b/app/javascript/dashboard/composables/commands/spec/useInboxHotKeys.spec.js @@ -0,0 +1,37 @@ +import { useInboxHotKeys } from '../useInboxHotKeys'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { useRoute } from 'dashboard/composables/route'; +import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers'; + +vi.mock('dashboard/composables/useI18n'); +vi.mock('dashboard/composables/route'); +vi.mock('dashboard/helper/routeHelpers'); +vi.mock('shared/helpers/mitt'); + +describe('useInboxHotKeys', () => { + beforeEach(() => { + useI18n.mockReturnValue({ t: vi.fn(key => key) }); + useRoute.mockReturnValue({ name: 'inbox_dashboard' }); + isAInboxViewRoute.mockReturnValue(true); + }); + + it('should return inbox hot keys when on an inbox view route', () => { + const { inboxHotKeys } = useInboxHotKeys(); + expect(inboxHotKeys.value.length).toBeGreaterThan(0); + expect(inboxHotKeys.value[0].id).toBe('snooze_notification'); + }); + + it('should return an empty array when not on an inbox view route', () => { + isAInboxViewRoute.mockReturnValue(false); + const { inboxHotKeys } = useInboxHotKeys(); + expect(inboxHotKeys.value).toEqual([]); + }); + + it('should have the correct structure for snooze actions', () => { + const { inboxHotKeys } = useInboxHotKeys(); + const snoozeNotificationAction = inboxHotKeys.value.find( + action => action.id === 'snooze_notification' + ); + expect(snoozeNotificationAction).toBeDefined(); + }); +}); diff --git a/app/javascript/dashboard/composables/commands/useAppearanceHotKeys.js b/app/javascript/dashboard/composables/commands/useAppearanceHotKeys.js new file mode 100644 index 000000000..3dd38b0cf --- /dev/null +++ b/app/javascript/dashboard/composables/commands/useAppearanceHotKeys.js @@ -0,0 +1,70 @@ +import { computed } from 'vue'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { + ICON_APPEARANCE, + ICON_LIGHT_MODE, + ICON_DARK_MODE, + ICON_SYSTEM_MODE, +} from 'dashboard/helper/commandbar/icons'; +import { LocalStorage } from 'shared/helpers/localStorage'; +import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; +import { setColorTheme } from 'dashboard/helper/themeHelper.js'; + +const getThemeOptions = t => [ + { + key: 'light', + label: t('COMMAND_BAR.COMMANDS.LIGHT_MODE'), + icon: ICON_LIGHT_MODE, + }, + { + key: 'dark', + label: t('COMMAND_BAR.COMMANDS.DARK_MODE'), + icon: ICON_DARK_MODE, + }, + { + key: 'auto', + label: t('COMMAND_BAR.COMMANDS.SYSTEM_MODE'), + icon: ICON_SYSTEM_MODE, + }, +]; + +const setAppearance = theme => { + LocalStorage.set(LOCAL_STORAGE_KEYS.COLOR_SCHEME, theme); + const isOSOnDarkMode = window.matchMedia( + '(prefers-color-scheme: dark)' + ).matches; + setColorTheme(isOSOnDarkMode); +}; + +export function useAppearanceHotKeys() { + const { t } = useI18n(); + + const themeOptions = computed(() => getThemeOptions(t)); + + const goToAppearanceHotKeys = computed(() => { + const options = themeOptions.value.map(theme => ({ + id: theme.key, + title: theme.label, + parent: 'appearance_settings', + section: t('COMMAND_BAR.SECTIONS.APPEARANCE'), + icon: theme.icon, + handler: () => { + setAppearance(theme.key); + }, + })); + return [ + { + id: 'appearance_settings', + title: t('COMMAND_BAR.COMMANDS.CHANGE_APPEARANCE'), + section: t('COMMAND_BAR.SECTIONS.APPEARANCE'), + icon: ICON_APPEARANCE, + children: options.map(option => option.id), + }, + ...options, + ]; + }); + + return { + goToAppearanceHotKeys, + }; +} diff --git a/app/javascript/dashboard/composables/commands/useBulkActionsHotKeys.js b/app/javascript/dashboard/composables/commands/useBulkActionsHotKeys.js new file mode 100644 index 000000000..8969e7cf4 --- /dev/null +++ b/app/javascript/dashboard/composables/commands/useBulkActionsHotKeys.js @@ -0,0 +1,89 @@ +import { computed } from 'vue'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { useMapGetter } from 'dashboard/composables/store'; +import wootConstants from 'dashboard/constants/globals'; + +import { + CMD_BULK_ACTION_SNOOZE_CONVERSATION, + CMD_BULK_ACTION_REOPEN_CONVERSATION, + CMD_BULK_ACTION_RESOLVE_CONVERSATION, +} from 'dashboard/helper/commandbar/events'; +import { + ICON_SNOOZE_CONVERSATION, + ICON_REOPEN_CONVERSATION, + ICON_RESOLVE_CONVERSATION, +} from 'dashboard/helper/commandbar/icons'; +import { emitter } from 'shared/helpers/mitt'; + +import { createSnoozeHandlers } from 'dashboard/helper/commandbar/actions'; + +const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS; + +const createEmitHandler = event => () => emitter.emit(event); + +const SNOOZE_CONVERSATION_BULK_ACTIONS = [ + { + id: 'bulk_action_snooze_conversation', + title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', + icon: ICON_SNOOZE_CONVERSATION, + children: Object.values(SNOOZE_OPTIONS), + }, + ...createSnoozeHandlers( + CMD_BULK_ACTION_SNOOZE_CONVERSATION, + 'bulk_action_snooze_conversation', + 'COMMAND_BAR.SECTIONS.BULK_ACTIONS' + ), +]; + +const RESOLVED_CONVERSATION_BULK_ACTIONS = [ + { + id: 'bulk_action_reopen_conversation', + title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', + icon: ICON_REOPEN_CONVERSATION, + handler: createEmitHandler(CMD_BULK_ACTION_REOPEN_CONVERSATION), + }, +]; + +const OPEN_CONVERSATION_BULK_ACTIONS = [ + { + id: 'bulk_action_resolve_conversation', + title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', + icon: ICON_RESOLVE_CONVERSATION, + handler: createEmitHandler(CMD_BULK_ACTION_RESOLVE_CONVERSATION), + }, +]; + +export function useBulkActionsHotKeys() { + const { t } = useI18n(); + + const selectedConversations = useMapGetter( + 'bulkActions/getSelectedConversationIds' + ); + + const prepareActions = actions => { + return actions.map(action => ({ + ...action, + title: t(action.title), + section: t(action.section), + })); + }; + + const bulkActionsHotKeys = computed(() => { + let actions = []; + if (selectedConversations.value.length > 0) { + actions = [ + ...SNOOZE_CONVERSATION_BULK_ACTIONS, + ...RESOLVED_CONVERSATION_BULK_ACTIONS, + ...OPEN_CONVERSATION_BULK_ACTIONS, + ]; + } + return prepareActions(actions); + }); + + return { + bulkActionsHotKeys, + }; +} diff --git a/app/javascript/dashboard/composables/commands/useConversationHotKeys.js b/app/javascript/dashboard/composables/commands/useConversationHotKeys.js new file mode 100644 index 000000000..6e6004e45 --- /dev/null +++ b/app/javascript/dashboard/composables/commands/useConversationHotKeys.js @@ -0,0 +1,408 @@ +import { computed } from 'vue'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { useStore, useMapGetter } from 'dashboard/composables/store'; +import { useRoute } from 'dashboard/composables/route'; +import { emitter } from 'shared/helpers/mitt'; +import { useConversationLabels } from 'dashboard/composables/useConversationLabels'; +import { useAI } from 'dashboard/composables/useAI'; +import { useAgentsList } from 'dashboard/composables/useAgentsList'; +import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events'; +import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; + +import wootConstants from 'dashboard/constants/globals'; + +import { + ICON_ADD_LABEL, + ICON_ASSIGN_AGENT, + ICON_ASSIGN_PRIORITY, + ICON_ASSIGN_TEAM, + ICON_REMOVE_LABEL, + ICON_PRIORITY_URGENT, + ICON_PRIORITY_HIGH, + ICON_PRIORITY_LOW, + ICON_PRIORITY_MEDIUM, + ICON_PRIORITY_NONE, + ICON_AI_ASSIST, + ICON_AI_SUMMARY, + ICON_AI_SHORTEN, + ICON_AI_EXPAND, + ICON_AI_GRAMMAR, +} from 'dashboard/helper/commandbar/icons'; + +import { + OPEN_CONVERSATION_ACTIONS, + SNOOZE_CONVERSATION_ACTIONS, + RESOLVED_CONVERSATION_ACTIONS, + SEND_TRANSCRIPT_ACTION, + UNMUTE_ACTION, + MUTE_ACTION, +} from 'dashboard/helper/commandbar/actions'; +import { + isAConversationRoute, + isAInboxViewRoute, +} from 'dashboard/helper/routeHelpers'; + +const prepareActions = (actions, t) => { + return actions.map(action => ({ + ...action, + title: t(action.title), + section: t(action.section), + })); +}; + +const createPriorityOptions = (t, currentPriority) => { + return [ + { + label: t('CONVERSATION.PRIORITY.OPTIONS.NONE'), + key: null, + icon: ICON_PRIORITY_NONE, + }, + { + label: t('CONVERSATION.PRIORITY.OPTIONS.URGENT'), + key: 'urgent', + icon: ICON_PRIORITY_URGENT, + }, + { + label: t('CONVERSATION.PRIORITY.OPTIONS.HIGH'), + key: 'high', + icon: ICON_PRIORITY_HIGH, + }, + { + label: t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'), + key: 'medium', + icon: ICON_PRIORITY_MEDIUM, + }, + { + label: t('CONVERSATION.PRIORITY.OPTIONS.LOW'), + key: 'low', + icon: ICON_PRIORITY_LOW, + }, + ].filter(item => item.key !== currentPriority); +}; + +const createNonDraftMessageAIAssistActions = (t, replyMode) => { + if (replyMode === REPLY_EDITOR_MODES.REPLY) { + return [ + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPLY_SUGGESTION'), + key: 'reply_suggestion', + icon: ICON_AI_ASSIST, + }, + ]; + } + return [ + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SUMMARIZE'), + key: 'summarize', + icon: ICON_AI_SUMMARY, + }, + ]; +}; + +const createDraftMessageAIAssistActions = t => { + return [ + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPHRASE'), + key: 'rephrase', + icon: ICON_AI_ASSIST, + }, + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.FIX_SPELLING_GRAMMAR'), + key: 'fix_spelling_grammar', + icon: ICON_AI_GRAMMAR, + }, + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.EXPAND'), + key: 'expand', + icon: ICON_AI_EXPAND, + }, + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SHORTEN'), + key: 'shorten', + icon: ICON_AI_SHORTEN, + }, + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FRIENDLY'), + key: 'make_friendly', + icon: ICON_AI_ASSIST, + }, + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FORMAL'), + key: 'make_formal', + icon: ICON_AI_ASSIST, + }, + { + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SIMPLIFY'), + key: 'simplify', + icon: ICON_AI_ASSIST, + }, + ]; +}; + +export function useConversationHotKeys() { + const { t } = useI18n(); + const store = useStore(); + const route = useRoute(); + + const { + activeLabels, + inactiveLabels, + addLabelToConversation, + removeLabelFromConversation, + } = useConversationLabels(); + + const { isAIIntegrationEnabled } = useAI(); + const { agentsList } = useAgentsList(); + + const currentChat = useMapGetter('getSelectedChat'); + const replyMode = useMapGetter('draftMessages/getReplyEditorMode'); + const contextMenuChatId = useMapGetter('getContextMenuChatId'); + const teams = useMapGetter('teams/getTeams'); + const getDraftMessage = useMapGetter('draftMessages/get'); + + const conversationId = computed(() => currentChat.value?.id); + const draftKey = computed( + () => `draft-${conversationId.value}-${replyMode.value}` + ); + + const draftMessage = computed(() => getDraftMessage.value(draftKey.value)); + + const hasAnAssignedTeam = computed(() => !!currentChat.value?.meta?.team); + + const teamsList = computed(() => { + if (hasAnAssignedTeam.value) { + return [{ id: 0, name: t('TEAMS_SETTINGS.LIST.NONE') }, ...teams.value]; + } + return teams.value; + }); + + const onChangeAssignee = action => { + store.dispatch('assignAgent', { + conversationId: currentChat.value.id, + agentId: action.agentInfo.id, + }); + }; + + const onChangePriority = action => { + store.dispatch('assignPriority', { + conversationId: currentChat.value.id, + priority: action.priority.key, + }); + }; + + const onChangeTeam = action => { + store.dispatch('assignTeam', { + conversationId: currentChat.value.id, + teamId: action.teamInfo.id, + }); + }; + + const statusActions = computed(() => { + const isOpen = currentChat.value?.status === wootConstants.STATUS_TYPE.OPEN; + const isSnoozed = + currentChat.value?.status === wootConstants.STATUS_TYPE.SNOOZED; + const isResolved = + currentChat.value?.status === wootConstants.STATUS_TYPE.RESOLVED; + + let actions = []; + if (isOpen) { + actions = [...OPEN_CONVERSATION_ACTIONS, ...SNOOZE_CONVERSATION_ACTIONS]; + } else if (isResolved || isSnoozed) { + actions = RESOLVED_CONVERSATION_ACTIONS; + } + return prepareActions(actions, t); + }); + + const priorityOptions = computed(() => + createPriorityOptions(t, currentChat.value?.priority) + ); + + const assignAgentActions = computed(() => { + const agentOptions = agentsList.value.map(agent => ({ + id: `agent-${agent.id}`, + title: agent.name, + parent: 'assign_an_agent', + section: t('COMMAND_BAR.SECTIONS.CHANGE_ASSIGNEE'), + agentInfo: agent, + icon: ICON_ASSIGN_AGENT, + handler: onChangeAssignee, + })); + return [ + { + id: 'assign_an_agent', + title: t('COMMAND_BAR.COMMANDS.ASSIGN_AN_AGENT'), + section: t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_ASSIGN_AGENT, + children: agentOptions.map(option => option.id), + }, + ...agentOptions, + ]; + }); + + const assignPriorityActions = computed(() => { + const options = priorityOptions.value.map(priority => ({ + id: `priority-${priority.key}`, + title: priority.label, + parent: 'assign_priority', + section: t('COMMAND_BAR.SECTIONS.CHANGE_PRIORITY'), + priority: priority, + icon: priority.icon, + handler: onChangePriority, + })); + return [ + { + id: 'assign_priority', + title: t('COMMAND_BAR.COMMANDS.ASSIGN_PRIORITY'), + section: t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_ASSIGN_PRIORITY, + children: options.map(option => option.id), + }, + ...options, + ]; + }); + + const assignTeamActions = computed(() => { + const teamOptions = teamsList.value.map(team => ({ + id: `team-${team.id}`, + title: team.name, + parent: 'assign_a_team', + section: t('COMMAND_BAR.SECTIONS.CHANGE_TEAM'), + teamInfo: team, + icon: ICON_ASSIGN_TEAM, + handler: onChangeTeam, + })); + return [ + { + id: 'assign_a_team', + title: t('COMMAND_BAR.COMMANDS.ASSIGN_A_TEAM'), + section: t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_ASSIGN_TEAM, + children: teamOptions.map(option => option.id), + }, + ...teamOptions, + ]; + }); + + const addLabelActions = computed(() => { + const availableLabels = inactiveLabels.value.map(label => ({ + id: label.title, + title: `#${label.title}`, + parent: 'add_a_label_to_the_conversation', + section: t('COMMAND_BAR.SECTIONS.ADD_LABEL'), + icon: ICON_ADD_LABEL, + handler: action => addLabelToConversation({ title: action.id }), + })); + return [ + ...availableLabels, + { + id: 'add_a_label_to_the_conversation', + title: t('COMMAND_BAR.COMMANDS.ADD_LABELS_TO_CONVERSATION'), + section: t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_ADD_LABEL, + children: inactiveLabels.value.map(label => label.title), + }, + ]; + }); + + const removeLabelActions = computed(() => { + const activeLabelsComputed = activeLabels.value.map(label => ({ + id: label.title, + title: `#${label.title}`, + parent: 'remove_a_label_to_the_conversation', + section: t('COMMAND_BAR.SECTIONS.REMOVE_LABEL'), + icon: ICON_REMOVE_LABEL, + handler: action => removeLabelFromConversation(action.id), + })); + return [ + ...activeLabelsComputed, + { + id: 'remove_a_label_to_the_conversation', + title: t('COMMAND_BAR.COMMANDS.REMOVE_LABEL_FROM_CONVERSATION'), + section: t('COMMAND_BAR.SECTIONS.CONVERSATION'), + icon: ICON_REMOVE_LABEL, + children: activeLabels.value.map(label => label.title), + }, + ]; + }); + + const labelActions = computed(() => { + if (activeLabels.value.length) { + return [...addLabelActions.value, ...removeLabelActions.value]; + } + return addLabelActions.value; + }); + + const conversationAdditionalActions = computed(() => { + return prepareActions( + [ + currentChat.value.muted ? UNMUTE_ACTION : MUTE_ACTION, + SEND_TRANSCRIPT_ACTION, + ], + t + ); + }); + + const AIAssistActions = computed(() => { + const aiOptions = draftMessage.value + ? createDraftMessageAIAssistActions(t) + : createNonDraftMessageAIAssistActions(t, replyMode.value); + const options = aiOptions.map(item => ({ + id: `ai-assist-${item.key}`, + title: item.label, + parent: 'ai_assist', + section: t('COMMAND_BAR.SECTIONS.AI_ASSIST'), + priority: item, + icon: item.icon, + handler: () => emitter.emit(CMD_AI_ASSIST, item.key), + })); + return [ + { + id: 'ai_assist', + title: t('COMMAND_BAR.COMMANDS.AI_ASSIST'), + section: t('COMMAND_BAR.SECTIONS.AI_ASSIST'), + icon: ICON_AI_ASSIST, + children: options.map(option => option.id), + }, + ...options, + ]; + }); + + const isConversationOrInboxRoute = computed(() => { + return isAConversationRoute(route.name) || isAInboxViewRoute(route.name); + }); + + const shouldShowSnoozeOption = computed(() => { + return ( + isAConversationRoute(route.name, true, false) && contextMenuChatId.value + ); + }); + + const getDefaultConversationHotKeys = computed(() => { + const defaultConversationHotKeys = [ + ...statusActions.value, + ...conversationAdditionalActions.value, + ...assignAgentActions.value, + ...assignTeamActions.value, + ...labelActions.value, + ...assignPriorityActions.value, + ]; + if (isAIIntegrationEnabled.value) { + return [...defaultConversationHotKeys, ...AIAssistActions.value]; + } + return defaultConversationHotKeys; + }); + + const conversationHotKeys = computed(() => { + if (shouldShowSnoozeOption.value) { + return prepareActions(SNOOZE_CONVERSATION_ACTIONS, t); + } + if (isConversationOrInboxRoute.value) { + return getDefaultConversationHotKeys.value; + } + return []; + }); + + return { + conversationHotKeys, + }; +} diff --git a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js b/app/javascript/dashboard/composables/commands/useGoToCommandHotKeys.js similarity index 80% rename from app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js rename to app/javascript/dashboard/composables/commands/useGoToCommandHotKeys.js index 6b16bf93d..c790cda42 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js +++ b/app/javascript/dashboard/composables/commands/useGoToCommandHotKeys.js @@ -1,3 +1,8 @@ +import { computed } from 'vue'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { useMapGetter } from 'dashboard/composables/store'; +import { useRouter } from 'dashboard/composables/route'; +import { useAdmin } from 'dashboard/composables/useAdmin'; import { ICON_ACCOUNT_SETTINGS, ICON_AGENT_REPORTS, @@ -14,11 +19,9 @@ import { ICON_TEAM_REPORTS, ICON_USER_PROFILE, ICON_CONVERSATION_REPORTS, -} from './CommandBarIcons'; -import { frontendURL } from '../../../helper/URLHelper'; -import { mapGetters } from 'vuex'; -import { useAdmin } from 'dashboard/composables/useAdmin'; -import { FEATURE_FLAGS } from '../../../featureFlags'; +} from 'dashboard/helper/commandbar/icons'; +import { frontendURL } from 'dashboard/helper/URLHelper'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; const GO_TO_COMMANDS = [ { @@ -172,45 +175,45 @@ const GO_TO_COMMANDS = [ }, ]; -export default { - setup() { - const { isAdmin } = useAdmin(); - return { - isAdmin, - }; - }, - computed: { - ...mapGetters({ - accountId: 'getCurrentAccountId', - isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', - }), - goToCommandHotKeys() { - let commands = GO_TO_COMMANDS.filter(cmd => { - if (cmd.featureFlag) { - return this.isFeatureEnabledonAccount( - this.accountId, - cmd.featureFlag - ); - } - return true; - }); +export function useGoToCommandHotKeys() { + const { t } = useI18n(); + const router = useRouter(); + const { isAdmin } = useAdmin(); - if (!this.isAdmin) { - commands = commands.filter(command => command.role.includes('agent')); + const currentAccountId = useMapGetter('getCurrentAccountId'); + const isFeatureEnabledOnAccount = useMapGetter( + 'accounts/isFeatureEnabledonAccount' + ); + + const openRoute = url => { + router.push(frontendURL(url)); + }; + + const goToCommandHotKeys = computed(() => { + let commands = GO_TO_COMMANDS.filter(cmd => { + if (cmd.featureFlag) { + return isFeatureEnabledOnAccount.value( + currentAccountId.value, + cmd.featureFlag + ); } + return true; + }); - 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)); - }, - }, -}; + if (!isAdmin.value) { + commands = commands.filter(command => command.role.includes('agent')); + } + + return commands.map(command => ({ + id: command.id, + section: t(command.section), + title: t(command.title), + icon: command.icon, + handler: () => openRoute(command.path(currentAccountId.value)), + })); + }); + + return { + goToCommandHotKeys, + }; +} diff --git a/app/javascript/dashboard/routes/dashboard/commands/inboxHotKeys.js b/app/javascript/dashboard/composables/commands/useInboxHotKeys.js similarity index 56% rename from app/javascript/dashboard/routes/dashboard/commands/inboxHotKeys.js rename to app/javascript/dashboard/composables/commands/useInboxHotKeys.js index 37b1986f0..fc11bcf53 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/inboxHotKeys.js +++ b/app/javascript/dashboard/composables/commands/useInboxHotKeys.js @@ -1,13 +1,19 @@ +import { computed } from 'vue'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { useRoute } from 'dashboard/composables/route'; import wootConstants from 'dashboard/constants/globals'; -import { CMD_SNOOZE_NOTIFICATION } from './commandBarBusEvents'; -import { ICON_SNOOZE_NOTIFICATION } from './CommandBarIcons'; +import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/events'; +import { ICON_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/icons'; import { emitter } from 'shared/helpers/mitt'; import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers'; const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS; +const createSnoozeHandler = option => () => + emitter.emit(CMD_SNOOZE_NOTIFICATION, option); + const INBOX_SNOOZE_EVENTS = [ { id: 'snooze_notification', @@ -21,8 +27,7 @@ const INBOX_SNOOZE_EVENTS = [ parent: 'snooze_notification', section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION', icon: ICON_SNOOZE_NOTIFICATION, - handler: () => - emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW), + handler: createSnoozeHandler(SNOOZE_OPTIONS.AN_HOUR_FROM_NOW), }, { id: SNOOZE_OPTIONS.UNTIL_TOMORROW, @@ -30,8 +35,7 @@ const INBOX_SNOOZE_EVENTS = [ section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION', parent: 'snooze_notification', icon: ICON_SNOOZE_NOTIFICATION, - handler: () => - emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_TOMORROW), + handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_TOMORROW), }, { id: SNOOZE_OPTIONS.UNTIL_NEXT_WEEK, @@ -39,8 +43,7 @@ const INBOX_SNOOZE_EVENTS = [ section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION', parent: 'snooze_notification', icon: ICON_SNOOZE_NOTIFICATION, - handler: () => - emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK), + handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_NEXT_WEEK), }, { id: SNOOZE_OPTIONS.UNTIL_NEXT_MONTH, @@ -48,8 +51,7 @@ const INBOX_SNOOZE_EVENTS = [ section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION', parent: 'snooze_notification', icon: ICON_SNOOZE_NOTIFICATION, - handler: () => - emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH), + handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_NEXT_MONTH), }, { id: SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME, @@ -57,26 +59,30 @@ const INBOX_SNOOZE_EVENTS = [ section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION', parent: 'snooze_notification', icon: ICON_SNOOZE_NOTIFICATION, - handler: () => - emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME), + handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME), }, ]; -export default { - computed: { - inboxHotKeys() { - if (isAInboxViewRoute(this.$route.name)) { - return this.prepareActions(INBOX_SNOOZE_EVENTS); - } - return []; - }, - }, - methods: { - prepareActions(actions) { - return actions.map(action => ({ - ...action, - title: this.$t(action.title), - section: this.$t(action.section), - })); - }, - }, -}; + +export function useInboxHotKeys() { + const { t } = useI18n(); + const route = useRoute(); + + const prepareActions = actions => { + return actions.map(action => ({ + ...action, + title: t(action.title), + section: action.section ? t(action.section) : undefined, + })); + }; + + const inboxHotKeys = computed(() => { + if (isAInboxViewRoute(route.name)) { + return prepareActions(INBOX_SNOOZE_EVENTS); + } + return []; + }); + + return { + inboxHotKeys, + }; +} diff --git a/app/javascript/dashboard/routes/dashboard/commands/commandBarActions.js b/app/javascript/dashboard/helper/commandbar/actions.js similarity index 94% rename from app/javascript/dashboard/routes/dashboard/commands/commandBarActions.js rename to app/javascript/dashboard/helper/commandbar/actions.js index c47c0a7ce..457754eb7 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/commandBarActions.js +++ b/app/javascript/dashboard/helper/commandbar/actions.js @@ -8,7 +8,7 @@ import { CMD_SEND_TRANSCRIPT, CMD_SNOOZE_CONVERSATION, CMD_UNMUTE_CONVERSATION, -} from './commandBarBusEvents'; +} from 'dashboard/helper/commandbar/events'; import { ICON_MUTE_CONVERSATION, @@ -17,7 +17,7 @@ import { ICON_SEND_TRANSCRIPT, ICON_SNOOZE_CONVERSATION, ICON_UNMUTE_CONVERSATION, -} from './CommandBarIcons'; +} from 'dashboard/helper/commandbar/icons'; const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS; @@ -46,6 +46,7 @@ export const SNOOZE_CONVERSATION_ACTIONS = [ { id: 'snooze_conversation', title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION', + section: 'COMMAND_BAR.SECTIONS.CONVERSATION', icon: ICON_SNOOZE_CONVERSATION, children: Object.values(SNOOZE_OPTIONS), }, diff --git a/app/javascript/dashboard/routes/dashboard/commands/commandBarBusEvents.js b/app/javascript/dashboard/helper/commandbar/events.js similarity index 100% rename from app/javascript/dashboard/routes/dashboard/commands/commandBarBusEvents.js rename to app/javascript/dashboard/helper/commandbar/events.js diff --git a/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js b/app/javascript/dashboard/helper/commandbar/icons.js similarity index 100% rename from app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js rename to app/javascript/dashboard/helper/commandbar/icons.js diff --git a/app/javascript/dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue b/app/javascript/dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue index 529b624b0..b55d57801 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue +++ b/app/javascript/dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue @@ -6,7 +6,7 @@ import { useI18n } from 'dashboard/composables/useI18n'; import { useEmitter } from 'dashboard/composables/emitter'; import { getUnixTime } from 'date-fns'; import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers'; -import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; +import { CMD_SNOOZE_CONVERSATION } from 'dashboard/helper/commandbar/events'; import wootConstants from 'dashboard/constants/globals'; import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue'; diff --git a/app/javascript/dashboard/routes/dashboard/commands/appearanceHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/appearanceHotKeys.js deleted file mode 100644 index 493a95e0c..000000000 --- a/app/javascript/dashboard/routes/dashboard/commands/appearanceHotKeys.js +++ /dev/null @@ -1,65 +0,0 @@ -import { - ICON_APPEARANCE, - ICON_LIGHT_MODE, - ICON_DARK_MODE, - ICON_SYSTEM_MODE, -} from './CommandBarIcons'; -import { LocalStorage } from 'shared/helpers/localStorage'; -import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; -import { setColorTheme } from 'dashboard/helper/themeHelper.js'; - -export default { - computed: { - themeOptions() { - return [ - { - key: 'light', - label: this.$t('COMMAND_BAR.COMMANDS.LIGHT_MODE'), - icon: ICON_LIGHT_MODE, - }, - { - key: 'dark', - label: this.$t('COMMAND_BAR.COMMANDS.DARK_MODE'), - icon: ICON_DARK_MODE, - }, - - { - key: 'auto', - label: this.$t('COMMAND_BAR.COMMANDS.SYSTEM_MODE'), - icon: ICON_SYSTEM_MODE, - }, - ]; - }, - goToAppearanceHotKeys() { - const options = this.themeOptions.map(theme => ({ - id: theme.key, - title: theme.label, - parent: 'appearance_settings', - section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'), - icon: theme.icon, - handler: () => { - this.setAppearance(theme.key); - }, - })); - return [ - { - id: 'appearance_settings', - title: this.$t('COMMAND_BAR.COMMANDS.CHANGE_APPEARANCE'), - section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'), - icon: ICON_APPEARANCE, - children: options.map(option => option.id), - }, - ...options, - ]; - }, - }, - methods: { - setAppearance(theme) { - LocalStorage.set(LOCAL_STORAGE_KEYS.COLOR_SCHEME, theme); - const isOSOnDarkMode = window.matchMedia( - '(prefers-color-scheme: dark)' - ).matches; - setColorTheme(isOSOnDarkMode); - }, - }, -}; diff --git a/app/javascript/dashboard/routes/dashboard/commands/bulkActionsHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/bulkActionsHotKeys.js deleted file mode 100644 index 74e68afc6..000000000 --- a/app/javascript/dashboard/routes/dashboard/commands/bulkActionsHotKeys.js +++ /dev/null @@ -1,86 +0,0 @@ -import { mapGetters } from 'vuex'; -import wootConstants from 'dashboard/constants/globals'; - -import { - CMD_BULK_ACTION_SNOOZE_CONVERSATION, - CMD_BULK_ACTION_REOPEN_CONVERSATION, - CMD_BULK_ACTION_RESOLVE_CONVERSATION, -} from './commandBarBusEvents'; -import { - ICON_SNOOZE_CONVERSATION, - ICON_REOPEN_CONVERSATION, - ICON_RESOLVE_CONVERSATION, -} from './CommandBarIcons'; -import { emitter } from 'shared/helpers/mitt'; - -import { createSnoozeHandlers } from './commandBarActions'; - -const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS; - -export const SNOOZE_CONVERSATION_BULK_ACTIONS = [ - { - id: 'bulk_action_snooze_conversation', - title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION', - section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', - icon: ICON_SNOOZE_CONVERSATION, - children: Object.values(SNOOZE_OPTIONS), - }, - ...createSnoozeHandlers( - CMD_BULK_ACTION_SNOOZE_CONVERSATION, - 'bulk_action_snooze_conversation', - 'COMMAND_BAR.SECTIONS.BULK_ACTIONS' - ), -]; - -export const RESOLVED_CONVERSATION_BULK_ACTIONS = [ - { - id: 'bulk_action_reopen_conversation', - title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION', - section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', - icon: ICON_REOPEN_CONVERSATION, - handler: () => emitter.emit(CMD_BULK_ACTION_REOPEN_CONVERSATION), - }, -]; - -export const OPEN_CONVERSATION_BULK_ACTIONS = [ - { - id: 'bulk_action_resolve_conversation', - title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION', - section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS', - icon: ICON_RESOLVE_CONVERSATION, - handler: () => emitter.emit(CMD_BULK_ACTION_RESOLVE_CONVERSATION), - }, -]; - -export default { - computed: { - ...mapGetters({ - selectedConversations: 'bulkActions/getSelectedConversationIds', - }), - bulkActionsHotKeys() { - let actions = []; - if (this.selectedConversations.length > 0) { - actions = [ - ...SNOOZE_CONVERSATION_BULK_ACTIONS, - ...RESOLVED_CONVERSATION_BULK_ACTIONS, - ...OPEN_CONVERSATION_BULK_ACTIONS, - ]; - } - return this.prepareActions(actions); - }, - }, - watch: { - selectedConversations() { - this.setCommandbarData(); - }, - }, - methods: { - 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/commandbar.vue b/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue index 01893bbf9..c798336b2 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue +++ b/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue @@ -1,118 +1,85 @@ - diff --git a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js deleted file mode 100644 index 652624872..000000000 --- a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js +++ /dev/null @@ -1,413 +0,0 @@ -import { mapGetters } from 'vuex'; -import wootConstants from 'dashboard/constants/globals'; -import { emitter } from 'shared/helpers/mitt'; - -import { CMD_AI_ASSIST } from './commandBarBusEvents'; -import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; -import { - ICON_ADD_LABEL, - ICON_ASSIGN_AGENT, - ICON_ASSIGN_PRIORITY, - ICON_ASSIGN_TEAM, - ICON_REMOVE_LABEL, - ICON_PRIORITY_URGENT, - ICON_PRIORITY_HIGH, - ICON_PRIORITY_LOW, - ICON_PRIORITY_MEDIUM, - ICON_PRIORITY_NONE, - ICON_AI_ASSIST, - ICON_AI_SUMMARY, - ICON_AI_SHORTEN, - ICON_AI_EXPAND, - ICON_AI_GRAMMAR, -} from './CommandBarIcons'; - -import { - OPEN_CONVERSATION_ACTIONS, - SNOOZE_CONVERSATION_ACTIONS, - RESOLVED_CONVERSATION_ACTIONS, - SEND_TRANSCRIPT_ACTION, - UNMUTE_ACTION, - MUTE_ACTION, -} from './commandBarActions'; -import { - isAConversationRoute, - isAInboxViewRoute, -} from '../../../helper/routeHelpers'; -export default { - watch: { - assignableAgents() { - this.setCommandbarData(); - }, - currentChat() { - this.setCommandbarData(); - }, - teamsList() { - this.setCommandbarData(); - }, - activeLabels() { - this.setCommandbarData(); - }, - draftMessage() { - this.setCommandbarData(); - }, - replyMode() { - this.setCommandbarData(); - }, - contextMenuChatId() { - this.setCommandbarData(); - }, - }, - computed: { - ...mapGetters({ - currentChat: 'getSelectedChat', - replyMode: 'draftMessages/getReplyEditorMode', - contextMenuChatId: 'getContextMenuChatId', - teams: 'teams/getTeams', - }), - draftMessage() { - return this.$store.getters['draftMessages/get'](this.draftKey); - }, - draftKey() { - return `draft-${this.conversationId}-${this.replyMode}`; - }, - inboxId() { - return this.currentChat?.inbox_id; - }, - conversationId() { - return this.currentChat?.id; - }, - hasAnAssignedTeam() { - return !!this.currentChat?.meta?.team; - }, - teamsList() { - if (this.hasAnAssignedTeam) { - return [ - { id: 0, name: this.$t('TEAMS_SETTINGS.LIST.NONE') }, - ...this.teams, - ]; - } - return this.teams; - }, - 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, - ...SNOOZE_CONVERSATION_ACTIONS, - ]; - } else if (isResolved || isSnoozed) { - actions = RESOLVED_CONVERSATION_ACTIONS; - } - return this.prepareActions(actions); - }, - - priorityOptions() { - return [ - { - label: this.$t('CONVERSATION.PRIORITY.OPTIONS.NONE'), - key: null, - icon: ICON_PRIORITY_NONE, - }, - { - label: this.$t('CONVERSATION.PRIORITY.OPTIONS.URGENT'), - key: 'urgent', - icon: ICON_PRIORITY_URGENT, - }, - { - label: this.$t('CONVERSATION.PRIORITY.OPTIONS.HIGH'), - key: 'high', - icon: ICON_PRIORITY_HIGH, - }, - { - label: this.$t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'), - key: 'medium', - icon: ICON_PRIORITY_MEDIUM, - }, - { - label: this.$t('CONVERSATION.PRIORITY.OPTIONS.LOW'), - key: 'low', - icon: ICON_PRIORITY_LOW, - }, - ].filter(item => item.key !== this.currentChat?.priority); - }, - 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, - ]; - }, - assignPriorityActions() { - const options = this.priorityOptions.map(priority => ({ - id: `priority-${priority.key}`, - title: priority.label, - parent: 'assign_priority', - section: this.$t('COMMAND_BAR.SECTIONS.CHANGE_PRIORITY'), - priority: priority, - icon: priority.icon, - handler: this.onChangePriority, - })); - return [ - { - id: 'assign_priority', - title: this.$t('COMMAND_BAR.COMMANDS.ASSIGN_PRIORITY'), - section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'), - icon: ICON_ASSIGN_PRIORITY, - children: options.map(option => option.id), - }, - ...options, - ]; - }, - 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, - ]); - }, - - nonDraftMessageAIAssistActions() { - if (this.replyMode === REPLY_EDITOR_MODES.REPLY) { - return [ - { - label: this.$t( - 'INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPLY_SUGGESTION' - ), - key: 'reply_suggestion', - icon: ICON_AI_ASSIST, - }, - ]; - } - return [ - { - label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SUMMARIZE'), - key: 'summarize', - icon: ICON_AI_SUMMARY, - }, - ]; - }, - - draftMessageAIAssistActions() { - return [ - { - label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPHRASE'), - key: 'rephrase', - icon: ICON_AI_ASSIST, - }, - { - label: this.$t( - 'INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.FIX_SPELLING_GRAMMAR' - ), - key: 'fix_spelling_grammar', - icon: ICON_AI_GRAMMAR, - }, - { - label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.EXPAND'), - key: 'expand', - icon: ICON_AI_EXPAND, - }, - { - label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SHORTEN'), - key: 'shorten', - icon: ICON_AI_SHORTEN, - }, - { - label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FRIENDLY'), - key: 'make_friendly', - icon: ICON_AI_ASSIST, - }, - { - label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FORMAL'), - key: 'make_formal', - icon: ICON_AI_ASSIST, - }, - { - label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SIMPLIFY'), - key: 'simplify', - icon: ICON_AI_ASSIST, - }, - ]; - }, - - AIAssistActions() { - const aiOptions = this.draftMessage - ? this.draftMessageAIAssistActions - : this.nonDraftMessageAIAssistActions; - const options = aiOptions.map(item => ({ - id: `ai-assist-${item.key}`, - title: item.label, - parent: 'ai_assist', - section: this.$t('COMMAND_BAR.SECTIONS.AI_ASSIST'), - priority: item, - icon: item.icon, - handler: () => emitter.emit(CMD_AI_ASSIST, item.key), - })); - return [ - { - id: 'ai_assist', - title: this.$t('COMMAND_BAR.COMMANDS.AI_ASSIST'), - section: this.$t('COMMAND_BAR.SECTIONS.AI_ASSIST'), - icon: ICON_AI_ASSIST, - children: options.map(option => option.id), - }, - ...options, - ]; - }, - - isConversationOrInboxRoute() { - return ( - isAConversationRoute(this.$route.name) || - isAInboxViewRoute(this.$route.name) - ); - }, - - shouldShowSnoozeOption() { - return ( - isAConversationRoute(this.$route.name, true, false) && - this.contextMenuChatId - ); - }, - - getDefaultConversationHotKeys() { - const defaultConversationHotKeys = [ - ...this.statusActions, - ...this.conversationAdditionalActions, - ...this.assignAgentActions, - ...this.assignTeamActions, - ...this.labelActions, - ...this.assignPriorityActions, - ]; - if (this.isAIIntegrationEnabled) { - return [...defaultConversationHotKeys, ...this.AIAssistActions]; - } - return defaultConversationHotKeys; - }, - - conversationHotKeys() { - if (this.shouldShowSnoozeOption) { - return this.prepareActions(SNOOZE_CONVERSATION_ACTIONS); - } - if (this.isConversationOrInboxRoute) { - return this.getDefaultConversationHotKeys; - } - return []; - }, - }, - - methods: { - onChangeAssignee(action) { - this.$store.dispatch('assignAgent', { - conversationId: this.currentChat.id, - agentId: action.agentInfo.id, - }); - }, - onChangePriority(action) { - this.$store.dispatch('assignPriority', { - conversationId: this.currentChat.id, - priority: action.priority.key, - }); - }, - 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/inbox/components/InboxItemHeader.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxItemHeader.vue index 271c4460a..1b2895808 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxItemHeader.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxItemHeader.vue @@ -2,7 +2,7 @@ import { mapGetters } from 'vuex'; import { useAlert } from 'dashboard/composables'; import { getUnixTime } from 'date-fns'; -import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; +import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/events'; import wootConstants from 'dashboard/constants/globals'; import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers'; import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';