fix: added send email feature using email editor
This commit is contained in:
parent
5d918fdeb6
commit
a68716c26e
@ -9,6 +9,7 @@
|
|||||||
"serve": "vite preview"
|
"serve": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tiptap/vue-3": "^2.0.4",
|
||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.3.0",
|
||||||
"@vueuse/integrations": "^10.3.0",
|
"@vueuse/integrations": "^10.3.0",
|
||||||
@ -17,11 +18,11 @@
|
|||||||
"frappe-ui": "^0.1.0-alpha.11",
|
"frappe-ui": "^0.1.0-alpha.11",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"sortablejs": "^1.15.0",
|
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
|
"sortablejs": "^1.15.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
|
"vite": "^4.4.9",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.2"
|
||||||
"vite": "^4.4.9"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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()" />
|
<Button label="Save" variant="solid" @click="() => updateLead()" />
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</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">
|
<TabList class="flex items-center gap-6 border-b pl-5 relative">
|
||||||
<Tab
|
<Tab
|
||||||
ref="tabRef"
|
ref="tabRef"
|
||||||
@ -58,14 +58,19 @@
|
|||||||
:style="{ left: `${indicatorLeftValue}px` }"
|
:style="{ left: `${indicatorLeftValue}px` }"
|
||||||
/>
|
/>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels class="flex h-full overflow-hidden">
|
<div class="flex h-full overflow-hidden">
|
||||||
<TabPanel
|
<div class="flex-1 flex flex-col">
|
||||||
class="flex-1 overflow-y-auto"
|
<TabPanels class="flex flex-1 overflow-hidden">
|
||||||
v-for="tab in tabs"
|
<TabPanel
|
||||||
:key="tab.label"
|
class="flex-1 overflow-y-auto"
|
||||||
>
|
v-for="tab in tabs"
|
||||||
<Activities :title="tab.activityTitle" :activities="tab.content" />
|
:key="tab.label"
|
||||||
</TabPanel>
|
>
|
||||||
|
<Activities :title="tab.activityTitle" :activities="tab.content" />
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
<CommunicationArea v-if="[0, 1].includes(selectedIndex)" v-model="lead" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-between border-l w-[390px] overflow-hidden"
|
class="flex flex-col justify-between border-l w-[390px] overflow-hidden"
|
||||||
>
|
>
|
||||||
@ -197,7 +202,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanels>
|
</div>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -212,6 +217,7 @@ import Toggler from '@/components/Toggler.vue'
|
|||||||
import Activities from '@/components/Activities.vue'
|
import Activities from '@/components/Activities.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
import CommunicationArea from '../components/CommunicationArea.vue'
|
||||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
||||||
import { TransitionPresets, useTransition } from '@vueuse/core'
|
import { TransitionPresets, useTransition } from '@vueuse/core'
|
||||||
import { dateFormat, timeAgo, dateTooltipFormat } from '@/utils'
|
import { dateFormat, timeAgo, dateTooltipFormat } from '@/utils'
|
||||||
|
|||||||
@ -5,7 +5,12 @@ import frappeui from 'frappe-ui/vite'
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [frappeui(), vue()],
|
plugins: [frappeui(), vue({
|
||||||
|
script: {
|
||||||
|
defineModel: true,
|
||||||
|
propsDestructure: true
|
||||||
|
}
|
||||||
|
})],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
|||||||
@ -2752,7 +2752,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.4.tgz#08e6c47a723200d02238d845cb09684c481f0066"
|
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.4.tgz#08e6c47a723200d02238d845cb09684c481f0066"
|
||||||
integrity sha512-C5LGGjH8VFET34V7vKkqlwpSzrPl+7oAcj9h+P3jvJQ076iYpmpnMtz6dNLSFGKpHp5mtyl4RoJzh7lTvlfyiA==
|
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"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/vue-3/-/vue-3-2.0.4.tgz#f04116a42aa60285026541a9c8db459dfa92e1a9"
|
resolved "https://registry.yarnpkg.com/@tiptap/vue-3/-/vue-3-2.0.4.tgz#f04116a42aa60285026541a9c8db459dfa92e1a9"
|
||||||
integrity sha512-XfoFl1RKCElYIoloGoqMC2iG4RalEtaGvwSAmqqNGdITCdwnuDhLlCvGAjnVbIR4d3Y0NRPyXZzGWfWSi4bbHg==
|
integrity sha512-XfoFl1RKCElYIoloGoqMC2iG4RalEtaGvwSAmqqNGdITCdwnuDhLlCvGAjnVbIR4d3Y0NRPyXZzGWfWSi4bbHg==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user