Skip to content

Commit

Permalink
Merge pull request #41 from shariquerik/email-box
Browse files Browse the repository at this point in the history
fix: Email Box
  • Loading branch information
shariquerik authored Dec 23, 2023
2 parents 0c08ba2 + 67ed472 commit b36eb58
Show file tree
Hide file tree
Showing 19 changed files with 1,834 additions and 844 deletions.
11 changes: 11 additions & 0 deletions crm/api/activities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import frappe
from frappe import _
from frappe.utils.caching import redis_cache
from frappe.desk.form.load import get_docinfo

@frappe.whitelist()
Expand Down Expand Up @@ -98,6 +99,7 @@ def get_deal_activities(name):
"recipients": communication.recipients,
"cc": communication.cc,
"bcc": communication.bcc,
"attachments": get_attachments(communication.name),
"read_by_recipient": communication.read_by_recipient,
},
"is_lead": False,
Expand Down Expand Up @@ -185,6 +187,7 @@ def get_lead_activities(name):
"recipients": communication.recipients,
"cc": communication.cc,
"bcc": communication.bcc,
"attachments": get_attachments(communication.name),
"read_by_recipient": communication.read_by_recipient,
},
"is_lead": True,
Expand All @@ -196,6 +199,14 @@ def get_lead_activities(name):

return activities

@redis_cache()
def get_attachments(name):
return frappe.db.get_all(
"File",
filters={"attached_to_doctype": "Communication", "attached_to_name": name},
fields=["name", "file_name", "file_url", "file_size", "is_private"],
)

