Merge pull request #43 from shariquerik/cc-bcc-reply-all
feat: CC, BCC & ReplyAll
This commit is contained in:
commit
96819956c9
@ -354,7 +354,7 @@
|
||||
<div
|
||||
class="cursor-pointer rounded-md bg-gray-50 p-3 text-base leading-6 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between gap-2">
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<UserAvatar :user="activity.data.sender" size="md" />
|
||||
<span>{{ activity.data.sender_full_name }}</span>
|
||||
@ -370,12 +370,29 @@
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-gray-700"
|
||||
@click="reply(activity.data.content)"
|
||||
@click="reply(activity.data)"
|
||||
>
|
||||
<ReplyIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-gray-700"
|
||||
@click="reply(activity.data, true)"
|
||||
>
|
||||
<ReplyAllIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 text-sm leading-5 text-gray-600">
|
||||
<span class="mr-1">TO:</span>
|
||||
<span>{{ activity.data.recipients }}</span>
|
||||
<span v-if="activity.data.cc">, </span>
|
||||
<span v-if="activity.data.cc" class="mr-1">CC:</span>
|
||||
<span v-if="activity.data.cc">{{ activity.data.cc }}</span>
|
||||
<span v-if="activity.data.bcc">, </span>
|
||||
<span v-if="activity.data.bcc" class="mr-1">BCC:</span>
|
||||
<span v-if="activity.data.bcc">{{ activity.data.bcc }}</span>
|
||||
</div>
|
||||
<span class="prose-f" v-html="activity.data.content" />
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<AttachmentItem
|
||||
@ -679,6 +696,7 @@ import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
|
||||
import InboundCallIcon from '@/components/Icons/InboundCallIcon.vue'
|
||||
import OutboundCallIcon from '@/components/Icons/OutboundCallIcon.vue'
|
||||
import ReplyIcon from '@/components/Icons/ReplyIcon.vue'
|
||||
import ReplyAllIcon from '@/components/Icons/ReplyAllIcon.vue'
|
||||
import AttachmentItem from '@/components/AttachmentItem.vue'
|
||||
import CommunicationArea from '@/components/CommunicationArea.vue'
|
||||
import NoteModal from '@/components/Modals/NoteModal.vue'
|
||||
@ -997,10 +1015,28 @@ function updateTaskStatus(status, task) {
|
||||
}
|
||||
|
||||
// Email
|
||||
function reply(message) {
|
||||
function reply(email, reply_all = false) {
|
||||
emailBox.value.show = true
|
||||
let editor = emailBox.value.editor.editor
|
||||
editor
|
||||
let editor = emailBox.value.editor
|
||||
let message = email.content
|
||||
let recipients = email.recipients.split(',').map((r) => r.trim())
|
||||
editor.toEmails = recipients
|
||||
editor.cc = editor.bcc = false
|
||||
editor.ccEmails = []
|
||||
editor.bccEmails = []
|
||||
|
||||
if (reply_all) {
|
||||
let cc = email.cc?.split(',').map((r) => r.trim())
|
||||
let bcc = email.bcc?.split(',').map((r) => r.trim())
|
||||
|
||||
editor.cc = cc ? true : false
|
||||
editor.bcc = bcc ? true : false
|
||||
|
||||
editor.ccEmails = cc
|
||||
editor.bccEmails = bcc
|
||||
}
|
||||
|
||||
editor.editor
|
||||
.chain()
|
||||
.clearContent()
|
||||
.insertContent(message)
|
||||
|
||||
@ -1,21 +1,35 @@
|
||||
<template>
|
||||
<div class="flex gap-1.5 border-t px-10 py-2.5">
|
||||
<Button
|
||||
ref="sendEmailRef"
|
||||
variant="ghost"
|
||||
:class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||
label="Reply"
|
||||
@click="showCommunicationBox = !showCommunicationBox"
|
||||
>
|
||||
<template #prefix>
|
||||
<EmailIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<!-- <Button variant="ghost" label="Comment">
|
||||
<template #prefix>
|
||||
<CommentIcon class="h-4" />
|
||||
</template>
|
||||
</Button> -->
|
||||
<div class="flex justify-between gap-3 border-t px-10 py-2.5">
|
||||
<div class="flex gap-1.5">
|
||||
<Button
|
||||
ref="sendEmailRef"
|
||||
variant="ghost"
|
||||
:class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||
label="Reply"
|
||||
@click="showCommunicationBox = !showCommunicationBox"
|
||||
>
|
||||
<template #prefix>
|
||||
<EmailIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<!-- <Button variant="ghost" label="Comment">
|
||||
<template #prefix>
|
||||
<CommentIcon class="h-4" />
|
||||
</template>
|
||||
</Button> -->
|
||||
</div>
|
||||
<div v-if="showCommunicationBox" class="flex gap-1.5">
|
||||
<Button
|
||||
label="CC"
|
||||
@click="newEmailEditor.cc = !newEmailEditor.cc"
|
||||
:class="[newEmailEditor.cc ? 'bg-gray-300 hover:bg-gray-200' : '']"
|
||||
/>
|
||||
<Button
|
||||
label="BCC"
|
||||
@click="newEmailEditor.bcc = !newEmailEditor.bcc"
|
||||
:class="[newEmailEditor.bcc ? 'bg-gray-300 hover:bg-gray-200' : '']"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="showCommunicationBox"
|
||||
@ -71,7 +85,7 @@ const showCommunicationBox = ref(false)
|
||||
const newEmail = ref('')
|
||||
const newEmailEditor = ref(null)
|
||||
const sendEmailRef = ref(null)
|
||||
const attachments = ref([]);
|
||||
const attachments = ref([])
|
||||
|
||||
watch(
|
||||
() => showCommunicationBox.value,
|
||||
@ -91,11 +105,14 @@ const onNewEmailChange = (value) => {
|
||||
}
|
||||
|
||||
async function sendMail() {
|
||||
let recipients = newEmailEditor.value.toEmails
|
||||
let cc = newEmailEditor.value.ccEmails
|
||||
let bcc = newEmailEditor.value.bccEmails
|
||||
await call('frappe.core.doctype.communication.email.make', {
|
||||
recipients: doc.value.data.email,
|
||||
recipients: recipients.join(', '),
|
||||
attachments: attachments.value.map((x) => x.name),
|
||||
cc: '',
|
||||
bcc: '',
|
||||
cc: cc.join(', '),
|
||||
bcc: bcc.join(', '),
|
||||
subject: 'Email from Agent',
|
||||
content: newEmail.value,
|
||||
doctype: props.doctype,
|
||||
|
||||
88
frontend/src/components/Controls/MultiselectInput.vue
Normal file
88
frontend/src/components/Controls/MultiselectInput.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Button
|
||||
v-for="value in values"
|
||||
:label="value"
|
||||
theme="gray"
|
||||
variant="subtle"
|
||||
class="rounded-full"
|
||||
>
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
class="h-3.5"
|
||||
name="x"
|
||||
@click.stop="removeValue(value)"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
<TextInput
|
||||
class="min-w-20 flex-1 border-none bg-white hover:bg-white focus:border-none focus:shadow-none focus-visible:ring-0"
|
||||
v-model="currentValue"
|
||||
@keydown.enter.capture.stop="addValue"
|
||||
@keydown.tab.capture.stop="addValue"
|
||||
@keydown.delete.capture.stop="removeLastValue"
|
||||
@keydown.meta.delete.capture.stop="removeAllValue"
|
||||
/>
|
||||
</div>
|
||||
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Button, ErrorMessage, FeatherIcon, TextInput } from 'frappe-ui'
|
||||
import { ref, defineModel } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
validate: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
errorMessage: {
|
||||
type: Function,
|
||||
default: (value) => `${value} is an Invalid value`,
|
||||
},
|
||||
})
|
||||
|
||||
const values = defineModel()
|
||||
const currentValue = ref('')
|
||||
const error = ref(null)
|
||||
|
||||
const addValue = () => {
|
||||
error.value = null
|
||||
if (currentValue.value) {
|
||||
const splitValues = currentValue.value.split(',')
|
||||
splitValues.forEach((value) => {
|
||||
value = value.trim()
|
||||
if (value) {
|
||||
// check if value is not already in the values array
|
||||
if (!values.value.includes(value)) {
|
||||
// check if value is valid
|
||||
if (value && props.validate && !props.validate(value)) {
|
||||
error.value = props.errorMessage(value)
|
||||
return
|
||||
}
|
||||
// add value to values array
|
||||
values.value.push(value)
|
||||
currentValue.value = currentValue.value.replace(value, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
!error.value && (currentValue.value = '')
|
||||
}
|
||||
}
|
||||
|
||||
const removeValue = (value) => {
|
||||
values.value = values.value.filter((v) => v !== value)
|
||||
}
|
||||
|
||||
const removeAllValue = () => {
|
||||
values.value = []
|
||||
}
|
||||
|
||||
const removeLastValue = () => {
|
||||
if (!currentValue.value) {
|
||||
values.value.pop()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -9,14 +9,39 @@
|
||||
:editable="editable"
|
||||
>
|
||||
<template #top>
|
||||
<div class="mx-10 border-b border-t py-2.5">
|
||||
<div
|
||||
class="mx-10 flex items-center gap-2 border-t py-2.5"
|
||||
:class="[cc || bcc ? '' : 'border-b']"
|
||||
>
|
||||
<span class="text-xs text-gray-500">TO:</span>
|
||||
<span
|
||||
v-if="modelValue.email"
|
||||
class="ml-2 cursor-pointer rounded-md bg-gray-100 px-2 py-1 text-sm text-gray-800"
|
||||
>
|
||||
{{ modelValue.email }}
|
||||
</span>
|
||||
<MultiselectInput
|
||||
class="flex-1"
|
||||
v-model="toEmails"
|
||||
:validate="validateEmail"
|
||||
:error-message="(value) => `${value} is an invalid email address`"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="cc"
|
||||
class="mx-10 flex items-center gap-2 py-2.5"
|
||||
:class="bcc ? '' : 'border-b'"
|
||||
>
|
||||
<span class="text-xs text-gray-500">CC:</span>
|
||||
<MultiselectInput
|
||||
class="flex-1"
|
||||
v-model="ccEmails"
|
||||
:validate="validateEmail"
|
||||
:error-message="(value) => `${value} is an invalid email address`"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="bcc" class="mx-10 flex items-center gap-2 border-b py-2.5">
|
||||
<span class="text-xs text-gray-500">BCC:</span>
|
||||
<MultiselectInput
|
||||
class="flex-1"
|
||||
v-model="bccEmails"
|
||||
:validate="validateEmail"
|
||||
:error-message="(value) => `${value} is an invalid email address`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:editor="{ editor }">
|
||||
@ -42,10 +67,12 @@
|
||||
</template>
|
||||
</AttachmentItem>
|
||||
</div>
|
||||
<div class="flex justify-between border-t px-10 py-2.5">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="flex justify-between gap-2 overflow-hidden border-t px-10 py-2.5"
|
||||
>
|
||||
<div class="flex items-center overflow-x-auto">
|
||||
<TextEditorFixedMenu
|
||||
class="-ml-1 overflow-x-auto"
|
||||
class="-ml-1"
|
||||
:buttons="textEditorMenuButtons"
|
||||
/>
|
||||
<FileUploader
|
||||
@ -70,10 +97,12 @@
|
||||
</FileUploader>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0">
|
||||
<Button v-bind="discardButtonProps || {}"> Discard </Button>
|
||||
<Button variant="solid" v-bind="submitButtonProps || {}">
|
||||
Submit
|
||||
</Button>
|
||||
<Button v-bind="discardButtonProps || {}" label="Discard" />
|
||||
<Button
|
||||
variant="solid"
|
||||
v-bind="submitButtonProps || {}"
|
||||
label="Submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -84,12 +113,14 @@
|
||||
<script setup>
|
||||
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||
import AttachmentItem from '@/components/AttachmentItem.vue'
|
||||
import MultiselectInput from '@/components/Controls/MultiselectInput.vue'
|
||||
import {
|
||||
TextEditorFixedMenu,
|
||||
TextEditor,
|
||||
FileUploader,
|
||||
FeatherIcon,
|
||||
} from 'frappe-ui'
|
||||
import { validateEmail } from '@/utils'
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import { ref, computed, defineModel } from 'vue'
|
||||
|
||||
@ -129,6 +160,12 @@ const modelValue = defineModel()
|
||||
const attachments = defineModel('attachments')
|
||||
|
||||
const textEditor = ref(null)
|
||||
const cc = ref(false)
|
||||
const bcc = ref(false)
|
||||
|
||||
const toEmails = ref(modelValue.value.email ? [modelValue.value.email] : [])
|
||||
const ccEmails = ref([])
|
||||
const bccEmails = ref([])
|
||||
|
||||
const editor = computed(() => {
|
||||
return textEditor.value.editor
|
||||
@ -138,7 +175,7 @@ function removeAttachment(attachment) {
|
||||
attachments.value = attachments.value.filter((a) => a !== attachment)
|
||||
}
|
||||
|
||||
defineExpose({ editor })
|
||||
defineExpose({ editor, cc, bcc, toEmails, ccEmails, bccEmails })
|
||||
|
||||
const textEditorMenuButtons = [
|
||||
'Paragraph',
|
||||
|
||||
16
frontend/src/components/Icons/ReplyAllIcon.vue
Normal file
16
frontend/src/components/Icons/ReplyAllIcon.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.85355 3.14645C5.65829 2.95118 5.34171 2.95118 5.14645 3.14645L1.14645 7.14645C0.951184 7.34171 0.951184 7.65829 1.14645 7.85355L5.14645 11.8536C5.34171 12.0488 5.65829 12.0488 5.85355 11.8536C6.04882 11.6583 6.04882 11.3417 5.85355 11.1464L2.20711 7.5L5.85355 3.85355C6.04882 3.65829 6.04882 3.34171 5.85355 3.14645ZM10 8C11.933 8 13.5 9.567 13.5 11.5V12C13.5 12.2761 13.7239 12.5 14 12.5C14.2761 12.5 14.5 12.2761 14.5 12V11.5C14.5 9.01472 12.4853 7 10 7H6.6728L9.81924 3.85355C10.0145 3.65829 10.0145 3.34171 9.81924 3.14645C9.62398 2.95118 9.3074 2.95118 9.11214 3.14645L5.11214 7.14645C4.91688 7.34171 4.91688 7.65829 5.11214 7.85355L9.11214 11.8536C9.3074 12.0488 9.62398 12.0488 9.81924 11.8536C10.0145 11.6583 10.0145 11.3417 9.81924 11.1464L6.6728 8H10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -106,3 +106,9 @@ export function formatNumberIntoCurrency(value) {
|
||||
export function startCase(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
export function validateEmail(email) {
|
||||
let regExp =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return regExp.test(email)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user