feat: Comment Box
This commit is contained in:
parent
669f26cdf2
commit
58aa8e5a13
169
frontend/src/components/CommentBox.vue
Normal file
169
frontend/src/components/CommentBox.vue
Normal file
@ -0,0 +1,169 @@
|
||||
<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"
|
||||
>
|
||||
<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 { 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 textEditor = ref(null)
|
||||
|
||||
const editor = computed(() => {
|
||||
return textEditor.value.editor
|
||||
})
|
||||
|
||||
function removeAttachment(attachment) {
|
||||
attachments.value = attachments.value.filter((a) => a !== attachment)
|
||||
}
|
||||
|
||||
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
|
||||
ref="sendEmailRef"
|
||||
variant="ghost"
|
||||
:class="[showCommunicationBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||
:class="[showEmailBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||
label="Reply"
|
||||
@click="showCommunicationBox = !showCommunicationBox"
|
||||
@click="toggleEmailBox()"
|
||||
>
|
||||
<template #prefix>
|
||||
<EmailIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<!-- <Button variant="ghost" label="Comment">
|
||||
<Button
|
||||
variant="ghost"
|
||||
label="Comment"
|
||||
:class="[showCommentBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
|
||||
@click="toggleCommentBox()"
|
||||
>
|
||||
<template #prefix>
|
||||
<CommentIcon class="h-4" />
|
||||
</template>
|
||||
</Button> -->
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="showCommunicationBox" class="flex gap-1.5">
|
||||
<div v-if="showEmailBox" class="flex gap-1.5">
|
||||
<Button
|
||||
label="CC"
|
||||
@click="toggleCC()"
|
||||
@ -32,9 +37,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="showCommunicationBox"
|
||||
@keydown.ctrl.enter.capture.stop="submitComment"
|
||||
@keydown.meta.enter.capture.stop="submitComment"
|
||||
v-show="showEmailBox"
|
||||
@keydown.ctrl.enter.capture.stop="submitEmail"
|
||||
@keydown.meta.enter.capture.stop="submitEmail"
|
||||
>
|
||||
<EmailEditor
|
||||
ref="newEmailEditor"
|
||||
@ -42,16 +47,16 @@
|
||||
@change="onNewEmailChange"
|
||||
:submitButtonProps="{
|
||||
variant: 'solid',
|
||||
onClick: submitComment,
|
||||
onClick: submitEmail,
|
||||
disabled: emailEmpty,
|
||||
}"
|
||||
:discardButtonProps="{
|
||||
onClick: () => {
|
||||
showCommunicationBox = false
|
||||
showEmailBox = false
|
||||
newEmail = ''
|
||||
},
|
||||
}"
|
||||
:editable="showCommunicationBox"
|
||||
:editable="showEmailBox"
|
||||
v-model="doc.data"
|
||||
v-model:attachments="attachments"
|
||||
:doctype="doctype"
|
||||
@ -59,10 +64,35 @@
|
||||
placeholder="Add a reply..."
|
||||
/>
|
||||
</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>
|
||||
|
||||
<script setup>
|
||||
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 { usersStore } from '@/stores/users'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
@ -83,9 +113,12 @@ const emit = defineEmits(['scroll'])
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const showCommunicationBox = ref(false)
|
||||
const showEmailBox = ref(false)
|
||||
const showCommentBox = ref(false)
|
||||
const newEmail = useStorage('emailBoxContent', '')
|
||||
const newComment = useStorage('commentBoxContent', '')
|
||||
const newEmailEditor = ref(null)
|
||||
const newCommentEditor = ref(null)
|
||||
const sendEmailRef = ref(null)
|
||||
const attachments = ref([])
|
||||
|
||||
@ -100,7 +133,7 @@ const subject = computed(() => {
|
||||
})
|
||||
|
||||
watch(
|
||||
() => showCommunicationBox.value,
|
||||
() => showEmailBox.value,
|
||||
(value) => {
|
||||
if (value) {
|
||||
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(() => {
|
||||
return !newEmail.value || newEmail.value === '<p></p>'
|
||||
})
|
||||
@ -116,6 +162,10 @@ const onNewEmailChange = (value) => {
|
||||
newEmail.value = value
|
||||
}
|
||||
|
||||
const onNewCommentChange = (value) => {
|
||||
newComment.value = value
|
||||
}
|
||||
|
||||
async function sendMail() {
|
||||
let recipients = newEmailEditor.value.toEmails
|
||||
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
|
||||
showCommunicationBox.value = false
|
||||
showEmailBox.value = false
|
||||
await sendMail()
|
||||
newEmail.value = ''
|
||||
reload.value = true
|
||||
emit('scroll')
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
if (commentEmpty.value) return
|
||||
showCommentBox.value = false
|
||||
await sendComment()
|
||||
newComment.value = ''
|
||||
reload.value = true
|
||||
emit('scroll')
|
||||
}
|
||||
|
||||
function toggleCC() {
|
||||
newEmailEditor.value.cc = !newEmailEditor.value.cc
|
||||
newEmailEditor.value.cc && nextTick(() => {
|
||||
newEmailEditor.value.ccInput.setFocus()
|
||||
})
|
||||
newEmailEditor.value.cc &&
|
||||
nextTick(() => {
|
||||
newEmailEditor.value.ccInput.setFocus()
|
||||
})
|
||||
}
|
||||
|
||||
function toggleBCC() {
|
||||
newEmailEditor.value.bcc = !newEmailEditor.value.bcc
|
||||
newEmailEditor.value.bcc && nextTick(() => {
|
||||
newEmailEditor.value.bccInput.setFocus()
|
||||
})
|
||||
newEmailEditor.value.bcc &&
|
||||
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>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
<MultiselectInput
|
||||
@ -31,7 +31,7 @@
|
||||
<div
|
||||
v-if="cc"
|
||||
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>
|
||||
<MultiselectInput
|
||||
@ -42,7 +42,7 @@
|
||||
:error-message="(value) => `${value} is an invalid email address`"
|
||||
/>
|
||||
</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>
|
||||
<MultiselectInput
|
||||
ref="bccInput"
|
||||
@ -55,7 +55,7 @@
|
||||
</template>
|
||||
<template v-slot:editor="{ editor }">
|
||||
<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"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user