Merge pull request #43 from shariquerik/cc-bcc-reply-all

feat: CC, BCC & ReplyAll
This commit is contained in:
Shariq Ansari 2023-12-26 20:55:04 +05:30 committed by GitHub
commit 96819956c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 241 additions and 41 deletions

View File

@ -354,7 +354,7 @@
<div <div
class="cursor-pointer rounded-md bg-gray-50 p-3 text-base leading-6 transition-all duration-300 ease-in-out" 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"> <div class="flex items-center gap-2">
<UserAvatar :user="activity.data.sender" size="md" /> <UserAvatar :user="activity.data.sender" size="md" />
<span>{{ activity.data.sender_full_name }}</span> <span>{{ activity.data.sender_full_name }}</span>
@ -370,12 +370,29 @@
<Button <Button
variant="ghost" variant="ghost"
class="text-gray-700" class="text-gray-700"
@click="reply(activity.data.content)" @click="reply(activity.data)"
> >
<ReplyIcon class="h-4 w-4" /> <ReplyIcon class="h-4 w-4" />
</Button> </Button>
<Button
variant="ghost"
class="text-gray-700"
@click="reply(activity.data, true)"
>
<ReplyAllIcon class="h-4 w-4" />
</Button>
</div> </div>
</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" /> <span class="prose-f" v-html="activity.data.content" />
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<AttachmentItem <AttachmentItem
@ -679,6 +696,7 @@ import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
import InboundCallIcon from '@/components/Icons/InboundCallIcon.vue' import InboundCallIcon from '@/components/Icons/InboundCallIcon.vue'
import OutboundCallIcon from '@/components/Icons/OutboundCallIcon.vue' import OutboundCallIcon from '@/components/Icons/OutboundCallIcon.vue'
import ReplyIcon from '@/components/Icons/ReplyIcon.vue' import ReplyIcon from '@/components/Icons/ReplyIcon.vue'
import ReplyAllIcon from '@/components/Icons/ReplyAllIcon.vue'
import AttachmentItem from '@/components/AttachmentItem.vue' import AttachmentItem from '@/components/AttachmentItem.vue'
import CommunicationArea from '@/components/CommunicationArea.vue' import CommunicationArea from '@/components/CommunicationArea.vue'
import NoteModal from '@/components/Modals/NoteModal.vue' import NoteModal from '@/components/Modals/NoteModal.vue'
@ -997,10 +1015,28 @@ function updateTaskStatus(status, task) {
} }
// Email // Email
function reply(message) { function reply(email, reply_all = false) {
emailBox.value.show = true emailBox.value.show = true
let editor = emailBox.value.editor.editor let editor = emailBox.value.editor
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() .chain()
.clearContent() .clearContent()
.insertContent(message) .insertContent(message)

View File

@ -1,21 +1,35 @@
<template> <template>
<div class="flex gap-1.5 border-t px-10 py-2.5"> <div class="flex justify-between gap-3 border-t px-10 py-2.5">
<Button <div class="flex gap-1.5">
ref="sendEmailRef" <Button
variant="ghost" ref="sendEmailRef"
:class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']" variant="ghost"
label="Reply" :class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
@click="showCommunicationBox = !showCommunicationBox" label="Reply"
> @click="showCommunicationBox = !showCommunicationBox"
<template #prefix> >
<EmailIcon class="h-4" /> <template #prefix>
</template> <EmailIcon class="h-4" />
</Button> </template>
<!-- <Button variant="ghost" label="Comment"> </Button>
<template #prefix> <!-- <Button variant="ghost" label="Comment">
<CommentIcon class="h-4" /> <template #prefix>
</template> <CommentIcon class="h-4" />
</Button> --> </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>
<div <div
v-show="showCommunicationBox" v-show="showCommunicationBox"
@ -71,7 +85,7 @@ const showCommunicationBox = ref(false)
const newEmail = ref('') const newEmail = ref('')
const newEmailEditor = ref(null) const newEmailEditor = ref(null)
const sendEmailRef = ref(null) const sendEmailRef = ref(null)
const attachments = ref([]); const attachments = ref([])
watch( watch(
() => showCommunicationBox.value, () => showCommunicationBox.value,
@ -91,11 +105,14 @@ const onNewEmailChange = (value) => {
} }
async function sendMail() { 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', { await call('frappe.core.doctype.communication.email.make', {
recipients: doc.value.data.email, recipients: recipients.join(', '),
attachments: attachments.value.map((x) => x.name), attachments: attachments.value.map((x) => x.name),
cc: '', cc: cc.join(', '),
bcc: '', bcc: bcc.join(', '),
subject: 'Email from Agent', subject: 'Email from Agent',
content: newEmail.value, content: newEmail.value,
doctype: props.doctype, doctype: props.doctype,

View 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>

View File

@ -9,14 +9,39 @@
:editable="editable" :editable="editable"
> >
<template #top> <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 class="text-xs text-gray-500">TO:</span>
<span <MultiselectInput
v-if="modelValue.email" class="flex-1"
class="ml-2 cursor-pointer rounded-md bg-gray-100 px-2 py-1 text-sm text-gray-800" v-model="toEmails"
> :validate="validateEmail"
{{ modelValue.email }} :error-message="(value) => `${value} is an invalid email address`"
</span> />
</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> </div>
</template> </template>
<template v-slot:editor="{ editor }"> <template v-slot:editor="{ editor }">
@ -42,10 +67,12 @@
</template> </template>
</AttachmentItem> </AttachmentItem>
</div> </div>
<div class="flex justify-between border-t px-10 py-2.5"> <div
<div class="flex items-center"> class="flex justify-between gap-2 overflow-hidden border-t px-10 py-2.5"
>
<div class="flex items-center overflow-x-auto">
<TextEditorFixedMenu <TextEditorFixedMenu
class="-ml-1 overflow-x-auto" class="-ml-1"
:buttons="textEditorMenuButtons" :buttons="textEditorMenuButtons"
/> />
<FileUploader <FileUploader
@ -70,10 +97,12 @@
</FileUploader> </FileUploader>
</div> </div>
<div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0"> <div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0">
<Button v-bind="discardButtonProps || {}"> Discard </Button> <Button v-bind="discardButtonProps || {}" label="Discard" />
<Button variant="solid" v-bind="submitButtonProps || {}"> <Button
Submit variant="solid"
</Button> v-bind="submitButtonProps || {}"
label="Submit"
/>
</div> </div>
</div> </div>
</div> </div>
@ -84,12 +113,14 @@
<script setup> <script setup>
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue' import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
import AttachmentItem from '@/components/AttachmentItem.vue' import AttachmentItem from '@/components/AttachmentItem.vue'
import MultiselectInput from '@/components/Controls/MultiselectInput.vue'
import { import {
TextEditorFixedMenu, TextEditorFixedMenu,
TextEditor, TextEditor,
FileUploader, FileUploader,
FeatherIcon, FeatherIcon,
} from 'frappe-ui' } from 'frappe-ui'
import { validateEmail } from '@/utils'
import { EditorContent } from '@tiptap/vue-3' import { EditorContent } from '@tiptap/vue-3'
import { ref, computed, defineModel } from 'vue' import { ref, computed, defineModel } from 'vue'
@ -129,6 +160,12 @@ const modelValue = defineModel()
const attachments = defineModel('attachments') const attachments = defineModel('attachments')
const textEditor = ref(null) 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(() => { const editor = computed(() => {
return textEditor.value.editor return textEditor.value.editor
@ -138,7 +175,7 @@ function removeAttachment(attachment) {
attachments.value = attachments.value.filter((a) => a !== attachment) attachments.value = attachments.value.filter((a) => a !== attachment)
} }
defineExpose({ editor }) defineExpose({ editor, cc, bcc, toEmails, ccEmails, bccEmails })
const textEditorMenuButtons = [ const textEditorMenuButtons = [
'Paragraph', 'Paragraph',

View 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>

View File

@ -106,3 +106,9 @@ export function formatNumberIntoCurrency(value) {
export function startCase(str) { export function startCase(str) {
return str.charAt(0).toUpperCase() + str.slice(1) 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)
}