From b679fc21588da61d7f0540934a1b77c315a3b77d Mon Sep 17 00:00:00 2001 From: aditya2r Date: Sat, 6 Jun 2026 15:42:49 -0400 Subject: [PATCH 1/4] feat: add "Send and set status" split button to reply box. Add a dropdown next to the Send button that allows agents to send a reply and update the conversation status in a single action. - Add split button with dropdown to ReplyBoxMenuBar - Extract email validation into reusable validateBeforeSend helper - Guard for status update on successful message send - Exclude Snoozed from status options --- .../src/features/conversation/ReplyBox.vue | 16 +++++++ .../features/conversation/ReplyBoxContent.vue | 25 ++++++++--- .../features/conversation/ReplyBoxMenuBar.vue | 44 +++++++++++++++++-- i18n/en-US.json | 1 + 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/frontend/apps/main/src/features/conversation/ReplyBox.vue b/frontend/apps/main/src/features/conversation/ReplyBox.vue index d1cabbaa..1e62de57 100644 --- a/frontend/apps/main/src/features/conversation/ReplyBox.vue +++ b/frontend/apps/main/src/features/conversation/ReplyBox.vue @@ -103,6 +103,7 @@ v-model:mentions="mentions" @toggleFullscreen="isEditorFullscreen = !isEditorFullscreen" @send="processSend" + @sendAndSetStatus="processSendAndSetStatus" @fileUpload="handleFileUpload" @fileDelete="handleFileDelete" @filesDropped="uploadFiles" @@ -137,6 +138,7 @@ v-model:mentions="mentions" @toggleFullscreen="isEditorFullscreen = !isEditorFullscreen" @send="processSend" + @sendAndSetStatus="processSendAndSetStatus" @fileUpload="handleFileUpload" @fileDelete="handleFileDelete" @filesDropped="uploadFiles" @@ -464,6 +466,20 @@ const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = mentions.value = [] } isSending.value = false + + // Return whether the message was sent successfully. + return !hasMessageSendingErrored +} + +/** + * Processes the send action and update conversation status. + * The status is only updated if the message is sent successfully. + */ +const processSendAndSetStatus = async (status) => { + const success = await processSend() + if (success) { + conversationStore.updateStatus(status) + } } /** diff --git a/frontend/apps/main/src/features/conversation/ReplyBoxContent.vue b/frontend/apps/main/src/features/conversation/ReplyBoxContent.vue index 24b9b6fc..be911978 100644 --- a/frontend/apps/main/src/features/conversation/ReplyBoxContent.vue +++ b/frontend/apps/main/src/features/conversation/ReplyBoxContent.vue @@ -130,6 +130,7 @@ :isSending="isSending" :enableSend="enableSend" :handleSend="handleSend" + :handleSendAndSetStatus="handleSendAndSetStatus" @emojiSelect="handleEmojiSelect" /> @@ -239,6 +240,7 @@ const props = defineProps({ const emit = defineEmits([ 'toggleFullscreen', 'send', + 'sendAndSetStatus', 'fileUpload', 'inlineImageUpload', 'fileDelete', @@ -302,21 +304,34 @@ const validateEmails = async () => { }) } -/** - * Send the reply or private note - */ -const handleSend = async () => { +const validateBeforeSend = async () => { await validateEmails() if (emailErrors.value.length > 0) { emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { variant: 'destructive', description: t('globals.messages.correctEmailErrors') }) - return + return false } + return true +} + +/** + * Send the reply or private note + */ +const handleSend = async () => { + if (!(await validateBeforeSend())) return emit('send') } +/** + * Send the reply or private note and set conversation status + */ +const handleSendAndSetStatus = async (status) => { + if (!(await validateBeforeSend())) return + emit('sendAndSetStatus', status) +} + const handleFileUpload = (event) => { emit('fileUpload', event) } diff --git a/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue b/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue index 593c42d5..e5a5544e 100644 --- a/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue +++ b/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue @@ -38,9 +38,37 @@ - +
+ + + + + + + {{ $t('replyBox.sendAndSetAs') }} + + {{ status.label }} + + + +
@@ -49,7 +77,14 @@ import { ref, defineAsyncComponent } from 'vue' import { onClickOutside } from '@vueuse/core' import { Button } from '@shared-ui/components/ui/button' import { Toggle } from '@shared-ui/components/ui/toggle' -import { Paperclip, Smile } from 'lucide-vue-next' +import { Paperclip, Smile, ChevronDownIcon } from 'lucide-vue-next' +import DropdownMenu from '@shared-ui/components/ui/dropdown-menu/DropdownMenu.vue' +import DropdownMenuTrigger from '@shared-ui/components/ui/dropdown-menu/DropdownMenuTrigger.vue' +import DropdownMenuItem from '@shared-ui/components/ui/dropdown-menu/DropdownMenuItem.vue' +import DropdownMenuContent from '@shared-ui/components/ui/dropdown-menu/DropdownMenuContent.vue' +import DropdownMenuLabel from '@shared-ui/components/ui/dropdown-menu/DropdownMenuLabel.vue' +import { useConversationStore } from '@main/stores/conversation' +const conversationStore = useConversationStore() const EmojiPicker = defineAsyncComponent(async () => { const [mod] = await Promise.all([ @@ -71,6 +106,7 @@ defineProps({ isSending: Boolean, enableSend: Boolean, handleSend: Function, + handleSendAndSetStatus: Function, showSendButton: { type: Boolean, default: true diff --git a/i18n/en-US.json b/i18n/en-US.json index 92b5a14a..9cca9a20 100644 --- a/i18n/en-US.json +++ b/i18n/en-US.json @@ -940,6 +940,7 @@ "replyBox.missingTagsTitle": "No tags on this conversation", "replyBox.removeBCC": "Remove BCC", "replyBox.sendAnyway": "Send anyway", + "replyBox.sendAndSetAs": "Send and set as", "replyBox.toRequired": "At least one recipient is required in the To field.", "report.agentStatus": "Agent Status", "report.chart.newConversations": "New conversations", From bf22811337b63e1544389e546dd311d8e6a6ef18 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 7 Jun 2026 11:56:15 +0530 Subject: [PATCH 2/4] fix chevron color --- .../apps/main/src/features/conversation/ReplyBoxMenuBar.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue b/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue index e5a5544e..d59bc06c 100644 --- a/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue +++ b/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue @@ -40,7 +40,7 @@
From 90ceece01bf028776f0900aa8f7d6abffffeeaa8 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 7 Jun 2026 12:17:29 +0530 Subject: [PATCH 3/4] fix apply status update after send confirmation dialogs. --- .../src/features/conversation/ReplyBox.vue | 26 ++++++------------- frontend/apps/main/src/stores/conversation.js | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/frontend/apps/main/src/features/conversation/ReplyBox.vue b/frontend/apps/main/src/features/conversation/ReplyBox.vue index 1e62de57..c6a6f728 100644 --- a/frontend/apps/main/src/features/conversation/ReplyBox.vue +++ b/frontend/apps/main/src/features/conversation/ReplyBox.vue @@ -13,7 +13,7 @@ {{ $t('globals.messages.cancel') }} - {{ + {{ $t('replyBox.sendAnyway') }} @@ -30,7 +30,7 @@ {{ $t('globals.messages.cancel') }} - {{ + {{ $t('replyBox.sendAnyway') }} @@ -249,6 +249,7 @@ const aiPrompts = computed(() => aiPromptStore.prompts) const replyBoxContentRef = ref(null) const showContactEmailWarning = ref(false) const showMissingTagsWarning = ref(false) +const deferredStatus = ref(null) const mentions = ref([]) aiPromptStore.fetchPrompts() @@ -307,10 +308,7 @@ const hasTextContent = computed(() => { return textContent.value.trim().length > 0 }) -/** - * Processes the send action. - */ -const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = false) => { +const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = false, statusToSet = null) => { let hasMessageSendingErrored = false isEditorFullscreen.value = false @@ -329,6 +327,7 @@ const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = currentInbox?.prompt_tags_on_reply && !(conversationStore.current.tags?.length > 0) ) { + deferredStatus.value = statusToSet showMissingTagsWarning.value = true return } @@ -354,6 +353,7 @@ const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = .map((e) => e.trim()) .includes(contactEmail) ) { + deferredStatus.value = statusToSet showContactEmailWarning.value = true return } @@ -464,23 +464,13 @@ const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = clearMediaFiles() emailErrors.value = [] mentions.value = [] + if (statusToSet) conversationStore.updateStatus(statusToSet) } isSending.value = false - - // Return whether the message was sent successfully. return !hasMessageSendingErrored } -/** - * Processes the send action and update conversation status. - * The status is only updated if the message is sent successfully. - */ -const processSendAndSetStatus = async (status) => { - const success = await processSend() - if (success) { - conversationStore.updateStatus(status) - } -} +const processSendAndSetStatus = (status) => processSend(false, false, status) /** * Watches for changes in the conversation's macro id and update message content. diff --git a/frontend/apps/main/src/stores/conversation.js b/frontend/apps/main/src/stores/conversation.js index 1dd3727a..ba789803 100644 --- a/frontend/apps/main/src/stores/conversation.js +++ b/frontend/apps/main/src/stores/conversation.js @@ -40,7 +40,7 @@ export const useConversationStore = defineStore('conversation', () => { return statuses.value.map(s => ({ label: s.name, value: s.id })) }) const statusOptionsNoSnooze = computed(() => - statuses.value.filter(s => s.name !== 'Snoozed').map(s => ({ + statuses.value.filter(s => s.name !== CONVERSATION_DEFAULT_STATUSES.SNOOZED).map(s => ({ label: s.name, value: s.id })) From ed95b9104f64bc68f1ad57fa8f96b776864c59b6 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 7 Jun 2026 12:28:12 +0530 Subject: [PATCH 4/4] refactor tidy reply box send button imports and dead code --- .../apps/main/src/features/conversation/ReplyBox.vue | 1 - .../src/features/conversation/ReplyBoxMenuBar.vue | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/apps/main/src/features/conversation/ReplyBox.vue b/frontend/apps/main/src/features/conversation/ReplyBox.vue index c6a6f728..a2cf4218 100644 --- a/frontend/apps/main/src/features/conversation/ReplyBox.vue +++ b/frontend/apps/main/src/features/conversation/ReplyBox.vue @@ -467,7 +467,6 @@ const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = if (statusToSet) conversationStore.updateStatus(statusToSet) } isSending.value = false - return !hasMessageSendingErrored } const processSendAndSetStatus = (status) => processSend(false, false, status) diff --git a/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue b/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue index d59bc06c..53272829 100644 --- a/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue +++ b/frontend/apps/main/src/features/conversation/ReplyBoxMenuBar.vue @@ -78,11 +78,13 @@ import { onClickOutside } from '@vueuse/core' import { Button } from '@shared-ui/components/ui/button' import { Toggle } from '@shared-ui/components/ui/toggle' import { Paperclip, Smile, ChevronDownIcon } from 'lucide-vue-next' -import DropdownMenu from '@shared-ui/components/ui/dropdown-menu/DropdownMenu.vue' -import DropdownMenuTrigger from '@shared-ui/components/ui/dropdown-menu/DropdownMenuTrigger.vue' -import DropdownMenuItem from '@shared-ui/components/ui/dropdown-menu/DropdownMenuItem.vue' -import DropdownMenuContent from '@shared-ui/components/ui/dropdown-menu/DropdownMenuContent.vue' -import DropdownMenuLabel from '@shared-ui/components/ui/dropdown-menu/DropdownMenuLabel.vue' +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuLabel +} from '@shared-ui/components/ui/dropdown-menu' import { useConversationStore } from '@main/stores/conversation' const conversationStore = useConversationStore()