diff --git a/crm/api/contact.py b/crm/api/contact.py
index 8ef93e43..5484bb9b 100644
--- a/crm/api/contact.py
+++ b/crm/api/contact.py
@@ -132,3 +132,36 @@ def set_as_primary(contact, field, value):
contact.save()
return True
+
+
+@frappe.whitelist()
+def search_emails(txt: str):
+ doctype = "Contact"
+ meta = frappe.get_meta(doctype)
+ filters = [["Contact", "email_id", "is", "set"]]
+
+ if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}):
+ filters.append([doctype, "enabled", "=", 1])
+ if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}):
+ filters.append([doctype, "disabled", "!=", 1])
+
+ or_filters = []
+ search_fields = ["full_name", "email_id", "name"]
+ if txt:
+ for f in search_fields:
+ or_filters.append([doctype, f.strip(), "like", f"%{txt}%"])
+
+ results = frappe.get_list(
+ doctype,
+ filters=filters,
+ fields=search_fields,
+ or_filters=or_filters,
+ limit_start=0,
+ limit_page_length=20,
+ order_by='email_id, full_name, name',
+ ignore_permissions=False,
+ as_list=True,
+ strict=False,
+ )
+
+ return results
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 4f46436b..b899efda 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,6 +9,7 @@
"serve": "vite preview"
},
"dependencies": {
+ "@tiptap/extension-paragraph": "^2.4.0",
"@twilio/voice-sdk": "^2.10.2",
"@vueuse/core": "^10.3.0",
"@vueuse/integrations": "^10.3.0",
diff --git a/frontend/src/components/Activities.vue b/frontend/src/components/Activities.vue
index 3bf06991..8df55c2d 100644
--- a/frontend/src/components/Activities.vue
+++ b/frontend/src/components/Activities.vue
@@ -442,7 +442,7 @@
'outgoing_call',
].includes(activity.activity_type),
'bg-white': ['added', 'removed', 'changed'].includes(
- activity.activity_type
+ activity.activity_type,
),
}"
>
@@ -528,7 +528,10 @@
{{ activity.data.bcc }}
-
+
{
},
]
return actions.filter((action) =>
- action.condition ? action.condition() : true
+ action.condition ? action.condition() : true,
)
})
@@ -1120,12 +1123,12 @@ const activities = computed(() => {
} else if (props.title == 'Emails') {
if (!all_activities.data?.versions) return []
activities = all_activities.data.versions.filter(
- (activity) => activity.activity_type === 'communication'
+ (activity) => activity.activity_type === 'communication',
)
} else if (props.title == 'Comments') {
if (!all_activities.data?.versions) return []
activities = all_activities.data.versions.filter(
- (activity) => activity.activity_type === 'comment'
+ (activity) => activity.activity_type === 'comment',
)
} else if (props.title == 'Calls') {
if (!all_activities.data?.calls) return []
@@ -1338,12 +1341,15 @@ function reply(email, reply_all = false) {
editor.bccEmails = bcc
}
+ let repliedMessage = `${message}
`
+
editor.editor
.chain()
.clearContent()
- .insertContent(message)
+ .insertContent('.
')
+ .updateAttributes('paragraph', {class:'reply-to-content'})
+ .insertContent(repliedMessage)
.focus('all')
- .setBlockquote()
.insertContentAt(0, { type: 'paragraph' })
.focus('start')
.run()
diff --git a/frontend/src/components/CommentBox.vue b/frontend/src/components/CommentBox.vue
index e5c8c1b1..ebdf00c9 100644
--- a/frontend/src/components/CommentBox.vue
+++ b/frontend/src/components/CommentBox.vue
@@ -12,7 +12,8 @@
@@ -37,11 +38,19 @@
-
-
+
+
+ appendEmoji()"
+ >
+
+
-
-
-
')
let emailContent = editor.getHTML()
emailContent = emailContent.startsWith('
')
@@ -236,22 +225,6 @@ async function submitComment() {
emit('scroll')
}
-function toggleCC() {
- newEmailEditor.value.cc = !newEmailEditor.value.cc
- newEmailEditor.value.cc &&
- nextTick(() => {
- newEmailEditor.value.ccInput.setFocus()
- })
-}
-
-function toggleBCC() {
- newEmailEditor.value.bcc = !newEmailEditor.value.bcc
- newEmailEditor.value.bcc &&
- nextTick(() => {
- newEmailEditor.value.bccInput.setFocus()
- })
-}
-
function toggleEmailBox() {
if (showCommentBox.value) {
showCommentBox.value = false
diff --git a/frontend/src/components/Controls/MultiselectInput.vue b/frontend/src/components/Controls/MultiselectInput.vue
index ed8b4102..a5cb6928 100644
--- a/frontend/src/components/Controls/MultiselectInput.vue
+++ b/frontend/src/components/Controls/MultiselectInput.vue
@@ -8,7 +8,7 @@
:label="value"
theme="gray"
variant="subtle"
- class="rounded-full"
+ class="rounded"
@keydown.delete.capture.stop="removeLastValue"
>
@@ -133,30 +133,26 @@ watchDebounced(
query,
(val) => {
val = val || ''
- if (text.value === val) return
+ if (text.value === val && options.value?.length) return
text.value = val
reload(val)
},
- { debounce: 300, immediate: true }
+ { debounce: 300, immediate: true },
)
const filterOptions = createResource({
- url: 'frappe.desk.search.search_link',
+ url: 'crm.api.contact.search_emails',
method: 'POST',
cache: [text.value, 'Contact'],
- params: {
- txt: text.value,
- doctype: 'Contact',
- },
+ params: { txt: text.value },
transform: (data) => {
let allData = data
- .filter((c) => {
- return c.description.split(', ')[1]
- })
.map((option) => {
- let email = option.description.split(', ')[1]
+ let fullName = option[0]
+ let email = option[1]
+ let name = option[2]
return {
- label: option.label || email,
+ label: fullName || name || email,
value: email,
}
})
@@ -177,10 +173,7 @@ const options = computed(() => {
function reload(val) {
filterOptions.update({
- params: {
- txt: val,
- doctype: 'Contact',
- },
+ params: { txt: val },
})
filterOptions.reload()
}
diff --git a/frontend/src/components/EmailContent.vue b/frontend/src/components/EmailContent.vue
index f68ea6b6..310a452b 100644
--- a/frontend/src/components/EmailContent.vue
+++ b/frontend/src/components/EmailContent.vue
@@ -4,9 +4,10 @@
:srcdoc="htmlContent"
class="prose-f block h-screen max-h-[500px] w-full"
style="
+ height: 40px;
mask-image: linear-gradient(
to bottom,
- black calc(100% - 30px),
+ black calc(100% - 20px),
transparent 100%
);
"
@@ -27,6 +28,90 @@ const files = import.meta.globEager('/src/index.css', { query: '?inline' })
const css = files['/src/index.css'].default
const iframeRef = ref(null)
+const _content = ref(props.content)
+
+const parser = new DOMParser()
+const doc = parser.parseFromString(_content.value, 'text/html')
+
+const gmailReplyToContent = doc.querySelectorAll('div.gmail_quote')
+const outlookReplyToContent = doc.querySelectorAll('div#appendonsend')
+const replyToContent = doc.querySelectorAll('p.reply-to-content')
+
+if (gmailReplyToContent.length) {
+ _content.value = parseReplyToContent(doc, 'div.gmail_quote', true)
+} else if (outlookReplyToContent.length) {
+ _content.value = parseReplyToContent(doc, 'div#appendonsend')
+} else if (replyToContent.length) {
+ _content.value = parseReplyToContent(doc, 'p.reply-to-content')
+}
+
+function parseReplyToContent(doc, selector, forGmail = false) {
+ function handleAllInstances(doc) {
+ const replyToContentElements = doc.querySelectorAll(selector)
+ if (replyToContentElements.length === 0) return
+ const replyToContentElement = replyToContentElements[0]
+ replaceReplyToContent(replyToContentElement, forGmail)
+ handleAllInstances(doc)
+ }
+
+ handleAllInstances(doc)
+
+ return doc.body.innerHTML
+}
+
+function replaceReplyToContent(replyToContentElement, forGmail) {
+ if (!replyToContentElement) return
+ let randomId = Math.random().toString(36).substring(2, 7)
+ const wrapper = doc.createElement('div')
+ wrapper.classList.add('replied-content')
+
+ const collapseLabel = doc.createElement('label')
+ collapseLabel.classList.add('collapse')
+ collapseLabel.setAttribute('for', randomId)
+ collapseLabel.innerHTML = '...'
+ wrapper.appendChild(collapseLabel)
+
+ const collapseInput = doc.createElement('input')
+ collapseInput.setAttribute('id', randomId)
+ collapseInput.setAttribute('class', 'replyCollapser')
+ collapseInput.setAttribute('type', 'checkbox')
+ wrapper.appendChild(collapseInput)
+
+ if (forGmail) {
+ const prevSibling = replyToContentElement.previousElementSibling
+ if (prevSibling && prevSibling.tagName === 'BR') {
+ prevSibling.remove()
+ }
+ let cloned = replyToContentElement.cloneNode(true)
+ cloned.classList.remove('gmail_quote')
+ wrapper.appendChild(cloned)
+ } else {
+ const allSiblings = Array.from(replyToContentElement.parentElement.children)
+ const replyToContentIndex = allSiblings.indexOf(replyToContentElement)
+ const followingSiblings = allSiblings.slice(replyToContentIndex + 1)
+
+ if (followingSiblings.length === 0) return
+
+ let clonedFollowingSiblings = followingSiblings.map((sibling) =>
+ sibling.cloneNode(true),
+ )
+
+ const div = doc.createElement('div')
+ div.append(...clonedFollowingSiblings)
+
+ wrapper.append(div)
+
+ // Remove all siblings after the reply-to-content element
+ for (let i = replyToContentIndex + 1; i < allSiblings.length; i++) {
+ replyToContentElement.parentElement.removeChild(allSiblings[i])
+ }
+ }
+
+ replyToContentElement.parentElement.replaceChild(
+ wrapper,
+ replyToContentElement,
+ )
+}
const htmlContent = `
@@ -35,6 +120,35 @@ const htmlContent = `
- ${props.content}
+ ${_content.value}