From bae20ca83e19952536c9b32e1c6fd7f995ea931a Mon Sep 17 00:00:00 2001 From: Nicky Duijf <47255128+NickThePigeon@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:40:46 +0200 Subject: [PATCH] feat(conversation): add label search to right-click context menu (#15084) --- .../dashboard/components/ui/ContextMenu.vue | 11 ++- .../conversation/contextMenu/Index.vue | 72 +++++++++++++++---- .../conversation/contextMenu/menuItem.vue | 4 +- .../i18n/locale/en/conversation.json | 2 + 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/app/javascript/dashboard/components/ui/ContextMenu.vue b/app/javascript/dashboard/components/ui/ContextMenu.vue index 0773e4fd2..9ce6866cb 100644 --- a/app/javascript/dashboard/components/ui/ContextMenu.vue +++ b/app/javascript/dashboard/components/ui/ContextMenu.vue @@ -77,6 +77,15 @@ const handleClose = () => { emit('close'); }; +const handleFocusOut = event => { + // Keep the menu open while focus stays inside it (e.g. the label search + // input); close it once focus leaves the menu entirely. + if (menuRef.value?.contains(event.relatedTarget)) { + return; + } + handleClose(); +}; + onUnmounted(() => { isLocked.value = false; }); @@ -89,7 +98,7 @@ onUnmounted(() => { class="fixed outline-none z-[9999] cursor-pointer" :style="position" tabindex="0" - @blur="handleClose" + @focusout="handleFocusOut" > diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue index 34009935c..351ca6d2c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue @@ -7,10 +7,13 @@ import { getSortedAgentsByAvailability, getAgentsByUpdatedPresence, } from 'dashboard/helper/agentHelper.js'; +import { picoSearch } from '@scmmishra/pico-search'; import MenuItem from './menuItem.vue'; import MenuItemWithSubmenu from './menuItemWithSubmenu.vue'; import wootConstants from 'dashboard/constants/globals'; import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue'; +import NextInput from 'dashboard/components-next/input/Input.vue'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; const MENU = { MARK_AS_READ: 'mark-as-read', @@ -31,6 +34,8 @@ export default { MenuItem, MenuItemWithSubmenu, AgentLoadingPlaceholder, + NextInput, + Icon, }, props: { chatId: { @@ -87,6 +92,7 @@ export default { data() { return { MENU, + labelSearchQuery: '', STATUS_TYPE: wootConstants.STATUS_TYPE, readOption: { label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_READ'), @@ -216,6 +222,14 @@ export default { // Don't show snooze if the conversation is already snoozed/resolved/pending return this.status === wootConstants.STATUS_TYPE.OPEN; }, + filteredLabels() { + const labels = this.labelSearchQuery + ? picoSearch(this.labels, this.labelSearchQuery, ['title']) + : this.labels; + // Assigned labels first, keeping each group's existing order. + const isAssigned = label => this.conversationLabels.includes(label.title); + return [...labels].sort((a, b) => isAssigned(b) - isAssigned(a)); + }, }, mounted() { this.$store.dispatch('inboxAssignableAgents/fetch', [this.inboxId]); @@ -335,21 +349,49 @@ export default { :option="labelMenuConfig" :sub-menu-available="!!labels.length" > - +
+ + + +
+
+ +

+ {{ $t('CONVERSATION.CARD_CONTEXT_MENU.NO_LABELS_FOUND') }} +

+
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 9915b26ef..62510ddd0 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -193,6 +193,8 @@ }, "ASSIGN_AGENT": "Assign agent", "ASSIGN_LABEL": "Assign label", + "SEARCH_LABELS": "Search labels", + "NO_LABELS_FOUND": "No labels found", "AGENTS_LOADING": "Loading agents...", "ASSIGN_TEAM": "Assign team", "DELETE": "Delete conversation",