fix: added send email feature using email editor
This commit is contained in:
parent
5d918fdeb6
commit
a68716c26e
@ -9,6 +9,7 @@
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/vue-3": "^2.0.4",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/integrations": "^10.3.0",
|
||||
@ -17,11 +18,11 @@
|
||||
"frappe-ui": "^0.1.0-alpha.11",
|
||||
"pinia": "^2.0.33",
|
||||
"postcss": "^8.4.5",
|
||||
"sortablejs": "^1.15.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"vite": "^4.4.9",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2",
|
||||
"vite": "^4.4.9"
|
||||
"vue-router": "^4.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
100
frontend/src/components/CommunicationArea.vue
Normal file
100
frontend/src/components/CommunicationArea.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="max-w-[81.7%] bg-white pl-16 p-4 pt-2 z-20">
|
||||
<button
|
||||
class="flex w-full items-center rounded-lg bg-gray-100 px-2 py-2 text-left text-base text-gray-600 hover:bg-gray-200"
|
||||
@click="showCommunicationBox = true"
|
||||
v-show="!showCommunicationBox"
|
||||
>
|
||||
<UserAvatar class="mr-3" :user="getUser().name" size="sm" />
|
||||
Add a reply...
|
||||
</button>
|
||||
<div
|
||||
v-show="showCommunicationBox"
|
||||
class="w-full rounded-lg border bg-white p-4 focus-within:border-gray-400"
|
||||
@keydown.ctrl.enter.capture.stop="submitComment"
|
||||
@keydown.meta.enter.capture.stop="submitComment"
|
||||
>
|
||||
<div class="mb-4 flex items-center">
|
||||
<UserAvatar :user="getUser().name" size="sm" />
|
||||
<span class="ml-2 text-base font-medium text-gray-900">
|
||||
{{ getUser().full_name }}
|
||||
</span>
|
||||
</div>
|
||||
<EmailEditor
|
||||
ref="newEmailEditor"
|
||||
:value="newEmail"
|
||||
@change="onNewEmailChange"
|
||||
:submitButtonProps="{
|
||||
variant: 'solid',
|
||||
onClick: submitComment,
|
||||
disabled: emailEmpty,
|
||||
}"
|
||||
:discardButtonProps="{
|
||||
onClick: () => {
|
||||
showCommunicationBox = false
|
||||
newEmail = ''
|
||||
},
|
||||
}"
|
||||
:editable="showCommunicationBox"
|
||||
v-model="modelValue.data"
|
||||
placeholder="Add a reply..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import EmailEditor from '@/components/EmailEditor.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { call } from 'frappe-ui'
|
||||
import { ref, watch, computed, defineModel } from 'vue'
|
||||
|
||||
const modelValue = defineModel()
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const showCommunicationBox = ref(false)
|
||||
const newEmail = ref('<p>Hi,<br><br>Gentle reminder!<br>We have a call at 3 - 5 PM today.<br><br>Thanks & Regards<br>Shariq Ansari</p>')
|
||||
const newEmailEditor = ref(null)
|
||||
|
||||
watch(
|
||||
() => showCommunicationBox.value,
|
||||
(value) => {
|
||||
if (value) {
|
||||
newEmailEditor.value.editor.commands.focus()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const emailEmpty = computed(() => {
|
||||
return !newEmail.value || newEmail.value === '<p></p>'
|
||||
})
|
||||
|
||||
const onNewEmailChange = (value) => {
|
||||
newEmail.value = value
|
||||
}
|
||||
|
||||
async function sendMail() {
|
||||
await call('frappe.core.doctype.communication.email.make', {
|
||||
recipients: modelValue.value.data.email,
|
||||
cc: '',
|
||||
bcc: '',
|
||||
subject: 'Email from Agent',
|
||||
content: newEmail.value,
|
||||
doctype: 'CRM Lead',
|
||||
name: modelValue.value.data.name,
|
||||
send_email: 1,
|
||||
sender: getUser().name,
|
||||
sender_full_name: getUser()?.full_name || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
if (emailEmpty.value) return
|
||||
showCommunicationBox.value = false
|
||||
await sendMail()
|
||||
newEmail.value = ''
|
||||
modelValue.value.reload()
|
||||
}
|
||||
</script>
|
||||
127
frontend/src/components/EmailEditor.vue
Normal file
127
frontend/src/components/EmailEditor.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<TextEditor
|
||||
ref="textEditor"
|
||||
:editor-class="['prose-sm max-w-none', editable && 'min-h-[4rem]']"
|
||||
:content="value"
|
||||
@change="editable ? $emit('change', $event) : null"
|
||||
:starterkit-options="{ heading: { levels: [2, 3, 4, 5, 6] } }"
|
||||
:placeholder="placeholder"
|
||||
:editable="editable"
|
||||
>
|
||||
<template #top>
|
||||
<div class="mb-2">
|
||||
<span class="text-base text-gray-600">To:</span>
|
||||
<span
|
||||
v-if="modelValue.email"
|
||||
class="ml-2 bg-gray-100 px-2 py-1 rounded-md text-sm text-gray-800 cursor-pointer"
|
||||
>{{ modelValue.email }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:editor="{ editor }">
|
||||
<EditorContent
|
||||
:class="[editable && 'max-h-[50vh] overflow-y-auto']"
|
||||
:editor="editor"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:bottom>
|
||||
<div
|
||||
v-if="editable"
|
||||
class="mt-2 flex flex-col justify-between sm:flex-row sm:items-center"
|
||||
>
|
||||
<TextEditorFixedMenu
|
||||
class="-ml-1 overflow-x-auto"
|
||||
:buttons="textEditorMenuButtons"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TextEditor>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TextEditorFixedMenu, TextEditor } from 'frappe-ui'
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import { ref, computed, defineModel } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
editorProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
submitButtonProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
discardButtonProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
const modelValue = defineModel()
|
||||
|
||||
const textEditor = ref(null)
|
||||
|
||||
const editor = computed(() => {
|
||||
return textEditor.value.editor
|
||||
})
|
||||
|
||||
defineExpose({ editor })
|
||||
|
||||
const textEditorMenuButtons = [
|
||||
'Paragraph',
|
||||
['Heading 2', 'Heading 3', 'Heading 4', 'Heading 5', 'Heading 6'],
|
||||
'Separator',
|
||||
'Bold',
|
||||
'Italic',
|
||||
'Separator',
|
||||
'Bullet List',
|
||||
'Numbered List',
|
||||
'Separator',
|
||||
'Align Left',
|
||||
'Align Center',
|
||||
'Align Right',
|
||||
'FontColor',
|
||||
'Separator',
|
||||
'Image',
|
||||
'Video',
|
||||
'Link',
|
||||
'Blockquote',
|
||||
'Code',
|
||||
'Horizontal Rule',
|
||||
[
|
||||
'InsertTable',
|
||||
'AddColumnBefore',
|
||||
'AddColumnAfter',
|
||||
'DeleteColumn',
|
||||
'AddRowBefore',
|
||||
'AddRowAfter',
|
||||
'DeleteRow',
|
||||
'MergeCells',
|
||||
'SplitCell',
|
||||
'ToggleHeaderColumn',
|
||||
'ToggleHeaderRow',
|
||||
'ToggleHeaderCell',
|
||||
'DeleteTable',
|
||||
],
|
||||
]
|
||||
</script>
|
||||
@ -35,7 +35,7 @@
|
||||
<Button label="Save" variant="solid" @click="() => updateLead()" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
<TabGroup v-if="lead.data" @change="onTabChange">
|
||||
<TabGroup v-slot="{ selectedIndex }" v-if="lead.data" @change="onTabChange">
|
||||
<TabList class="flex items-center gap-6 border-b pl-5 relative">
|
||||
<Tab
|
||||
ref="tabRef"
|
||||
@ -58,14 +58,19 @@
|
||||
:style="{ left: `${indicatorLeftValue}px` }"
|
||||
/>
|
||||
</TabList>
|
||||
<TabPanels class="flex h-full overflow-hidden">
|
||||
<TabPanel
|
||||
class="flex-1 overflow-y-auto"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.label"
|
||||
>
|
||||
<Activities :title="tab.activityTitle" :activities="tab.content" />
|
||||
</TabPanel>
|
||||
<div class="flex h-full overflow-hidden">
|
||||
<div class="flex-1 flex flex-col">
|
||||
<TabPanels class="flex flex-1 overflow-hidden">
|
||||
<TabPanel
|
||||
class="flex-1 overflow-y-auto"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.label"
|
||||
>
|
||||
<Activities :title="tab.activityTitle" :activities="tab.content" />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
<CommunicationArea v-if="[0, 1].includes(selectedIndex)" v-model="lead" />
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col justify-between border-l w-[390px] overflow-hidden"
|
||||
>
|
||||
@ -197,7 +202,7 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanels>
|
||||
</div>
|
||||
</TabGroup>
|
||||
</template>
|
||||
<script setup>
|
||||
@ -212,6 +217,7 @@ import Toggler from '@/components/Toggler.vue'
|
||||
import Activities from '@/components/Activities.vue'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import CommunicationArea from '../components/CommunicationArea.vue'
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
||||
import { TransitionPresets, useTransition } from '@vueuse/core'
|
||||
import { dateFormat, timeAgo, dateTooltipFormat } from '@/utils'
|
||||
|
||||
@ -5,7 +5,12 @@ import frappeui from 'frappe-ui/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [frappeui(), vue()],
|
||||
plugins: [frappeui(), vue({
|
||||
script: {
|
||||
defineModel: true,
|
||||
propsDestructure: true
|
||||
}
|
||||
})],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
|
||||
@ -2752,7 +2752,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.4.tgz#08e6c47a723200d02238d845cb09684c481f0066"
|
||||
integrity sha512-C5LGGjH8VFET34V7vKkqlwpSzrPl+7oAcj9h+P3jvJQ076iYpmpnMtz6dNLSFGKpHp5mtyl4RoJzh7lTvlfyiA==
|
||||
|
||||
"@tiptap/vue-3@^2.0.3":
|
||||
"@tiptap/vue-3@^2.0.3", "@tiptap/vue-3@^2.0.4":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/vue-3/-/vue-3-2.0.4.tgz#f04116a42aa60285026541a9c8db459dfa92e1a9"
|
||||
integrity sha512-XfoFl1RKCElYIoloGoqMC2iG4RalEtaGvwSAmqqNGdITCdwnuDhLlCvGAjnVbIR4d3Y0NRPyXZzGWfWSi4bbHg==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user