1
0
forked from test/crm

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 import frappe
from frappe import _ from frappe import _
from frappe.utils.caching import redis_cache
from frappe.desk.form.load import get_docinfo from frappe.desk.form.load import get_docinfo
@frappe.whitelist() @frappe.whitelist()
@ -98,6 +99,7 @@ def get_deal_activities(name):
"recipients": communication.recipients, "recipients": communication.recipients,
"cc": communication.cc, "cc": communication.cc,
"bcc": communication.bcc, "bcc": communication.bcc,
"attachments": get_attachments(communication.name),
"read_by_recipient": communication.read_by_recipient, "read_by_recipient": communication.read_by_recipient,
}, },
"is_lead": False, "is_lead": False,
@ -185,6 +187,7 @@ def get_lead_activities(name):
"recipients": communication.recipients, "recipients": communication.recipients,
"cc": communication.cc, "cc": communication.cc,
"bcc": communication.bcc, "bcc": communication.bcc,
"attachments": get_attachments(communication.name),
"read_by_recipient": communication.read_by_recipient, "read_by_recipient": communication.read_by_recipient,
}, },
"is_lead": True, "is_lead": True,
@ -196,6 +199,14 @@ def get_lead_activities(name):
return activities 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): def handle_multiple_versions(versions):
activities = [] activities = []
grouped_versions = [] grouped_versions = []

View File

@ -96,6 +96,7 @@ def get_call_log(name):
if doc.note: if doc.note:
note = frappe.db.get_values("CRM Note", doc.note, ["title", "content"])[0] note = frappe.db.get_values("CRM Note", doc.note, ["title", "content"])[0]
_doc.note_doc = { _doc.note_doc = {
"name": doc.note,
"title": note[0], "title": note[0],
"content": note[1] "content": note[1]
} }

View File

@ -14,6 +14,7 @@
"@vueuse/integrations": "^10.3.0", "@vueuse/integrations": "^10.3.0",
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
"frappe-ui": "^0.1.17", "frappe-ui": "^0.1.17",
"mime": "^4.0.1",
"pinia": "^2.0.33", "pinia": "^2.0.33",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",

View File