def handle_multiple_versions(versions):
activities = []
grouped_versions = []
Expand Down
1 change: 1 addition & 0 deletions crm/fcrm/doctype/crm_call_log/crm_call_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def get_call_log(name):
if doc.note:
note = frappe.db.get_values("CRM Note", doc.note, ["title", "content"])[0]
_doc.note_doc = {
"name": doc.note,
"title": note[0],
"content": note[1]
}
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@vueuse/integrations": "^10.3.0",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.17",
"mime": "^4.0.1",
"pinia": "^2.0.33",
"socket.io-client": "^4.7.2",
"sortablejs": "^1.15.0",
Expand Down
97 changes: 74 additions & 23 deletions frontend/src/components/Activities.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
<span>New Task</span>
</Button>
</div>
<div v-if="activities?.length" class="flex-1 overflow-y-auto">
<div v-if="title == 'Notes'" class="grid grid-cols-3 gap-4 px-10 pb-5">
<div v-if="activities?.length" class="activities flex-1 overflow-y-auto">
<div
v-if="title == 'Notes'"
class="activity grid grid-cols-3 gap-4 px-10 pb-5"
>
<div
v-for="note in activities"
class="group flex h-48 cursor-pointer flex-col justify-between gap-2 rounded-md bg-gray-50 px-4 py-3 hover:bg-gray-100"
Expand Down Expand Up @@ -77,7 +80,7 @@
</div>
</div>
</div>
<div v-else-if="title == 'Tasks'" class="px-10 pb-5">
<div v-else-if="title == 'Tasks'" class="activity px-10 pb-5">
<div v-for="(task, i) in activities">
<div
class="flex cursor-pointer gap-6 rounded p-2.5 duration-300 ease-in-out hover:bg-gray-50"
Expand Down Expand Up @@ -164,7 +167,7 @@
/>
</div>
</div>
<div v-else-if="title == 'Calls'">
<div v-else-if="title == 'Calls'" class="activity">
<div v-for="(call, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
<div
Expand Down Expand Up @@ -222,11 +225,7 @@
v-if="call.show_recording"
class="flex items-center justify-between rounded border"
>
<audio
class="audio-control"
controls
:src="call.recording_url"
/>
<audio class="audio-control" controls :src="call.recording_url" />
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
Expand Down Expand Up @@ -266,7 +265,7 @@
</div>
</div>
</div>
<div v-else v-for="(activity, i) in activities">
<div v-else v-for="(activity, i) in activities" class="activity">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
<div
class="relative flex justify-center before:absolute before:left-[50%] before:top-0 before:-z-10 before:border-l before:border-gray-200"
Expand Down Expand Up @@ -316,15 +315,25 @@
{{ timeAgo(activity.creation) }}
</Tooltip>
</div>
<div>
<div class="flex gap-0.5">
<Button
variant="ghost"
icon="more-horizontal"
class="text-gray-600"
/>
class="text-gray-700"
@click="reply(activity.data.content)"
>
<ReplyIcon class="h-4 w-4" />
</Button>
</div>
</div>
<div class="px-1" v-html="activity.data.content" />
<span class="prose-f" v-html="activity.data.content" />
<div class="flex flex-wrap gap-2">
<AttachmentItem
v-for="a in activity.data.attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
/>
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -587,6 +596,8 @@
v-if="['Emails', 'Activity'].includes(title)"
v-model="doc"
v-model:reload="reload_email"
:doctype="doctype"
@scroll="scroll"
/>
<NoteModal
v-model="showNoteModal"
Expand Down Expand Up @@ -620,6 +631,8 @@ import DotIcon from '@/components/Icons/DotIcon.vue'
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 AttachmentItem from '@/components/AttachmentItem.vue'
import CommunicationArea from '@/components/CommunicationArea.vue'
import NoteModal from '@/components/Modals/NoteModal.vue'
import TaskModal from '@/components/Modals/TaskModal.vue'
Expand All @@ -644,7 +657,8 @@ import {
createListResource,
call,
} from 'frappe-ui'
import { ref, computed, h, defineModel, markRaw, watch } from 'vue'
import { useElementVisibility } from '@vueuse/core'
import { ref, computed, h, defineModel, markRaw, watch, nextTick } from 'vue'
const { getUser } = usersStore()
const { getContact } = contactsStore()
Expand Down Expand Up @@ -761,7 +775,7 @@ function all_activities() {
if (!versions.data) return []
if (!calls.data) return versions.data
return [...versions.data, ...calls.data].sort(
(a, b) => new Date(b.creation) - new Date(a.creation)
(a, b) => new Date(a.creation) - new Date(b.creation)
)
}
Expand All @@ -771,15 +785,21 @@ const activities = computed(() => {
activities = all_activities()
} else if (props.title == 'Emails') {
if (!versions.data) return []
activities = versions.data.filter(
(activity) => activity.activity_type === 'communication'
)
activities = versions.data
.filter((activity) => activity.activity_type === 'communication')
.sort((a, b) => new Date(a.creation) - new Date(b.creation))
} else if (props.title == 'Calls') {
return calls.data
return calls.data.sort(
(a, b) => new Date(a.creation) - new Date(b.creation)
)
} else if (props.title == 'Tasks') {
return tasks.data
return tasks.data.sort(
(a, b) => new Date(a.creation) - new Date(b.creation)
)
} else if (props.title == 'Notes') {
return notes.data
return notes.data.sort(
(a, b) => new Date(a.creation) - new Date(b.creation)
)
}
activities.forEach((activity) => {
activity.icon = timelineIcon(activity.activity_type, activity.is_lead)
Expand Down Expand Up @@ -876,6 +896,7 @@ function timelineIcon(activity_type, is_lead) {
// Notes
const showNoteModal = ref(false)
const note = ref({})
const emailBox = ref(null)
function showNote(n) {
note.value = n || {
Expand Down Expand Up @@ -928,13 +949,43 @@ function updateTaskStatus(status, task) {
})
}
// Email
function reply(message) {
emailBox.value.show = true
let editor = emailBox.value.editor.editor
editor
.chain()
.clearContent()
.insertContent(message)
.focus('all')
.setBlockquote()
.insertContentAt(0, { type: 'paragraph' })
.focus('start')
.run()
}
watch([reload, reload_email], ([reload_value, reload_email_value]) => {
if (reload_value || reload_email_value) {
versions.reload()
reload.value = false
reload_email.value = false
}
})
function scroll(el) {
setTimeout(() => {
if (!el) {
let e = document.getElementsByClassName('activity')
el = e[e.length - 1]
}
if (!useElementVisibility(el).value) {
el.scrollIntoView({ behavior: 'smooth' })
el.focus()
}
}, 500)
}
nextTick(() => scroll())
</script>
<style scoped>
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/components/AttachmentItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<template>
<span>
<a :href="isShowable ? null : url" target="_blank">
<Button
:label="label"
theme="gray"
variant="outline"
@click="toggleDialog()"
>
<template #prefix>
<component :is="getIcon()" class="h-4 w-4" />
</template>
<template #suffix>
<slot name="suffix" />
</template>
</Button>
</a>
<Dialog
v-model="showDialog"
:options="{
title: label,
size: '4xl',
}"
>
<template #body-content>
<div
v-if="isText"
class="prose prose-sm max-w-none whitespace-pre-wrap"
>
{{ content }}
</div>
<img v-if="isImage" :src="url" class="m-auto rounded border" />
</template>
</Dialog>
</span>
</template>

<script setup>
import { ref } from 'vue'
import { Button, Dialog } from 'frappe-ui'
import mime from 'mime'
import FileTypeIcon from '@/components/Icons/FileTypeIcon.vue'
import FileImageIcon from '@/components/Icons/FileImageIcon.vue'
import FileTextIcon from '@/components/Icons/FileTextIcon.vue'
import FileSpreadsheetIcon from '@/components/Icons/FileSpreadsheetIcon.vue'
import FileIcon from '@/components/Icons/FileIcon.vue'
const props = defineProps({
label: {
type: String,
default: null,
},
url: {
type: String,
default: null,
},
})
const showDialog = ref(false)
const mimeType = mime.getType(props.label) || ''
const isImage = mimeType.startsWith('image/')
const isPdf = mimeType === 'application/pdf'
const isSpreadsheet = mimeType.includes('spreadsheet')
const isText = mimeType === 'text/plain'
const isShowable = props.url && (isText || isImage)
const content = ref('')
function getIcon() {
if (isText) return FileTypeIcon
else if (isImage) return FileImageIcon
else if (isPdf) return FileTextIcon
else if (isSpreadsheet) return FileSpreadsheetIcon
else return FileIcon
}
function toggleDialog() {
if (!isShowable) return
if (isText) {
fetch(props.url).then((res) => res.text().then((t) => (content.value = t)))
}
showDialog.value = !showDialog.value
}
</script>
Loading

0 comments on commit b36eb58

Please sign in to comment.