commit
b36eb58ace
@ -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 = []
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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">
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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"
|
||||
@ -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'
|
||||
@ -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()
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -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 || {
|
||||
@ -928,6 +949,21 @@ 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()
|
||||
@ -935,6 +971,21 @@ watch([reload, reload_email], ([reload_value, reload_email_value]) => {
|
||||
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>
|
||||
|
||||
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>
|
||||
@ -1,70 +1,77 @@
|
||||
<template>
|
||||
<div class="flex gap-3 px-10 pb-6 pt-2">
|
||||
<UserAvatar
|
||||
:user="getUser().name"
|
||||
size="xl"
|
||||
:class="showCommunicationBox ? 'mt-3' : ''"
|
||||
/>
|
||||
<div class="flex gap-1.5 border-t px-10 py-2.5">
|
||||
<Button
|
||||
ref="sendEmailRef"
|
||||
variant="outline"
|
||||
size="md"
|
||||
class="inline-flex h-8.5 w-full justify-between"
|
||||
@click="showCommunicationBox = true"
|
||||
v-show="!showCommunicationBox"
|
||||
variant="ghost"
|
||||
:class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||
label="Reply"
|
||||
@click="showCommunicationBox = !showCommunicationBox"
|
||||
>
|
||||
<div class="text-base text-gray-600">Add a reply...</div>
|
||||
<template #suffix>
|
||||
<div class="flex gap-3">
|
||||
<!-- <FeatherIcon name="paperclip" class="h-4" /> -->
|
||||
</div>
|
||||
<template #prefix>
|
||||
<EmailIcon class="h-4" />
|
||||
</template>
|
||||
</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"
|
||||
>
|
||||
<EmailEditor
|
||||
ref="newEmailEditor"
|
||||
:value="newEmail"
|
||||
@change="onNewEmailChange"
|
||||
:submitButtonProps="{
|
||||
variant: 'solid',
|
||||
onClick: submitComment,
|
||||
disabled: emailEmpty,
|
||||
}"
|
||||
:discardButtonProps="{
|
||||
onClick: () => {
|
||||
showCommunicationBox = false
|
||||
newEmail = ''
|
||||
},
|
||||
}"
|
||||
:editable="showCommunicationBox"
|
||||
v-model="doc.data"
|
||||
placeholder="Add a reply..."
|
||||
/>
|
||||
</div>
|
||||
<!-- <Button variant="ghost" label="Comment">
|
||||
<template #prefix>
|
||||
<CommentIcon class="h-4" />
|
||||
</template>
|
||||
</Button> -->
|
||||
</div>
|
||||
<div
|
||||
v-show="showCommunicationBox"
|
||||
@keydown.ctrl.enter.capture.stop="submitComment"
|
||||
@keydown.meta.enter.capture.stop="submitComment"
|
||||
>
|
||||
<EmailEditor
|
||||
ref="newEmailEditor"
|
||||
:value="newEmail"
|
||||
@change="onNewEmailChange"
|
||||
:submitButtonProps="{
|
||||
variant: 'solid',
|
||||
onClick: submitComment,
|
||||
disabled: emailEmpty,
|
||||
}"
|
||||
:discardButtonProps="{
|
||||
onClick: () => {
|
||||
showCommunicationBox = false
|
||||
newEmail = ''
|
||||
},
|
||||
}"
|
||||
:editable="showCommunicationBox"
|
||||
v-model="doc.data"
|
||||
v-model:attachments="attachments"
|
||||
:doctype="doctype"
|
||||
placeholder="Add a reply..."
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import EmailEditor from '@/components/EmailEditor.vue'
|
||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
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')
|
||||
|
||||
const emit = defineEmits(['scroll'])
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const showCommunicationBox = ref(false)
|
||||
const newEmail = ref('')
|
||||
const newEmailEditor = ref(null)
|
||||
const sendEmailRef = ref(null)
|
||||
const attachments = ref([]);
|
||||
|
||||
watch(
|
||||
() => showCommunicationBox.value,
|
||||
@ -84,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,
|
||||
@ -109,7 +112,8 @@ async function submitComment() {
|
||||
await sendMail()
|
||||
newEmail.value = ''
|
||||
reload.value = true
|
||||
emit('scroll')
|
||||
}
|
||||
|
||||
defineExpose({ show: showCommunicationBox })
|
||||
defineExpose({ show: showCommunicationBox, editor: newEmailEditor })
|
||||
</script>
|
||||
|
||||
@ -85,6 +85,7 @@ const text = ref('')
|
||||
watchDebounced(
|
||||
() => autocomplete.value?.query,
|
||||
(val) => {
|
||||
val = val || ''
|
||||
if (text.value === val) return
|
||||
text.value = val
|
||||
options.update({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<TextEditor
|
||||
ref="textEditor"
|
||||
:editor-class="['prose-sm max-w-none', editable && 'min-h-[4rem]']"
|
||||
:editor-class="['prose-sm max-w-none', editable && 'min-h-[7rem]']"
|
||||
:content="value"
|
||||
@change="editable ? $emit('change', $event) : null"
|
||||
:starterkit-options="{ heading: { levels: [2, 3, 4, 5, 6] } }"
|
||||
@ -9,35 +9,72 @@
|
||||
:editable="editable"
|
||||
>
|
||||
<template #top>
|
||||
<div class="mb-2">
|
||||
<span class="text-base text-gray-600">To:</span>
|
||||
<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"
|
||||
class="ml-2 bg-gray-100 px-2 py-1 rounded-md text-sm text-gray-800 cursor-pointer"
|
||||
>{{ modelValue.email }}</span
|
||||
class="ml-2 cursor-pointer rounded-md bg-gray-100 px-2 py-1 text-sm text-gray-800"
|
||||
>
|
||||
{{ modelValue.email }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:editor="{ editor }">
|
||||
<EditorContent
|
||||
:class="[editable && '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="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 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>
|
||||
@ -45,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'
|
||||
|
||||
@ -62,6 +106,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
doctype: {
|
||||
type: String,
|
||||
default: 'CRM Lead',
|
||||
},
|
||||
editorProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
@ -78,6 +126,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
const modelValue = defineModel()
|
||||
const attachments = defineModel('attachments')
|
||||
|
||||
const textEditor = ref(null)
|
||||
|
||||
@ -85,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>
|
||||
16
frontend/src/components/Icons/ReplyIcon.vue
Normal file
16
frontend/src/components/Icons/ReplyIcon.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="M5.64645 3.14645C5.84171 2.95118 6.15829 2.95118 6.35355 3.14645C6.54882 3.34171 6.54882 3.65829 6.35355 3.85355L3.20711 7H10C12.4853 7 14.5 9.01472 14.5 11.5V12C14.5 12.2761 14.2761 12.5 14 12.5C13.7239 12.5 13.5 12.2761 13.5 12V11.5C13.5 9.567 11.933 8 10 8H3.20711L6.35355 11.1464C6.54882 11.3417 6.54882 11.6583 6.35355 11.8536C6.15829 12.0488 5.84171 12.0488 5.64645 11.8536L1.64645 7.85355C1.45118 7.65829 1.45118 7.34171 1.64645 7.14645L5.64645 3.14645Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -63,6 +63,8 @@ const props = defineProps({
|
||||
const show = defineModel()
|
||||
const notes = defineModel('reloadNotes')
|
||||
|
||||
const emit = defineEmits(['after'])
|
||||
|
||||
const title = ref(null)
|
||||
const editMode = ref(false)
|
||||
let _note = ref({})
|
||||
@ -81,7 +83,8 @@ async function updateNote() {
|
||||
fieldname: _note.value,
|
||||
})
|
||||
if (d.name) {
|
||||
notes.value.reload()
|
||||
notes.value?.reload()
|
||||
emit('after', d)
|
||||
}
|
||||
} else {
|
||||
let d = await call('frappe.client.insert', {
|
||||
@ -94,7 +97,8 @@ async function updateNote() {
|
||||
},
|
||||
})
|
||||
if (d.name) {
|
||||
notes.value.reload()
|
||||
notes.value?.reload()
|
||||
emit('after', d)
|
||||
}
|
||||
}
|
||||
show.value = false
|
||||
|
||||
@ -1,2 +1,26 @@
|
||||
@import './assets/Inter/inter.css';
|
||||
@import 'frappe-ui/src/style.css';
|
||||
|
||||
@layer components {
|
||||
.prose-f {
|
||||
@apply
|
||||
break-all
|
||||
max-w-none
|
||||
prose
|
||||
prose-code:break-all
|
||||
prose-code:whitespace-pre-wrap
|
||||
prose-img:border
|
||||
prose-img:rounded-lg
|
||||
prose-sm
|
||||
prose-table:table-fixed
|
||||
prose-td:border
|
||||
prose-td:border-gray-300
|
||||
prose-td:p-2
|
||||
prose-td:relative
|
||||
prose-th:bg-gray-100
|
||||
prose-th:border
|
||||
prose-th:border-gray-300
|
||||
prose-th:p-2
|
||||
prose-th:relative
|
||||
}
|
||||
}
|
||||
@ -135,7 +135,7 @@
|
||||
<NoteModal
|
||||
v-model="showNoteModal"
|
||||
:note="callLog.data?.note_doc"
|
||||
@updateNote="updateNote"
|
||||
@after="updateNote"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user