Merge pull request #60 from shariquerik/comment-feat-1
feat: Comment Box
This commit is contained in:
commit
4c976ea757
184
frontend/src/components/CommentBox.vue
Normal file
184
frontend/src/components/CommentBox.vue
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<TextEditor
|
||||||
|
ref="textEditor"
|
||||||
|
: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] } }"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:editable="editable"
|
||||||
|
:mentions="users"
|
||||||
|
>
|
||||||
|
<template v-slot:editor="{ editor }">
|
||||||
|
<EditorContent
|
||||||
|
:class="[
|
||||||
|
editable && 'mx-10 max-h-[50vh] overflow-y-auto border-t py-3',
|
||||||
|
]"
|
||||||
|
:editor="editor"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:bottom>
|
||||||
|
<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 gap-2 overflow-hidden border-t px-10 py-2.5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center overflow-x-auto">
|
||||||
|
<TextEditorFixedMenu
|
||||||
|
class="-ml-1"
|
||||||
|
: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 || {}" label="Discard" />
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
v-bind="submitButtonProps || {}"
|
||||||
|
label="Submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TextEditor>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||||
|
import AttachmentItem from '@/components/AttachmentItem.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { TextEditorFixedMenu, TextEditor, FileUploader } from 'frappe-ui'
|
||||||
|
import { EditorContent } from '@tiptap/vue-3'
|
||||||
|
import { ref, computed, defineModel } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
default: 'CRM Lead',
|
||||||
|
},
|
||||||
|
editorProps: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
submitButtonProps: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
discardButtonProps: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['change'])
|
||||||
|
const modelValue = defineModel()
|
||||||
|
const attachments = defineModel('attachments')
|
||||||
|
|
||||||
|
const { users: usersList } = usersStore()
|
||||||
|
|
||||||
|
const textEditor = ref(null)
|
||||||
|
|
||||||
|
const editor = computed(() => {
|
||||||
|
return textEditor.value.editor
|
||||||
|
})
|
||||||
|
|
||||||
|
function removeAttachment(attachment) {
|
||||||
|
attachments.value = attachments.value.filter((a) => a !== attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = computed(() => {
|
||||||
|
return (
|
||||||
|
usersList.data
|
||||||
|
?.filter((user) => user.enabled)
|
||||||
|
.map((user) => ({
|
||||||
|
label: user.full_name.trimEnd(),
|
||||||
|
value: user.name,
|
||||||
|
})) || []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ editor })
|
||||||
|
|
||||||
|
const textEditorMenuButtons = [
|
||||||
|
'Paragraph',
|
||||||
|
['Heading 2', 'Heading 3', 'Heading 4', 'Heading 5', 'Heading 6'],
|
||||||
|
'Separator',
|
||||||
|
'Bold',
|
||||||
|
'Italic',
|
||||||
|
'Separator',
|
||||||
|
'Bullet List',
|
||||||
|
'Numbered List',
|
||||||
|
'Separator',
|
||||||
|
'Align Left',
|
||||||
|
'Align Center',
|
||||||
|
'Align Right',
|
||||||
|
'FontColor',
|
||||||
|
'Separator',
|
||||||
|
'Image',
|
||||||
|
'Video',
|
||||||
|
'Link',
|
||||||
|
'Blockquote',
|
||||||
|
'Code',
|
||||||
|
'Horizontal Rule',
|
||||||
|
[
|
||||||
|
'InsertTable',
|
||||||
|
'AddColumnBefore',
|
||||||
|
'AddColumnAfter',
|
||||||
|
'DeleteColumn',
|
||||||
|
'AddRowBefore',
|
||||||
|
'AddRowAfter',
|
||||||
|
'DeleteRow',
|
||||||
|
'MergeCells',
|
||||||
|
'SplitCell',
|
||||||
|
'ToggleHeaderColumn',
|
||||||
|
'ToggleHeaderRow',
|
||||||
|
'ToggleHeaderCell',
|
||||||
|
'DeleteTable',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@ -4,21 +4,26 @@
|
|||||||
<Button
|
<Button
|
||||||
ref="sendEmailRef"
|
ref="sendEmailRef"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
:class="[showEmailBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||||
label="Reply"
|
label="Reply"
|
||||||
@click="showCommunicationBox = !showCommunicationBox"
|
@click="toggleEmailBox()"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<EmailIcon class="h-4" />
|
<EmailIcon class="h-4" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
<!-- <Button variant="ghost" label="Comment">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
label="Comment"
|
||||||
|
:class="[showCommentBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||||
|
@click="toggleCommentBox()"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<CommentIcon class="h-4" />
|
<CommentIcon class="h-4" />
|
||||||
</template>
|
</template>
|
||||||
</Button> -->
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showCommunicationBox" class="flex gap-1.5">
|
<div v-if="showEmailBox" class="flex gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
label="CC"
|
label="CC"
|
||||||
@click="toggleCC()"
|
@click="toggleCC()"
|
||||||
@ -32,9 +37,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="showCommunicationBox"
|
v-show="showEmailBox"
|
||||||
@keydown.ctrl.enter.capture.stop="submitComment"
|
@keydown.ctrl.enter.capture.stop="submitEmail"
|
||||||
@keydown.meta.enter.capture.stop="submitComment"
|
@keydown.meta.enter.capture.stop="submitEmail"
|
||||||
>
|
>
|
||||||
<EmailEditor
|
<EmailEditor
|
||||||
ref="newEmailEditor"
|
ref="newEmailEditor"
|
||||||
@ -42,16 +47,16 @@
|
|||||||
@change="onNewEmailChange"
|
@change="onNewEmailChange"
|
||||||
:submitButtonProps="{
|
:submitButtonProps="{
|
||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
onClick: submitComment,
|
onClick: submitEmail,
|
||||||
disabled: emailEmpty,
|
disabled: emailEmpty,
|
||||||
}"
|
}"
|
||||||
:discardButtonProps="{
|
:discardButtonProps="{
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
showCommunicationBox = false
|
showEmailBox = false
|
||||||
newEmail = ''
|
newEmail = ''
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
:editable="showCommunicationBox"
|
:editable="showEmailBox"
|
||||||
v-model="doc.data"
|
v-model="doc.data"
|
||||||
v-model:attachments="attachments"
|
v-model:attachments="attachments"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
@ -59,10 +64,35 @@
|
|||||||
placeholder="Add a reply..."
|
placeholder="Add a reply..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="showCommentBox">
|
||||||
|
<CommentBox
|
||||||
|
ref="newCommentEditor"
|
||||||
|
:value="newComment"
|
||||||
|
@change="onNewCommentChange"
|
||||||
|
:submitButtonProps="{
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: submitComment,
|
||||||
|
disabled: commentEmpty,
|
||||||
|
}"
|
||||||
|
:discardButtonProps="{
|
||||||
|
onClick: () => {
|
||||||
|
showCommentBox = false
|
||||||
|
newComment = ''
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
:editable="showCommentBox"
|
||||||
|
v-model="doc.data"
|
||||||
|
v-model:attachments="attachments"
|
||||||
|
:doctype="doctype"
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import EmailEditor from '@/components/EmailEditor.vue'
|
import EmailEditor from '@/components/EmailEditor.vue'
|
||||||
|
import CommentBox from '@/components/CommentBox.vue'
|
||||||
|
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
@ -83,9 +113,12 @@ const emit = defineEmits(['scroll'])
|
|||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
const showCommunicationBox = ref(false)
|
const showEmailBox = ref(false)
|
||||||
|
const showCommentBox = ref(false)
|
||||||
const newEmail = useStorage('emailBoxContent', '')
|
const newEmail = useStorage('emailBoxContent', '')
|
||||||
|
const newComment = useStorage('commentBoxContent', '')
|
||||||
const newEmailEditor = ref(null)
|
const newEmailEditor = ref(null)
|
||||||
|
const newCommentEditor = ref(null)
|
||||||
const sendEmailRef = ref(null)
|
const sendEmailRef = ref(null)
|
||||||
const attachments = ref([])
|
const attachments = ref([])
|
||||||
|
|
||||||
@ -100,7 +133,7 @@ const subject = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => showCommunicationBox.value,
|
() => showEmailBox.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
newEmailEditor.value.editor.commands.focus()
|
newEmailEditor.value.editor.commands.focus()
|
||||||
@ -108,6 +141,19 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => showCommentBox.value,
|
||||||
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
newCommentEditor.value.editor.commands.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const commentEmpty = computed(() => {
|
||||||
|
return !newComment.value || newComment.value === '<p></p>'
|
||||||
|
})
|
||||||
|
|
||||||
const emailEmpty = computed(() => {
|
const emailEmpty = computed(() => {
|
||||||
return !newEmail.value || newEmail.value === '<p></p>'
|
return !newEmail.value || newEmail.value === '<p></p>'
|
||||||
})
|
})
|
||||||
@ -116,6 +162,10 @@ const onNewEmailChange = (value) => {
|
|||||||
newEmail.value = value
|
newEmail.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onNewCommentChange = (value) => {
|
||||||
|
newComment.value = value
|
||||||
|
}
|
||||||
|
|
||||||
async function sendMail() {
|
async function sendMail() {
|
||||||
let recipients = newEmailEditor.value.toEmails
|
let recipients = newEmailEditor.value.toEmails
|
||||||
let subject = newEmailEditor.value.subject
|
let subject = newEmailEditor.value.subject
|
||||||
@ -136,28 +186,63 @@ async function sendMail() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitComment() {
|
async function sendComment() {
|
||||||
|
await call("frappe.desk.form.utils.add_comment", {
|
||||||
|
reference_doctype: props.doctype,
|
||||||
|
reference_name: doc.value.data.name,
|
||||||
|
content: newComment.value,
|
||||||
|
comment_email: getUser().name,
|
||||||
|
comment_by: getUser()?.full_name || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEmail() {
|
||||||
if (emailEmpty.value) return
|
if (emailEmpty.value) return
|
||||||
showCommunicationBox.value = false
|
showEmailBox.value = false
|
||||||
await sendMail()
|
await sendMail()
|
||||||
newEmail.value = ''
|
newEmail.value = ''
|
||||||
reload.value = true
|
reload.value = true
|
||||||
emit('scroll')
|
emit('scroll')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitComment() {
|
||||||
|
if (commentEmpty.value) return
|
||||||
|
showCommentBox.value = false
|
||||||
|
await sendComment()
|
||||||
|
newComment.value = ''
|
||||||
|
reload.value = true
|
||||||
|
emit('scroll')
|
||||||
|
}
|
||||||
|
|
||||||
function toggleCC() {
|
function toggleCC() {
|
||||||
newEmailEditor.value.cc = !newEmailEditor.value.cc
|
newEmailEditor.value.cc = !newEmailEditor.value.cc
|
||||||
newEmailEditor.value.cc && nextTick(() => {
|
newEmailEditor.value.cc &&
|
||||||
newEmailEditor.value.ccInput.setFocus()
|
nextTick(() => {
|
||||||
})
|
newEmailEditor.value.ccInput.setFocus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleBCC() {
|
function toggleBCC() {
|
||||||
newEmailEditor.value.bcc = !newEmailEditor.value.bcc
|
newEmailEditor.value.bcc = !newEmailEditor.value.bcc
|
||||||
newEmailEditor.value.bcc && nextTick(() => {
|
newEmailEditor.value.bcc &&
|
||||||
newEmailEditor.value.bccInput.setFocus()
|
nextTick(() => {
|
||||||
})
|
newEmailEditor.value.bccInput.setFocus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show: showCommunicationBox, editor: newEmailEditor })
|
function toggleEmailBox() {
|
||||||
|
if (showCommentBox.value) {
|
||||||
|
showCommentBox.value = false
|
||||||
|
}
|
||||||
|
showEmailBox.value = !showEmailBox.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCommentBox() {
|
||||||
|
if (showEmailBox.value) {
|
||||||
|
showEmailBox.value = false
|
||||||
|
}
|
||||||
|
showCommentBox.value = !showCommentBox.value
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show: showEmailBox, editor: newEmailEditor })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx-10 flex items-center gap-2 border-t py-2.5"
|
class="mx-10 flex items-center gap-2 border-t py-2.5"
|
||||||
:class="[cc || bcc ? '' : 'border-b']"
|
:class="[cc || bcc ? 'border-b' : '']"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-gray-500">TO:</span>
|
<span class="text-xs text-gray-500">TO:</span>
|
||||||
<MultiselectInput
|
<MultiselectInput
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="cc"
|
v-if="cc"
|
||||||
class="mx-10 flex items-center gap-2 py-2.5"
|
class="mx-10 flex items-center gap-2 py-2.5"
|
||||||
:class="bcc ? '' : 'border-b'"
|
:class="bcc ? 'border-b' : ''"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-gray-500">CC:</span>
|
<span class="text-xs text-gray-500">CC:</span>
|
||||||
<MultiselectInput
|
<MultiselectInput
|
||||||
@ -42,7 +42,7 @@
|
|||||||
:error-message="(value) => `${value} is an invalid email address`"
|
:error-message="(value) => `${value} is an invalid email address`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bcc" class="mx-10 flex items-center gap-2 border-b py-2.5">
|
<div v-if="bcc" class="mx-10 flex items-center gap-2 py-2.5">
|
||||||
<span class="text-xs text-gray-500">BCC:</span>
|
<span class="text-xs text-gray-500">BCC:</span>
|
||||||
<MultiselectInput
|
<MultiselectInput
|
||||||
ref="bccInput"
|
ref="bccInput"
|
||||||
@ -55,7 +55,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:editor="{ editor }">
|
<template v-slot:editor="{ editor }">
|
||||||
<EditorContent
|
<EditorContent
|
||||||
:class="[editable && 'mx-10 max-h-[50vh] overflow-y-auto py-3']"
|
:class="[editable && 'mx-10 max-h-[50vh] overflow-y-auto py-3 border-t']"
|
||||||
:editor="editor"
|
:editor="editor"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -30,7 +30,6 @@ export const usersStore = defineStore('crm-users', () => {
|
|||||||
email = session.user
|
email = session.user
|
||||||
}
|
}
|
||||||
if (!usersByName[email]) {
|
if (!usersByName[email]) {
|
||||||
users.reload()
|
|
||||||
usersByName[email] = {
|
usersByName[email] = {
|
||||||
name: email,
|
name: email,
|
||||||
email: email,
|
email: email,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user