Merge pull request #41 from shariquerik/email-box

fix: Email Box
This commit is contained in:
Shariq Ansari 2023-12-23 22:04:04 +05:30 committed by GitHub
commit b36eb58ace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1804 additions and 814 deletions

View File

@ -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 = []

View File

@ -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]
}

View File

@ -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",

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -85,6 +85,7 @@ const text = ref('')
watchDebounced(
() => autocomplete.value?.query,
(val) => {
val = val || ''
if (text.value === val) return
text.value = val
options.update({

View File

@ -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 = [

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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

View File

@ -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
}
}

View File

@ -135,7 +135,7 @@
<NoteModal
v-model="showNoteModal"
:note="callLog.data?.note_doc"
@updateNote="updateNote"
@after="updateNote"
/>
</template>

2054
yarn.lock

File diff suppressed because it is too large Load Diff