@ -26,8 +26,11 @@
<span>New Task</span> <span>New Task</span>
</Button> </Button>
</div> </div>
<div v-if="activities?.length" class="flex-1 overflow-y-auto"> <div v-if="activities?.length" class="activities flex-1 overflow-y-auto">
<div v-if="title == 'Notes'" class="grid grid-cols-3 gap-4 px-10 pb-5"> <div
v-if="title == 'Notes'"
class="activity grid grid-cols-3 gap-4 px-10 pb-5"
>
<div <div
v-for="note in activities" 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" 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>
</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 v-for="(task, i) in activities">
<div <div
class="flex cursor-pointer gap-6 rounded p-2.5 duration-300 ease-in-out hover:bg-gray-50" 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> </div>
<div v-else-if="title == 'Calls'"> <div v-else-if="title == 'Calls'" class="activity">
<div v-for="(call, i) in activities"> <div v-for="(call, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10"> <div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
<div <div
@ -222,11 +225,7 @@
v-if="call.show_recording" v-if="call.show_recording"
class="flex items-center justify-between rounded border" class="flex items-center justify-between rounded border"
> >
<audio <audio class="audio-control" controls :src="call.recording_url" />
class="audio-control"
controls
:src="call.recording_url"
/>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
@ -266,7 +265,7 @@
</div> </div>
</div> </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="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
<div <div
class="relative flex justify-center before:absolute before:left-[50%] before:top-0 before:-z-10 before:border-l before:border-gray-200" 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) }} {{ timeAgo(activity.creation) }}
</Tooltip> </Tooltip>
</div> </div>
<div> <div class="flex gap-0.5">
<Button <Button
variant="ghost" variant="ghost"
icon="more-horizontal" class="text-gray-700"
class="text-gray-600" @click="reply(activity.data.content)"
/> >
<ReplyIcon class="h-4 w-4" />
</Button>
</div> </div>
</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> </div>
<div <div
@ -587,6 +596,8 @@
v-if="['Emails', 'Activity'].includes(title)" v-if="['Emails', 'Activity'].includes(title)"
v-model="doc" v-model="doc"
v-model:reload="reload_email" v-model:reload="reload_email"
:doctype="doctype"
@scroll="scroll"
/> />
<NoteModal <NoteModal
v-model="showNoteModal" v-model="showNoteModal"
@ -620,6 +631,8 @@ import DotIcon from '@/components/Icons/DotIcon.vue'
import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue' import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
import InboundCallIcon from '@/components/Icons/InboundCallIcon.vue' import InboundCallIcon from '@/components/Icons/InboundCallIcon.vue'
import OutboundCallIcon from '@/components/Icons/OutboundCallIcon.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 CommunicationArea from '@/components/CommunicationArea.vue'
import NoteModal from '@/components/Modals/NoteModal.vue' import NoteModal from '@/components/Modals/NoteModal.vue'
import TaskModal from '@/components/Modals/TaskModal.vue' import TaskModal from '@/components/Modals/TaskModal.vue'
@ -644,7 +657,8 @@ import {
createListResource, createListResource,
call, call,
} from 'frappe-ui' } 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 { getUser } = usersStore()
const { getContact } = contactsStore() const { getContact } = contactsStore()
@ -761,7 +775,7 @@ function all_activities() {
if (!versions.data) return [] if (!versions.data) return []
if (!calls.data) return versions.data if (!calls.data) return versions.data
return [...versions.data, ...calls.data].sort( 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() activities = all_activities()
} else if (props.title == 'Emails') { } else if (props.title == 'Emails') {
if (!versions.data) return [] if (!versions.data) return []
activities = versions.data.filter( activities = versions.data
(activity) => activity.activity_type === 'communication' .filter((activity) => activity.activity_type === 'communication')
) .sort((a, b) => new Date(a.creation) - new Date(b.creation))
} else if (props.title == 'Calls') { } 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') { } 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') { } 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) => { activities.forEach((activity) => {
activity.icon = timelineIcon(activity.activity_type, activity.is_lead) activity.icon = timelineIcon(activity.activity_type, activity.is_lead)
@ -876,6 +896,7 @@ function timelineIcon(activity_type, is_lead) {
// Notes // Notes
const showNoteModal = ref(false) const showNoteModal = ref(false)
const note = ref({}) const note = ref({})
const emailBox = ref(null)
function showNote(n) { function showNote(n) {
note.value = 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]) => { watch([reload, reload_email], ([reload_value, reload_email_value]) => {
if (reload_value || reload_email_value) { if (reload_value || reload_email_value) {
versions.reload() versions.reload()
@ -935,6 +971,21 @@ watch([reload, reload_email], ([reload_value, reload_email_value]) => {
reload_email.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> </script>
<style scoped> <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> <template>
<div class="flex gap-3 px-10 pb-6 pt-2"> <div class="flex gap-1.5 border-t px-10 py-2.5">
<UserAvatar
:user="getUser().name"
size="xl"
:class="showCommunicationBox ? 'mt-3' : ''"
/>
<Button <Button
ref="sendEmailRef" ref="sendEmailRef"
variant="outline" variant="ghost"
size="md" :class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
class="inline-flex h-8.5 w-full justify-between" label="Reply"
@click="showCommunicationBox = true" @click="showCommunicationBox = !showCommunicationBox"
v-show="!showCommunicationBox"
> >
<div class="text-base text-gray-600">Add a reply...</div> <template #prefix>
<template #suffix> <EmailIcon class="h-4" />
<div class="flex gap-3">
<!-- <FeatherIcon name="paperclip" class="h-4" /> -->
</div>
</template> </template>
</Button> </Button>
<div <!-- <Button variant="ghost" label="Comment">
v-show="showCommunicationBox" <template #prefix>
class="w-full rounded-lg border bg-white p-4 focus-within:border-gray-400" <CommentIcon class="h-4" />
@keydown.ctrl.enter.capture.stop="submitComment" </template>
@keydown.meta.enter.capture.stop="submitComment" </Button> -->
> </div>
<EmailEditor <div
ref="newEmailEditor" v-show="showCommunicationBox"
:value="newEmail" @keydown.ctrl.enter.capture.stop="submitComment"
@change="onNewEmailChange" @keydown.meta.enter.capture.stop="submitComment"
:submitButtonProps="{ >
variant: 'solid', <EmailEditor
onClick: submitComment, ref="newEmailEditor"
disabled: emailEmpty, :value="newEmail"
}" @change="onNewEmailChange"
:discardButtonProps="{ :submitButtonProps="{
onClick: () => { variant: 'solid',
showCommunicationBox = false onClick: submitComment,
newEmail = '' disabled: emailEmpty,
}, }"
}" :discardButtonProps="{
:editable="showCommunicationBox" onClick: () => {
v-model="doc.data" showCommunicationBox = false
placeholder="Add a reply..." newEmail = ''
/> },
</div> }"
:editable="showCommunicationBox"
v-model="doc.data"
v-model:attachments="attachments"
:doctype="doctype"
placeholder="Add a reply..."
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import UserAvatar from '@/components/UserAvatar.vue'
import EmailEditor from '@/components/EmailEditor.vue' import EmailEditor from '@/components/EmailEditor.vue'
import EmailIcon from '@/components/Icons/EmailIcon.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { call } from 'frappe-ui' import { call } from 'frappe-ui'
import { ref, watch, computed, defineModel } from 'vue' import { ref, watch, computed, defineModel } from 'vue'
const props = defineProps({
doctype: {
type: String,
default: 'CRM Lead',
},
})
const doc = defineModel() const doc = defineModel()
const reload = defineModel('reload') const reload = defineModel('reload')
const emit = defineEmits(['scroll'])
const { getUser } = usersStore() const { getUser } = usersStore()
const showCommunicationBox = ref(false) const showCommunicationBox = ref(false)
const newEmail = ref('') const newEmail = ref('')
const newEmailEditor = ref(null) const newEmailEditor = ref(null)
const sendEmailRef = ref(null) const sendEmailRef = ref(null)
const attachments = ref([]);
watch( watch(
() => showCommunicationBox.value, () => showCommunicationBox.value,
@ -84,18 +91,14 @@ const onNewEmailChange = (value) => {
} }
async function sendMail() { async function sendMail() {
let doctype = 'CRM Lead'
if (doc.value.data.lead) {
doctype = 'CRM Deal'
}
await call('frappe.core.doctype.communication.email.make', { await call('frappe.core.doctype.communication.email.make', {
recipients: doc.value.data.email, recipients: doc.value.data.email,
attachments: attachments.value.map((x) => x.name),
cc: '', cc: '',
bcc: '', bcc: '',
subject: 'Email from Agent', subject: 'Email from Agent',
content: newEmail.value, content: newEmail.value,
doctype: doctype, doctype: props.doctype,
name: doc.value.data.name, name: doc.value.data.name,
send_email: 1, send_email: 1,
sender: getUser().name, sender: getUser().name,
@ -109,7 +112,8 @@ async function submitComment() {
await sendMail() await sendMail()
newEmail.value = '' newEmail.value = ''
reload.value = true reload.value = true
emit('scroll')
} }
defineExpose({ show: showCommunicationBox }) defineExpose({ show: showCommunicationBox, editor: newEmailEditor })
</script> </script>

View File

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

View File

@ -1,7 +1,7 @@
<template> <template>
<TextEditor <TextEditor
ref="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" :content="value"
@change="editable ? $emit('change', $event) : null" @change="editable ? $emit('change', $event) : null"
:starterkit-options="{ heading: { levels: [2, 3, 4, 5, 6] } }" :starterkit-options="{ heading: { levels: [2, 3, 4, 5, 6] } }"
@ -9,35 +9,72 @@
:editable="editable" :editable="editable"
> >
<template #top> <template #top>
<div class="mb-2"> <div class="mx-10 border-b border-t py-2.5">
<span class="text-base text-gray-600">To:</span> <span class="text-xs text-gray-500">TO:</span>
<span <span
v-if="modelValue.email" v-if="modelValue.email"
class="ml-2 bg-gray-100 px-2 py-1 rounded-md text-sm text-gray-800 cursor-pointer" class="ml-2 cursor-pointer rounded-md bg-gray-100 px-2 py-1 text-sm text-gray-800"
>{{ modelValue.email }}</span
> >
{{ modelValue.email }}
</span>
</div> </div>
</template> </template>
<template v-slot:editor="{ editor }"> <template v-slot:editor="{ editor }">
<EditorContent <EditorContent
:class="[editable && 'max-h-[50vh] overflow-y-auto']" :class="[editable && 'mx-10 max-h-[50vh] overflow-y-auto py-3']"
:editor="editor" :editor="editor"
/> />
</template> </template>
<template v-slot:bottom> <template v-slot:bottom>
<div <div v-if="editable" class="flex flex-col gap-2">
v-if="editable" <div class="flex flex-wrap gap-2 px-10">
class="mt-2 flex flex-col justify-between sm:flex-row sm:items-center" <AttachmentItem
> v-for="a in attachments"
<TextEditorFixedMenu :key="a.file_url"
class="-ml-1 overflow-x-auto" :label="a.file_name"
:buttons="textEditorMenuButtons" >
/> <template #suffix>
<div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0"> <FeatherIcon
<Button v-bind="discardButtonProps || {}"> Discard </Button> class="h-3.5"
<Button variant="solid" v-bind="submitButtonProps || {}"> name="x"
Submit @click.stop="removeAttachment(a)"
</Button> />
</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>
</div> </div>
</template> </template>
@ -45,7 +82,14 @@
</template> </template>
<script setup> <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 { EditorContent } from '@tiptap/vue-3'
import { ref, computed, defineModel } from 'vue' import { ref, computed, defineModel } from 'vue'
@ -62,6 +106,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
doctype: {
type: String,
default: 'CRM Lead',
},
editorProps: { editorProps: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@ -78,6 +126,7 @@ const props = defineProps({
const emit = defineEmits(['change']) const emit = defineEmits(['change'])
const modelValue = defineModel() const modelValue = defineModel()
const attachments = defineModel('attachments')
const textEditor = ref(null) const textEditor = ref(null)
@ -85,6 +134,10 @@ const editor = computed(() => {
return textEditor.value.editor return textEditor.value.editor
}) })
function removeAttachment(attachment) {
attachments.value = attachments.value.filter((a) => a !== attachment)
}
defineExpose({ editor }) defineExpose({ editor })
const textEditorMenuButtons = [ 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 show = defineModel()
const notes = defineModel('reloadNotes') const notes = defineModel('reloadNotes')
const emit = defineEmits(['after'])
const title = ref(null) const title = ref(null)
const editMode = ref(false) const editMode = ref(false)
let _note = ref({}) let _note = ref({})
@ -81,7 +83,8 @@ async function updateNote() {
fieldname: _note.value, fieldname: _note.value,
}) })
if (d.name) { if (d.name) {
notes.value.reload() notes.value?.reload()
emit('after', d)
} }
} else { } else {
let d = await call('frappe.client.insert', { let d = await call('frappe.client.insert', {
@ -94,7 +97,8 @@ async function updateNote() {
}, },
}) })
if (d.name) { if (d.name) {
notes.value.reload() notes.value?.reload()
emit('after', d)
} }
} }
show.value = false show.value = false

View File

@ -1,2 +1,26 @@
@import './assets/Inter/inter.css'; @import './assets/Inter/inter.css';
@import 'frappe-ui/src/style.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 <NoteModal
v-model="showNoteModal" v-model="showNoteModal"
:note="callLog.data?.note_doc" :note="callLog.data?.note_doc"
@updateNote="updateNote" @after="updateNote"
/> />
</template> </template>

2054
yarn.lock

File diff suppressed because it is too large Load Diff