mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 16:14:12 +00:00
308 lines
9.1 KiB
Vue
308 lines
9.1 KiB
Vue
<template>
|
|
<!-- Set fixed width only when not in fullscreen. -->
|
|
<div class="flex flex-col h-full" :class="{ 'max-h-[600px]': !isFullscreen }">
|
|
<!-- Message type toggle -->
|
|
<div
|
|
class="flex justify-between items-center"
|
|
:class="{ 'mb-4': !isFullscreen, 'border-b border-border pb-4': isFullscreen }"
|
|
>
|
|
<Tabs v-model="messageType" class="rounded-lg border">
|
|
<TabsList class="bg-muted p-1 rounded-lg">
|
|
<TabsTrigger
|
|
value="reply"
|
|
class="px-3 py-1 rounded-lg transition-colors duration-200"
|
|
:class="{ 'bg-background text-foreground': messageType === 'reply' }"
|
|
>
|
|
{{ $t('replyBox.reply') }}
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="private_note"
|
|
class="px-3 py-1 rounded-lg transition-colors duration-200"
|
|
:class="{ 'bg-background text-foreground': messageType === 'private_note' }"
|
|
>
|
|
{{ $t('replyBox.privateNote') }}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
<span
|
|
class="text-muted-foreground hover:text-foreground transition-colors duration-200 cursor-pointer"
|
|
variant="ghost"
|
|
@click="toggleFullscreen"
|
|
>
|
|
<component
|
|
:is="isFullscreen ? Minimize2 : Maximize2"
|
|
:size="isFullscreen ? '18' : '15'"
|
|
:class="{ 'mr-2': !isFullscreen }"
|
|
/>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- CC and BCC fields -->
|
|
<div
|
|
:class="['space-y-3', isFullscreen ? 'p-4 border-b border-border' : 'mb-4']"
|
|
v-if="messageType === 'reply'"
|
|
>
|
|
<div class="flex items-center space-x-2">
|
|
<label class="w-12 text-sm font-medium text-muted-foreground">CC:</label>
|
|
<Input
|
|
type="text"
|
|
:placeholder="t('replyBox.emailAddresess')"
|
|
v-model="cc"
|
|
class="flex-grow px-3 py-2 text-sm border rounded-md focus:ring-2 focus:ring-ring"
|
|
@blur="validateEmails('cc')"
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
@click="toggleBcc"
|
|
class="text-sm bg-secondary text-secondary-foreground hover:bg-secondary/80"
|
|
>
|
|
{{ showBcc ? 'Remove BCC' : 'BCC' }}
|
|
</Button>
|
|
</div>
|
|
<div v-if="showBcc" class="flex items-center space-x-2">
|
|
<label class="w-12 text-sm font-medium text-muted-foreground">BCC:</label>
|
|
<Input
|
|
type="text"
|
|
:placeholder="t('replyBox.emailAddresess')"
|
|
v-model="bcc"
|
|
class="flex-grow px-3 py-2 text-sm border rounded-md focus:ring-2 focus:ring-ring"
|
|
@blur="validateEmails('bcc')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CC and BCC field validation errors -->
|
|
<div
|
|
v-if="emailErrors.length > 0"
|
|
class="mb-4 px-2 py-1 bg-destructive/10 border border-destructive text-destructive rounded"
|
|
>
|
|
<p v-for="error in emailErrors" :key="error" class="text-sm">{{ error }}</p>
|
|
</div>
|
|
|
|
<!-- Main tiptap editor -->
|
|
<div class="flex-grow flex flex-col overflow-hidden">
|
|
<Editor
|
|
v-model:selectedText="selectedText"
|
|
v-model:isBold="isBold"
|
|
v-model:isItalic="isItalic"
|
|
v-model:htmlContent="htmlContent"
|
|
v-model:textContent="textContent"
|
|
v-model:cursorPosition="cursorPosition"
|
|
:placeholder="editorPlaceholder"
|
|
:aiPrompts="aiPrompts"
|
|
@aiPromptSelected="handleAiPromptSelected"
|
|
:contentToSet="contentToSet"
|
|
@send="handleSend"
|
|
:clearContent="clearEditorContent"
|
|
:setInlineImage="setInlineImage"
|
|
:insertContent="insertContent"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Macro preview -->
|
|
<MacroActionsPreview
|
|
v-if="conversationStore.conversation?.macro?.actions?.length > 0"
|
|
:actions="conversationStore.conversation.macro.actions"
|
|
:onRemove="conversationStore.removeMacroAction"
|
|
class="mt-2"
|
|
/>
|
|
|
|
<!-- Attachments preview -->
|
|
<AttachmentsPreview
|
|
:attachments="attachments"
|
|
:uploadingFiles="uploadingFiles"
|
|
:onDelete="handleOnFileDelete"
|
|
v-if="attachments.length > 0 || uploadingFiles.length > 0"
|
|
class="mt-2"
|
|
/>
|
|
|
|
<!-- Editor menu bar with send button -->
|
|
<ReplyBoxMenuBar
|
|
class="mt-1 shrink-0"
|
|
:handleFileUpload="handleFileUpload"
|
|
:handleInlineImageUpload="handleInlineImageUpload"
|
|
:isBold="isBold"
|
|
:isItalic="isItalic"
|
|
:isSending="isSending"
|
|
@toggleBold="toggleBold"
|
|
@toggleItalic="toggleItalic"
|
|
:enableSend="enableSend"
|
|
:handleSend="handleSend"
|
|
@emojiSelect="handleEmojiSelect"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, nextTick } from 'vue'
|
|
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
|
import { Maximize2, Minimize2 } from 'lucide-vue-next'
|
|
import Editor from './ConversationTextEditor.vue'
|
|
import { useConversationStore } from '@/stores/conversation'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import { useEmitter } from '@/composables/useEmitter'
|
|
import AttachmentsPreview from '@/features/conversation/message/attachment/AttachmentsPreview.vue'
|
|
import MacroActionsPreview from '@/features/conversation/MacroActionsPreview.vue'
|
|
import ReplyBoxMenuBar from '@/features/conversation/ReplyBoxMenuBar.vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
// Define models for two-way binding
|
|
const messageType = defineModel('messageType', { default: 'reply' })
|
|
const cc = defineModel('cc', { default: '' })
|
|
const bcc = defineModel('bcc', { default: '' })
|
|
const showBcc = defineModel('showBcc', { default: false })
|
|
const emailErrors = defineModel('emailErrors', { default: () => [] })
|
|
const htmlContent = defineModel('htmlContent', { default: '' })
|
|
const textContent = defineModel('textContent', { default: '' })
|
|
const selectedText = defineModel('selectedText', { default: '' })
|
|
const isBold = defineModel('isBold', { default: false })
|
|
const isItalic = defineModel('isItalic', { default: false })
|
|
const cursorPosition = defineModel('cursorPosition', { default: 0 })
|
|
|
|
const props = defineProps({
|
|
isFullscreen: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
aiPrompts: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
isSending: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
uploadingFiles: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
clearEditorContent: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
contentToSet: {
|
|
type: String,
|
|
default: null
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits([
|
|
'toggleFullscreen',
|
|
'send',
|
|
'fileUpload',
|
|
'inlineImageUpload',
|
|
'fileDelete',
|
|
'aiPromptSelected'
|
|
])
|
|
|
|
const conversationStore = useConversationStore()
|
|
const emitter = useEmitter()
|
|
const { t } = useI18n()
|
|
|
|
const insertContent = ref(null)
|
|
const setInlineImage = ref(null)
|
|
const editorPlaceholder = t('replyBox.editor.placeholder')
|
|
|
|
const toggleBcc = async () => {
|
|
showBcc.value = !showBcc.value
|
|
await nextTick()
|
|
// If hiding BCC field, clear the content
|
|
if (!showBcc.value) {
|
|
bcc.value = ''
|
|
}
|
|
}
|
|
|
|
const toggleFullscreen = () => {
|
|
emit('toggleFullscreen')
|
|
}
|
|
|
|
const toggleBold = () => {
|
|
isBold.value = !isBold.value
|
|
}
|
|
|
|
const toggleItalic = () => {
|
|
isItalic.value = !isItalic.value
|
|
}
|
|
|
|
const attachments = computed(() => {
|
|
return conversationStore.conversation.mediaFiles.filter(
|
|
(upload) => upload.disposition === 'attachment'
|
|
)
|
|
})
|
|
|
|
const enableSend = computed(() => {
|
|
return (
|
|
(textContent.value.trim().length > 0 ||
|
|
conversationStore.conversation?.macro?.actions?.length > 0) &&
|
|
emailErrors.value.length === 0 &&
|
|
!props.uploadingFiles.length
|
|
)
|
|
})
|
|
|
|
/**
|
|
* Validate email addresses in the CC and BCC fields
|
|
* @param {string} field - 'cc' or 'bcc'
|
|
*/
|
|
const validateEmails = (field) => {
|
|
const emails = field === 'cc' ? cc.value : bcc.value
|
|
const emailList = emails
|
|
.split(',')
|
|
.map((e) => e.trim())
|
|
.filter((e) => e !== '')
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
const invalidEmails = emailList.filter((email) => !emailRegex.test(email))
|
|
|
|
// Remove any existing errors for this field
|
|
emailErrors.value = emailErrors.value.filter(
|
|
(error) => !error.startsWith(`${t('replyBox.invalidEmailsIn')} ${field.toUpperCase()}`)
|
|
)
|
|
|
|
// Add new error if there are invalid emails
|
|
if (invalidEmails.length > 0) {
|
|
emailErrors.value.push(
|
|
`${t('replyBox.invalidEmailsIn')} ${field.toUpperCase()}: ${invalidEmails.join(', ')}`
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send the reply or private note
|
|
*/
|
|
const handleSend = async () => {
|
|
validateEmails('cc')
|
|
validateEmails('bcc')
|
|
if (emailErrors.value.length > 0) {
|
|
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
|
variant: 'destructive',
|
|
description: t('replyBox.correctEmailErrors')
|
|
})
|
|
return
|
|
}
|
|
emit('send')
|
|
}
|
|
|
|
const handleFileUpload = (event) => {
|
|
emit('fileUpload', event)
|
|
}
|
|
|
|
const handleInlineImageUpload = (event) => {
|
|
emit('inlineImageUpload', event)
|
|
}
|
|
|
|
const handleOnFileDelete = (uuid) => {
|
|
emit('fileDelete', uuid)
|
|
}
|
|
|
|
const handleEmojiSelect = (emoji) => {
|
|
insertContent.value = undefined
|
|
// Force reactivity so the user can select the same emoji multiple times
|
|
nextTick(() => (insertContent.value = emoji))
|
|
}
|
|
|
|
const handleAiPromptSelected = (key) => {
|
|
emit('aiPromptSelected', key)
|
|
}
|
|
</script>
|