Compare commits

...
9 Commits
6 changed files with 77 additions and 24 deletions
@@ -77,6 +77,15 @@ const handleClose = () => {
emit('close'); 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(() => { onUnmounted(() => {
isLocked.value = false; isLocked.value = false;
}); });
@@ -89,7 +98,7 @@ onUnmounted(() => {
class="fixed outline-none z-[9999] cursor-pointer" class="fixed outline-none z-[9999] cursor-pointer"
:style="position" :style="position"
tabindex="0" tabindex="0"
@blur="handleClose" @focusout="handleFocusOut"
> >
<slot /> <slot />
</div> </div>
@@ -7,10 +7,13 @@ import {
getSortedAgentsByAvailability, getSortedAgentsByAvailability,
getAgentsByUpdatedPresence, getAgentsByUpdatedPresence,
} from 'dashboard/helper/agentHelper.js'; } from 'dashboard/helper/agentHelper.js';
import { picoSearch } from '@scmmishra/pico-search';
import MenuItem from './menuItem.vue'; import MenuItem from './menuItem.vue';
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue'; import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue'; 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 = { const MENU = {
MARK_AS_READ: 'mark-as-read', MARK_AS_READ: 'mark-as-read',
@@ -31,6 +34,8 @@ export default {
MenuItem, MenuItem,
MenuItemWithSubmenu, MenuItemWithSubmenu,
AgentLoadingPlaceholder, AgentLoadingPlaceholder,
NextInput,
Icon,
}, },
props: { props: {
chatId: { chatId: {
@@ -87,6 +92,7 @@ export default {
data() { data() {
return { return {
MENU, MENU,
labelSearchQuery: '',
STATUS_TYPE: wootConstants.STATUS_TYPE, STATUS_TYPE: wootConstants.STATUS_TYPE,
readOption: { readOption: {
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_READ'), 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 // Don't show snooze if the conversation is already snoozed/resolved/pending
return this.status === wootConstants.STATUS_TYPE.OPEN; 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() { mounted() {
this.$store.dispatch('inboxAssignableAgents/fetch', [this.inboxId]); this.$store.dispatch('inboxAssignableAgents/fetch', [this.inboxId]);
@@ -335,21 +349,49 @@ export default {
:option="labelMenuConfig" :option="labelMenuConfig"
:sub-menu-available="!!labels.length" :sub-menu-available="!!labels.length"
> >
<MenuItem <div class="pb-1 w-[12.5rem]">
v-for="label in labels" <NextInput
:key="label.id" v-model="labelSearchQuery"
:option="generateMenuLabelConfig(label, 'label')" type="search"
:variant=" size="sm"
conversationLabels.includes(label.title) class="w-full"
? 'label-assigned' custom-input-class="!ps-8 !text-xs"
: 'label' :placeholder="$t('CONVERSATION.CARD_CONTEXT_MENU.SEARCH_LABELS')"
" @click.stop
@click.stop=" @keydown.stop
conversationLabels.includes(label.title) >
? $emit('removeLabel', label) <template #prefix>
: $emit('assignLabel', label) <Icon
" icon="i-lucide-search"
/> class="absolute z-10 -translate-y-1/2 pointer-events-none size-3.5 text-n-slate-10 top-1/2 start-2"
/>
</template>
</NextInput>
</div>
<div class="overflow-x-hidden overflow-y-auto max-h-[12.5rem]">
<MenuItem
v-for="label in filteredLabels"
:key="label.id"
:option="generateMenuLabelConfig(label, 'label')"
:variant="
conversationLabels.includes(label.title)
? 'label-assigned'
: 'label'
"
@mousedown.prevent
@click.stop="
conversationLabels.includes(label.title)
? $emit('removeLabel', label)
: $emit('assignLabel', label)
"
/>
<p
v-if="!filteredLabels.length"
class="px-2 py-2 m-0 text-xs text-center text-n-slate-11"
>
{{ $t('CONVERSATION.CARD_CONTEXT_MENU.NO_LABELS_FOUND') }}
</p>
</div>
</MenuItemWithSubmenu> </MenuItemWithSubmenu>
<MenuItemWithSubmenu <MenuItemWithSubmenu
v-if="isAllowed([MENU.AGENT])" v-if="isAllowed([MENU.AGENT])"
@@ -15,7 +15,7 @@ defineProps({
</script> </script>
<template> <template>
<div class="menu text-n-slate-12 min-h-7 min-w-0" role="button"> <div class="menu group text-n-slate-12 min-h-7 min-w-0" role="button">
<fluent-icon <fluent-icon
v-if="variant === 'icon' && option.icon" v-if="variant === 'icon' && option.icon"
:icon="option.icon" :icon="option.icon"
@@ -52,7 +52,7 @@ defineProps({
<Icon <Icon
v-if="variant === 'label-assigned'" v-if="variant === 'label-assigned'"
icon="i-lucide-check" icon="i-lucide-check"
class="flex-shrink-0 size-3.5 mr-1" class="flex-shrink-0 size-3.5 text-n-brand group-hover:text-white"
/> />
</div> </div>
</template> </template>
@@ -193,6 +193,8 @@
}, },
"ASSIGN_AGENT": "Assign agent", "ASSIGN_AGENT": "Assign agent",
"ASSIGN_LABEL": "Assign label", "ASSIGN_LABEL": "Assign label",
"SEARCH_LABELS": "Search labels",
"NO_LABELS_FOUND": "No labels found",
"AGENTS_LOADING": "Loading agents...", "AGENTS_LOADING": "Loading agents...",
"ASSIGN_TEAM": "Assign team", "ASSIGN_TEAM": "Assign team",
"DELETE": "Delete conversation", "DELETE": "Delete conversation",
+1 -1
View File
@@ -34,7 +34,7 @@
"@amplitude/analytics-browser": "^2.11.10", "@amplitude/analytics-browser": "^2.11.10",
"@breezystack/lamejs": "^1.2.7", "@breezystack/lamejs": "^1.2.7",
"@chatwoot/ninja-keys": "1.2.3", "@chatwoot/ninja-keys": "1.2.3",
"@chatwoot/prosemirror-schema": "1.3.23", "@chatwoot/prosemirror-schema": "1.3.28",
"@chatwoot/utils": "^0.0.56", "@chatwoot/utils": "^0.0.56",
"@formkit/core": "^1.7.2", "@formkit/core": "^1.7.2",
"@formkit/vue": "^1.7.2", "@formkit/vue": "^1.7.2",
+5 -5
View File
@@ -25,8 +25,8 @@ importers:
specifier: 1.2.3 specifier: 1.2.3
version: 1.2.3 version: 1.2.3
'@chatwoot/prosemirror-schema': '@chatwoot/prosemirror-schema':
specifier: 1.3.23 specifier: 1.3.28
version: 1.3.23 version: 1.3.28
'@chatwoot/utils': '@chatwoot/utils':
specifier: ^0.0.56 specifier: ^0.0.56
version: 0.0.56 version: 0.0.56
@@ -452,8 +452,8 @@ packages:
'@chatwoot/ninja-keys@1.2.3': '@chatwoot/ninja-keys@1.2.3':
resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==} resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
'@chatwoot/prosemirror-schema@1.3.23': '@chatwoot/prosemirror-schema@1.3.28':
resolution: {integrity: sha512-jGxbWELCdlVI64BJiE1wT84ekJHYDXXKiluQIKT3aKPEjPwMR48umKF3A0yHjKoR7IIxCC9oM77TvXOA0ebLtw==} resolution: {integrity: sha512-BaK5u13BPcXNDXFwpml7b1G6u8t6dfRqT6TGpZqk+4NiAfWpPHREmG8TovPKpgTmnuPmcH3j2NkSeVYpArEqgg==}
'@chatwoot/utils@0.0.56': '@chatwoot/utils@0.0.56':
resolution: {integrity: sha512-A6dmPLfTSrW4qYNY73btyi4PqpfzcXRSaucscZTQdzNqF6G/QUdgnBmHtho8HeiYby/kSHXaSxLJj+0dx3yEQQ==} resolution: {integrity: sha512-A6dmPLfTSrW4qYNY73btyi4PqpfzcXRSaucscZTQdzNqF6G/QUdgnBmHtho8HeiYby/kSHXaSxLJj+0dx3yEQQ==}
@@ -5124,7 +5124,7 @@ snapshots:
hotkeys-js: 3.8.7 hotkeys-js: 3.8.7
lit: 2.2.6 lit: 2.2.6
'@chatwoot/prosemirror-schema@1.3.23': '@chatwoot/prosemirror-schema@1.3.28':
dependencies: dependencies:
markdown-it-sup: 2.0.0 markdown-it-sup: 2.0.0
prosemirror-commands: 1.7.1 prosemirror-commands: 1.7.1