fix: allow uploading file & show on email and email box
This commit is contained in:
parent
9d26985609
commit
ccd57e213a
@ -2,6 +2,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils.caching import redis_cache
|
||||
from frappe.desk.form.load import get_docinfo
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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 = []
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -326,6 +326,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
@ -588,6 +596,7 @@
|
||||
v-if="['Emails', 'Activity'].includes(title)"
|
||||
v-model="doc"
|
||||
v-model:reload="reload_email"
|
||||
:doctype="doctype"
|
||||
@scroll="scroll"
|
||||
/>
|
||||
<NoteModal
|
||||
@ -623,6 +632,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 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'
|
||||
|
||||
83
frontend/src/components/AttachmentItem.vue
Normal file
83
frontend/src/components/AttachmentItem.vue
Normal file
@ -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>
|
||||
@ -39,6 +39,8 @@
|
||||
}"
|
||||
:editable="showCommunicationBox"
|
||||
v-model="doc.data"
|
||||
v-model:attachments="attachments"
|
||||
:doctype="doctype"
|
||||
placeholder="Add a reply..."
|
||||
/>
|
||||
</div>
|
||||
@ -51,6 +53,13 @@ import { usersStore } from '@/stores/users'
|
||||
import { call } from 'frappe-ui'
|
||||
import { ref, watch, computed, defineModel } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
type: String,
|
||||
default: 'CRM Lead',
|
||||
},
|
||||
})
|
||||
|
||||
const doc = defineModel()
|
||||
const reload = defineModel('reload')
|
||||
|
||||
@ -62,6 +71,7 @@ const showCommunicationBox = ref(false)
|
||||
const newEmail = ref('')
|
||||
const newEmailEditor = ref(null)
|
||||
const sendEmailRef = ref(null)
|
||||
const attachments = ref([]);
|
||||
|
||||
watch(
|
||||
() => showCommunicationBox.value,
|
||||
@ -81,18 +91,14 @@ const onNewEmailChange = (value) => {
|
||||
}
|
||||
|
||||
async function sendMail() {
|
||||
let doctype = 'CRM Lead'
|
||||
if (doc.value.data.lead) {
|
||||
doctype = 'CRM Deal'
|
||||
}
|
||||
|
||||
await call('frappe.core.doctype.communication.email.make', {
|
||||
recipients: doc.value.data.email,
|
||||
attachments: attachments.value.map((x) => x.name),
|
||||
cc: '',
|
||||
bcc: '',
|
||||
subject: 'Email from Agent',
|
||||
content: newEmail.value,
|
||||
doctype: doctype,
|
||||
doctype: props.doctype,
|
||||
name: doc.value.data.name,
|
||||
send_email: 1,
|
||||
sender: getUser().name,
|
||||
|
||||
@ -85,6 +85,7 @@ const text = ref('')
|
||||
watchDebounced(
|
||||
() => autocomplete.value?.query,
|
||||
(val) => {
|
||||
val = val || ''
|
||||
if (text.value === val) return
|
||||
text.value = val
|
||||
options.update({
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
:editable="editable"
|
||||
>
|
||||
<template #top>
|
||||
<div class="mx-10 border-t border-b py-2.5">
|
||||
<div class="mx-10 border-b border-t py-2.5">
|
||||
<span class="text-xs text-gray-500">TO:</span>
|
||||
<span
|
||||
v-if="modelValue.email"
|
||||
@ -21,21 +21,60 @@
|
||||
</template>
|
||||
<template v-slot:editor="{ editor }">
|
||||
<EditorContent
|
||||
:class="[editable && 'mx-10 py-3 max-h-[50vh] overflow-y-auto']"
|
||||
:class="[editable && 'mx-10 max-h-[50vh] overflow-y-auto py-3']"
|
||||
:editor="editor"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:bottom>
|
||||
<div v-if="editable" class="flex justify-between border-t px-10 py-2.5">
|
||||
<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 v-if="editable" class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 px-10">
|
||||
<AttachmentItem
|
||||
v-for="a in attachments"
|
||||
:key="a.file_url"
|
||||
:label="a.file_name"
|
||||
>
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
class="h-3.5"
|
||||
name="x"
|
||||
@click.stop="removeAttachment(a)"
|
||||
/>
|
||||
</template>
|
||||
</AttachmentItem>
|
||||
</div>
|
||||
<div class="flex justify-between border-t px-10 py-2.5">
|
||||
<div class="flex items-center">
|
||||
<TextEditorFixedMenu
|
||||
class="-ml-1 overflow-x-auto"
|
||||
:buttons="textEditorMenuButtons"
|
||||
/>
|
||||
<FileUploader
|
||||
:upload-args="{
|
||||
doctype: doctype,
|
||||
docname: modelValue.name,
|
||||
private: true,
|
||||
}"
|
||||
@success="(f) => attachments.push(f)"
|
||||
>
|
||||
<template #default="{ openFileSelector }">
|
||||
<Button
|
||||
theme="gray"
|
||||
variant="ghost"
|
||||
@click="openFileSelector()"
|
||||
>
|
||||
<template #icon>
|
||||
<AttachmentIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -43,7 +82,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TextEditorFixedMenu, TextEditor } from 'frappe-ui'
|
||||
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||
import AttachmentItem from '@/components/AttachmentItem.vue'
|
||||
import {
|
||||
TextEditorFixedMenu,
|
||||
TextEditor,
|
||||
FileUploader,
|
||||
FeatherIcon,
|
||||
} from 'frappe-ui'
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import { ref, computed, defineModel } from 'vue'
|
||||
|
||||
@ -60,6 +106,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
doctype: {
|
||||
type: String,
|
||||
default: 'CRM Lead',
|
||||
},
|
||||
editorProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
@ -76,6 +126,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
const modelValue = defineModel()
|
||||
const attachments = defineModel('attachments')
|
||||
|
||||
const textEditor = ref(null)
|
||||
|
||||
@ -83,6 +134,10 @@ const editor = computed(() => {
|
||||
return textEditor.value.editor
|
||||
})
|
||||
|
||||
function removeAttachment(attachment) {
|
||||
attachments.value = attachments.value.filter((a) => a !== attachment)
|
||||
}
|
||||
|
||||
defineExpose({ editor })
|
||||
|
||||
const textEditorMenuButtons = [
|
||||
|
||||
16
frontend/src/components/Icons/AttachmentIcon.vue
Normal file
16
frontend/src/components/Icons/AttachmentIcon.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="M12.5684 2.50774C11.5403 1.49742 9.95026 1.49742 8.92215 2.50774L3.62404 7.71417C2.12532 9.18695 2.12532 11.669 3.62404 13.1418C5.12762 14.6194 7.66861 14.6194 9.17219 13.1418L12.1609 10.2049C12.3578 10.0113 12.6744 10.0141 12.8679 10.211C13.0615 10.408 13.0587 10.7246 12.8618 10.9181L9.8731 13.8551C7.98045 15.715 4.81578 15.715 2.92313 13.8551C1.02562 11.9904 1.02562 8.86558 2.92313 7.00091L8.22124 1.79449C9.63842 0.401838 11.8521 0.401838 13.2693 1.79449C14.6914 3.19191 14.6914 5.38225 13.2693 6.77968L13.2668 6.78213L13.2668 6.78212L8.37876 11.5189C8.37834 11.5193 8.37793 11.5197 8.37752 11.5201C7.51767 12.3638 6.11144 12.3939 5.29119 11.5097C4.43611 10.6596 4.40778 9.26893 5.30922 8.46081L7.33823 6.46692C7.53518 6.27337 7.85175 6.27613 8.04531 6.47309C8.23886 6.67005 8.23609 6.98662 8.03913 7.18017L6.0014 9.18264L5.99203 9.19185L5.98219 9.20055C5.5391 9.59243 5.5104 10.3231 6.0014 10.8056L6.01078 10.8148L6.01967 10.8245C6.42299 11.2649 7.18224 11.2926 7.67785 10.8056L7.68034 10.8032L7.68035 10.8032L12.5684 6.06643C12.5688 6.06604 12.5692 6.06565 12.5696 6.06526C13.5917 5.05969 13.5913 3.51289 12.5684 2.50774Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
19
frontend/src/components/Icons/FileIcon.vue
Normal file
19
frontend/src/components/Icons/FileIcon.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
</template>
|
||||
21
frontend/src/components/Icons/FileImageIcon.vue
Normal file
21
frontend/src/components/Icons/FileImageIcon.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file-image"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<circle cx="10" cy="13" r="2" />
|
||||
<path d="m20 17-1.09-1.09a2 2 0 0 0-2.82 0L10 22" />
|
||||
</svg>
|
||||
</template>
|
||||
23
frontend/src/components/Icons/FileSpreadsheetIcon.vue
Normal file
23
frontend/src/components/Icons/FileSpreadsheetIcon.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file-spreadsheet"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="M8 13h2" />
|
||||
<path d="M8 17h2" />
|
||||
<path d="M14 13h2" />
|
||||
<path d="M14 17h2" />
|
||||
</svg>
|
||||
</template>
|
||||
22
frontend/src/components/Icons/FileTextIcon.vue
Normal file
22
frontend/src/components/Icons/FileTextIcon.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file-text"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" x2="8" y1="13" y2="13" />
|
||||
<line x1="16" x2="8" y1="17" y2="17" />
|
||||
<line x1="10" x2="8" y1="9" y2="9" />
|
||||
</svg>
|
||||
</template>
|
||||
22
frontend/src/components/Icons/FileTypeIcon.vue
Normal file
22
frontend/src/components/Icons/FileTypeIcon.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file-type"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="M9 13v-1h6v1" />
|
||||
<path d="M11 18h2" />
|
||||
<path d="M12 12v6" />
|
||||
</svg>
|
||||
</template>
|
||||
Loading…
x
Reference in New Issue
Block a user