Merge pull request #373 from aditya2r/feat/unified-send-and-close-ticket-action

feat: add "Send and set status" split button to reply box.
This commit is contained in:
Abhinav Raut
2026-06-07 12:35:39 +05:30
committed by GitHub
5 changed files with 75 additions and 16 deletions
@@ -13,7 +13,7 @@
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ $t('globals.messages.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="processSend(true, true)">{{
<AlertDialogAction @click="processSend(true, true, deferredStatus)">{{
$t('replyBox.sendAnyway')
}}</AlertDialogAction>
</AlertDialogFooter>
@@ -30,7 +30,7 @@
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ $t('globals.messages.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="processSend(false, true)">{{
<AlertDialogAction @click="processSend(false, true, deferredStatus)">{{
$t('replyBox.sendAnyway')
}}</AlertDialogAction>
</AlertDialogFooter>
@@ -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"
@@ -247,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()
@@ -305,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
@@ -327,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
}
@@ -352,6 +353,7 @@ const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck =
.map((e) => e.trim())
.includes(contactEmail)
) {
deferredStatus.value = statusToSet
showContactEmailWarning.value = true
return
}
@@ -462,10 +464,13 @@ const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck =
clearMediaFiles()
emailErrors.value = []
mentions.value = []
if (statusToSet) conversationStore.updateStatus(statusToSet)
}
isSending.value = false
}
const processSendAndSetStatus = (status) => processSend(false, false, status)
/**
* Watches for changes in the conversation's macro id and update message content.
*/
@@ -130,6 +130,7 @@
:isSending="isSending"
:enableSend="enableSend"
:handleSend="handleSend"
:handleSendAndSetStatus="handleSendAndSetStatus"
@emojiSelect="handleEmojiSelect"
/>
</div>
@@ -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)
}
@@ -38,9 +38,37 @@
<Smile class="h-4 w-4" />
</Toggle>
</div>
<Button class="h-8 w-6 px-8" @click="handleSend" :disabled="!enableSend" :isLoading="isSending" v-if="showSendButton">
{{ $t('globals.messages.send') }}
</Button>
<div class="flex items-center">
<Button
class="h-8 px-4 rounded-r-none"
@click="handleSend"
:disabled="!enableSend"
:isLoading="isSending"
v-if="showSendButton"
>
{{ $t('globals.messages.send') }}
</Button>
<DropdownMenu v-if="showSendButton">
<DropdownMenuTrigger as-child>
<Button
class="h-8 px-2 rounded-l-none border-l border-primary-foreground/30 [&[data-state=open]>svg]:rotate-180"
:disabled="!enableSend"
>
<ChevronDownIcon class="text-primary-foreground transition-transform" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>{{ $t('replyBox.sendAndSetAs') }}</DropdownMenuLabel>
<DropdownMenuItem
v-for="status in conversationStore.statusOptionsNoSnooze"
:key="status.value"
@click="handleSendAndSetStatus(status.label)"
>
{{ status.label }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</template>
@@ -49,7 +77,16 @@ 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,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownMenuContent,
DropdownMenuLabel
} from '@shared-ui/components/ui/dropdown-menu'
import { useConversationStore } from '@main/stores/conversation'
const conversationStore = useConversationStore()
const EmojiPicker = defineAsyncComponent(async () => {
const [mod] = await Promise.all([
@@ -71,6 +108,7 @@ defineProps({
isSending: Boolean,
enableSend: Boolean,
handleSend: Function,
handleSendAndSetStatus: Function,
showSendButton: {
type: Boolean,
default: true
@@ -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
}))
+1
View File
@@ -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",