mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
feat: Use new compose conversation in conversation sidebar (#11085)
# Pull Request Template ## Description This PR includes the implementation of the new Compose Conversation form in the conversation sidebar, replacing the old one. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/4312e20a63714eb892d7b5cd0dcda893?sid=9bd5254e-2b1f-462c-b2c1-a3048a111683 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
This commit is contained in:
@@ -27,8 +27,14 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
isModal: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -61,6 +67,8 @@ const directUploadsEnabled = computed(
|
|||||||
const activeContact = computed(() => contactById.value(props.contactId));
|
const activeContact = computed(() => contactById.value(props.contactId));
|
||||||
|
|
||||||
const composePopoverClass = computed(() => {
|
const composePopoverClass = computed(() => {
|
||||||
|
if (props.isModal) return '';
|
||||||
|
|
||||||
return props.alignPosition === 'right'
|
return props.alignPosition === 'right'
|
||||||
? 'absolute ltr:left-0 ltr:right-[unset] rtl:right-0 rtl:left-[unset]'
|
? 'absolute ltr:left-0 ltr:right-[unset] rtl:right-0 rtl:left-[unset]'
|
||||||
: 'absolute rtl:left-0 rtl:right-[unset] ltr:right-0 ltr:left-[unset]';
|
: 'absolute rtl:left-0 rtl:right-[unset] ltr:right-0 ltr:left-[unset]';
|
||||||
@@ -131,9 +139,14 @@ const clearSelectedContact = () => {
|
|||||||
|
|
||||||
const closeCompose = () => {
|
const closeCompose = () => {
|
||||||
showComposeNewConversation.value = false;
|
showComposeNewConversation.value = false;
|
||||||
selectedContact.value = null;
|
if (!props.contactId) {
|
||||||
|
// If contactId is passed as prop
|
||||||
|
// Then don't allow to remove the selected contact
|
||||||
|
selectedContact.value = null;
|
||||||
|
}
|
||||||
targetInbox.value = null;
|
targetInbox.value = null;
|
||||||
resetContacts();
|
resetContacts();
|
||||||
|
emit('close');
|
||||||
};
|
};
|
||||||
|
|
||||||
const createConversation = async ({ payload, isFromWhatsApp }) => {
|
const createConversation = async ({ payload, isFromWhatsApp }) => {
|
||||||
@@ -182,7 +195,15 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
|
if (!showComposeNewConversation.value) return;
|
||||||
|
|
||||||
showComposeNewConversation.value = false;
|
showComposeNewConversation.value = false;
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModalBackdropClick = () => {
|
||||||
|
if (!props.isModal) return;
|
||||||
|
handleClickOutside();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => resetContacts());
|
onMounted(() => resetContacts());
|
||||||
@@ -205,7 +226,7 @@ useKeyboardEvents(keyboardEvents);
|
|||||||
v-on-click-outside="[
|
v-on-click-outside="[
|
||||||
handleClickOutside,
|
handleClickOutside,
|
||||||
// Fixed and edge case https://github.com/chatwoot/chatwoot/issues/10785
|
// Fixed and edge case https://github.com/chatwoot/chatwoot/issues/10785
|
||||||
// This will prevent closing the compose conversation modal when the editor Create link popup is open.
|
// This will prevent closing the compose conversation modal when the editor Create link popup is open
|
||||||
{ ignore: ['div.ProseMirror-prompt'] },
|
{ ignore: ['div.ProseMirror-prompt'] },
|
||||||
]"
|
]"
|
||||||
class="relative"
|
class="relative"
|
||||||
@@ -218,29 +239,37 @@ useKeyboardEvents(keyboardEvents);
|
|||||||
:is-open="showComposeNewConversation"
|
:is-open="showComposeNewConversation"
|
||||||
:toggle="toggle"
|
:toggle="toggle"
|
||||||
/>
|
/>
|
||||||
<ComposeNewConversationForm
|
<div
|
||||||
v-if="showComposeNewConversation"
|
v-if="showComposeNewConversation"
|
||||||
:contacts="contacts"
|
:class="{
|
||||||
:contact-id="contactId"
|
'fixed z-50 bg-n-alpha-black1 backdrop-blur-[4px] flex items-start pt-[clamp(3rem,15vh,12rem)] justify-center inset-0':
|
||||||
:is-loading="isSearching"
|
isModal,
|
||||||
:current-user="currentUser"
|
}"
|
||||||
:selected-contact="selectedContact"
|
@click.self="onModalBackdropClick"
|
||||||
:target-inbox="targetInbox"
|
>
|
||||||
:is-creating-contact="isCreatingContact"
|
<ComposeNewConversationForm
|
||||||
:is-fetching-inboxes="isFetchingInboxes"
|
:class="[{ 'mt-2': !isModal }, composePopoverClass]"
|
||||||
:is-direct-uploads-enabled="directUploadsEnabled"
|
:contacts="contacts"
|
||||||
:contact-conversations-ui-flags="uiFlags"
|
:contact-id="contactId"
|
||||||
:contacts-ui-flags="contactsUiFlags"
|
:is-loading="isSearching"
|
||||||
:class="composePopoverClass"
|
:current-user="currentUser"
|
||||||
:message-signature="messageSignature"
|
:selected-contact="selectedContact"
|
||||||
:send-with-signature="sendWithSignature"
|
:target-inbox="targetInbox"
|
||||||
@search-contacts="onContactSearch"
|
:is-creating-contact="isCreatingContact"
|
||||||
@reset-contact-search="resetContacts"
|
:is-fetching-inboxes="isFetchingInboxes"
|
||||||
@update-selected-contact="handleSelectedContact"
|
:is-direct-uploads-enabled="directUploadsEnabled"
|
||||||
@update-target-inbox="handleTargetInbox"
|
:contact-conversations-ui-flags="uiFlags"
|
||||||
@clear-selected-contact="clearSelectedContact"
|
:contacts-ui-flags="contactsUiFlags"
|
||||||
@create-conversation="createConversation"
|
:message-signature="messageSignature"
|
||||||
@discard="closeCompose"
|
:send-with-signature="sendWithSignature"
|
||||||
/>
|
@search-contacts="onContactSearch"
|
||||||
|
@reset-contact-search="resetContacts"
|
||||||
|
@update-selected-contact="handleSelectedContact"
|
||||||
|
@update-target-inbox="handleTargetInbox"
|
||||||
|
@clear-selected-contact="clearSelectedContact"
|
||||||
|
@create-conversation="createConversation"
|
||||||
|
@discard="closeCompose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -232,4 +232,20 @@ useKeyboardEvents(keyboardEvents);
|
|||||||
.emoji-dialog::before {
|
.emoji-dialog::before {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The <label> tag inside the file-upload component overlaps the button due to its position.
|
||||||
|
// This causes the button's hover state to not work, as it's positioned below the label (z-index).
|
||||||
|
// Increasing the button's z-index would break the file upload functionality.
|
||||||
|
// This style ensures the label remains clickable while preserving the button's hover effect.
|
||||||
|
:deep() {
|
||||||
|
.file-uploads.file-uploads-html5 {
|
||||||
|
label {
|
||||||
|
@apply hover:cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover button {
|
||||||
|
@apply dark:bg-n-solid-2 bg-n-alpha-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-[42rem] mt-2 divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
|
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
|
||||||
>
|
>
|
||||||
<ContactSelector
|
<ContactSelector
|
||||||
:contacts="contacts"
|
:contacts="contacts"
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ const handleInput = value => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="selectedContact"
|
v-else-if="selectedContact"
|
||||||
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 px-3 min-h-7 min-w-0"
|
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 min-h-7 min-w-0"
|
||||||
|
:class="!contactId ? 'ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1' : 'px-3'"
|
||||||
>
|
>
|
||||||
<span class="text-sm truncate text-n-slate-12">
|
<span class="text-sm truncate text-n-slate-12">
|
||||||
{{
|
{{
|
||||||
@@ -123,6 +124,7 @@ const handleInput = value => {
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="!contactId"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="i-lucide-x"
|
icon="i-lucide-x"
|
||||||
color="slate"
|
color="slate"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const targetInboxLabel = computed(() => {
|
|||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-if="targetInbox"
|
v-if="targetInbox"
|
||||||
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 truncate px-3 h-7 min-w-0"
|
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 truncate ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 h-7 min-w-0"
|
||||||
>
|
>
|
||||||
<span class="text-sm truncate text-n-slate-12">
|
<span class="text-sm truncate text-n-slate-12">
|
||||||
{{ targetInboxLabel }}
|
{{ targetInboxLabel }}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const handleSendMessage = template => {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showTemplatesMenu"
|
v-if="showTemplatesMenu"
|
||||||
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto left-0 flex flex-col gap-2 p-4 items-center w-[350px] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto left-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||||
>
|
>
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
|
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="absolute top-full mt-1.5 max-h-[500px] overflow-y-auto left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[460px] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||||
>
|
>
|
||||||
<span class="text-sm text-n-slate-12">
|
<span class="text-sm text-n-slate-12">
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {
|
|
||||||
getInboxClassByType,
|
|
||||||
getReadableInboxByType,
|
|
||||||
} from 'dashboard/helper/inbox';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {},
|
|
||||||
props: {
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
inboxIdentifier: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
channelType: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
computedInboxIcon() {
|
|
||||||
if (!this.channelType) return 'chat';
|
|
||||||
const classByType = getInboxClassByType(
|
|
||||||
this.channelType,
|
|
||||||
this.inboxIdentifier
|
|
||||||
);
|
|
||||||
return classByType;
|
|
||||||
},
|
|
||||||
computedInboxType() {
|
|
||||||
if (!this.channelType) return 'chat';
|
|
||||||
const classByType = getReadableInboxByType(
|
|
||||||
this.channelType,
|
|
||||||
this.inboxIdentifier
|
|
||||||
);
|
|
||||||
return classByType;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center h-[2.375rem] min-w-0 py-0 px-1">
|
|
||||||
<span
|
|
||||||
class="inline-flex rounded mr-1 rtl:ml-1 rtl:mr-0 bg-slate-25 dark:bg-slate-700 p-0.5 items-center flex-shrink-0 justify-center w-6 h-6"
|
|
||||||
>
|
|
||||||
<fluent-icon
|
|
||||||
:icon="computedInboxIcon"
|
|
||||||
size="14"
|
|
||||||
class="text-slate-800 dark:text-slate-200"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-col w-full min-w-0 ml-1 mr-1">
|
|
||||||
<h5 class="option__title">
|
|
||||||
{{ name }}
|
|
||||||
</h5>
|
|
||||||
<p
|
|
||||||
class="option__body overflow-hidden whitespace-nowrap text-ellipsis"
|
|
||||||
:title="inboxIdentifier"
|
|
||||||
>
|
|
||||||
{{ inboxIdentifier || computedInboxType }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.option__body {
|
|
||||||
@apply inline-block text-slate-600 dark:text-slate-200 leading-[1.3] min-w-0 m-0;
|
|
||||||
}
|
|
||||||
.option__title {
|
|
||||||
@apply leading-[1.1] text-xs m-0 text-slate-800 dark:text-slate-100;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,8 +7,8 @@ import ContactInfoRow from './ContactInfoRow.vue';
|
|||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import SocialIcons from './SocialIcons.vue';
|
import SocialIcons from './SocialIcons.vue';
|
||||||
import EditContact from './EditContact.vue';
|
import EditContact from './EditContact.vue';
|
||||||
import NewConversation from './NewConversation.vue';
|
|
||||||
import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue';
|
import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue';
|
||||||
|
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ export default {
|
|||||||
ContactInfoRow,
|
ContactInfoRow,
|
||||||
EditContact,
|
EditContact,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
ComposeConversation,
|
||||||
SocialIcons,
|
SocialIcons,
|
||||||
NewConversation,
|
|
||||||
ContactMergeModal,
|
ContactMergeModal,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -49,7 +49,6 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showConversationModal: false,
|
|
||||||
showMergeModal: false,
|
showMergeModal: false,
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
};
|
};
|
||||||
@@ -92,17 +91,29 @@ export default {
|
|||||||
return ` ${this.contact.name}?`;
|
return ` ${this.contact.name}?`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'contact.id': {
|
||||||
|
handler(id) {
|
||||||
|
this.$store.dispatch('contacts/fetchContactableInbox', id);
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dynamicTime,
|
dynamicTime,
|
||||||
toggleEditModal() {
|
toggleEditModal() {
|
||||||
this.showEditModal = !this.showEditModal;
|
this.showEditModal = !this.showEditModal;
|
||||||
},
|
},
|
||||||
toggleConversationModal() {
|
openComposeConversationModal(toggleFn) {
|
||||||
this.showConversationModal = !this.showConversationModal;
|
toggleFn();
|
||||||
emitter.emit(
|
// Flag to prevent triggering drag n drop,
|
||||||
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
// When compose modal is active
|
||||||
this.showConversationModal
|
emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, true);
|
||||||
);
|
},
|
||||||
|
closeComposeConversationModal() {
|
||||||
|
// Flag to enable drag n drop,
|
||||||
|
// When compose modal is closed
|
||||||
|
emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
|
||||||
},
|
},
|
||||||
toggleDeleteModal() {
|
toggleDeleteModal() {
|
||||||
this.showDeleteModal = !this.showDeleteModal;
|
this.showDeleteModal = !this.showDeleteModal;
|
||||||
@@ -113,7 +124,6 @@ export default {
|
|||||||
},
|
},
|
||||||
closeDelete() {
|
closeDelete() {
|
||||||
this.showDeleteModal = false;
|
this.showDeleteModal = false;
|
||||||
this.showConversationModal = false;
|
|
||||||
this.showEditModal = false;
|
this.showEditModal = false;
|
||||||
},
|
},
|
||||||
findCountryFlag(countryCode, cityAndCountry) {
|
findCountryFlag(countryCode, cityAndCountry) {
|
||||||
@@ -250,14 +260,22 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center w-full mt-0.5 gap-2">
|
<div class="flex items-center w-full mt-0.5 gap-2">
|
||||||
<NextButton
|
<ComposeConversation
|
||||||
v-tooltip.top-end="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
:contact-id="String(contact.id)"
|
||||||
icon="i-ph-chat-circle-dots"
|
is-modal
|
||||||
slate
|
@close="closeComposeConversationModal"
|
||||||
faded
|
>
|
||||||
sm
|
<template #trigger="{ toggle }">
|
||||||
@click="toggleConversationModal"
|
<NextButton
|
||||||
/>
|
v-tooltip.top-end="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||||
|
icon="i-ph-chat-circle-dots"
|
||||||
|
slate
|
||||||
|
faded
|
||||||
|
sm
|
||||||
|
@click="openComposeConversationModal(toggle)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ComposeConversation>
|
||||||
<NextButton
|
<NextButton
|
||||||
v-tooltip.top-end="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
v-tooltip.top-end="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||||
icon="i-ph-pencil-simple"
|
icon="i-ph-pencil-simple"
|
||||||
@@ -293,12 +311,6 @@ export default {
|
|||||||
:contact="contact"
|
:contact="contact"
|
||||||
@cancel="toggleEditModal"
|
@cancel="toggleEditModal"
|
||||||
/>
|
/>
|
||||||
<NewConversation
|
|
||||||
v-if="contact.id"
|
|
||||||
:show="showConversationModal"
|
|
||||||
:contact="contact"
|
|
||||||
@cancel="toggleConversationModal"
|
|
||||||
/>
|
|
||||||
<ContactMergeModal
|
<ContactMergeModal
|
||||||
v-if="showMergeModal"
|
v-if="showMergeModal"
|
||||||
:primary-contact="contact"
|
:primary-contact="contact"
|
||||||
|
|||||||
@@ -1,638 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
// constants & helpers
|
|
||||||
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
|
||||||
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
|
||||||
import { getInboxSource, INBOX_TYPES } from 'dashboard/helper/inbox';
|
|
||||||
|
|
||||||
// store
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
// composables
|
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
import { required, requiredIf } from '@vuelidate/validators';
|
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
|
||||||
|
|
||||||
// mixins
|
|
||||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
|
||||||
|
|
||||||
// components
|
|
||||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
|
||||||
import CannedResponse from 'dashboard/components/widgets/conversation/CannedResponse.vue';
|
|
||||||
import InboxDropdownItem from 'dashboard/components/widgets/InboxDropdownItem.vue';
|
|
||||||
import MessageSignatureMissingAlert from 'dashboard/components/widgets/conversation/MessageSignatureMissingAlert.vue';
|
|
||||||
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead.vue';
|
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
|
||||||
import FileUpload from 'vue-upload-component';
|
|
||||||
import WhatsappTemplates from './WhatsappTemplates.vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
appendSignature,
|
|
||||||
removeSignature,
|
|
||||||
} from 'dashboard/helper/editorHelper';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Thumbnail,
|
|
||||||
WootMessageEditor,
|
|
||||||
ReplyEmailHead,
|
|
||||||
CannedResponse,
|
|
||||||
WhatsappTemplates,
|
|
||||||
InboxDropdownItem,
|
|
||||||
FileUpload,
|
|
||||||
AttachmentPreview,
|
|
||||||
MessageSignatureMissingAlert,
|
|
||||||
},
|
|
||||||
mixins: [inboxMixin, fileUploadMixin],
|
|
||||||
props: {
|
|
||||||
contact: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
onSubmit: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['cancel', 'success'],
|
|
||||||
setup() {
|
|
||||||
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
|
|
||||||
useUISettings();
|
|
||||||
const v$ = useVuelidate();
|
|
||||||
const uploadAttachment = ref(false);
|
|
||||||
|
|
||||||
return {
|
|
||||||
fetchSignatureFlagFromUISettings,
|
|
||||||
setSignatureFlagForInbox,
|
|
||||||
v$,
|
|
||||||
uploadAttachment,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: '',
|
|
||||||
subject: '',
|
|
||||||
message: '',
|
|
||||||
showCannedResponseMenu: false,
|
|
||||||
cannedResponseSearchKey: '',
|
|
||||||
bccEmails: '',
|
|
||||||
ccEmails: '',
|
|
||||||
targetInbox: {},
|
|
||||||
whatsappTemplateSelected: false,
|
|
||||||
attachedFiles: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validations() {
|
|
||||||
return {
|
|
||||||
subject: {
|
|
||||||
required: requiredIf(this.isAnEmailInbox),
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
targetInbox: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
uiFlags: 'contacts/getUIFlags',
|
|
||||||
conversationsUiFlags: 'contactConversations/getUIFlags',
|
|
||||||
currentUser: 'getCurrentUser',
|
|
||||||
globalConfig: 'globalConfig/get',
|
|
||||||
messageSignature: 'getMessageSignature',
|
|
||||||
inboxesList: 'inboxes/getInboxes',
|
|
||||||
}),
|
|
||||||
sendWithSignature() {
|
|
||||||
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
|
||||||
},
|
|
||||||
signatureToApply() {
|
|
||||||
return this.messageSignature;
|
|
||||||
},
|
|
||||||
newMessagePayload() {
|
|
||||||
const payload = {
|
|
||||||
inboxId: this.targetInbox.id,
|
|
||||||
sourceId: this.targetInbox.sourceId,
|
|
||||||
contactId: this.contact.id,
|
|
||||||
message: { content: this.message },
|
|
||||||
mailSubject: this.subject,
|
|
||||||
assigneeId: this.currentUser.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.attachedFiles && this.attachedFiles.length) {
|
|
||||||
payload.files = [];
|
|
||||||
this.setAttachmentPayload(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ccEmails) {
|
|
||||||
payload.message.cc_emails = this.ccEmails;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.bccEmails) {
|
|
||||||
payload.message.bcc_emails = this.bccEmails;
|
|
||||||
}
|
|
||||||
return payload;
|
|
||||||
},
|
|
||||||
selectedInbox: {
|
|
||||||
get() {
|
|
||||||
const inboxList = this.contact.contact_inboxes || [];
|
|
||||||
const selectedContactInbox = inboxList.find(
|
|
||||||
inbox => inbox.inbox?.id && inbox.inbox?.id === this.targetInbox?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedContactInbox) {
|
|
||||||
return { inbox: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the matching inbox from the inboxesList
|
|
||||||
const matchingInbox =
|
|
||||||
this.inboxesList.find(
|
|
||||||
item => item.id === selectedContactInbox.inbox?.id
|
|
||||||
) || {};
|
|
||||||
|
|
||||||
// The entire inbox payload is not available in this object, so we need to patch it from the store
|
|
||||||
return {
|
|
||||||
...selectedContactInbox,
|
|
||||||
inbox: {
|
|
||||||
...matchingInbox,
|
|
||||||
...selectedContactInbox.inbox,
|
|
||||||
sourceId: selectedContactInbox.source_id || matchingInbox.sourceId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.targetInbox = value.inbox;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
showNoInboxAlert() {
|
|
||||||
if (!this.contact.contact_inboxes) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.inboxes.length === 0 && !this.uiFlags.isFetchingInboxes;
|
|
||||||
},
|
|
||||||
isSignatureEnabledForInbox() {
|
|
||||||
return this.isAnEmailInbox && this.sendWithSignature;
|
|
||||||
},
|
|
||||||
signatureToggleTooltip() {
|
|
||||||
return this.sendWithSignature
|
|
||||||
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
|
|
||||||
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
|
||||||
},
|
|
||||||
|
|
||||||
inboxes() {
|
|
||||||
const inboxList = this.contact.contact_inboxes || [];
|
|
||||||
if (!inboxList.length) return [];
|
|
||||||
|
|
||||||
return inboxList.map(inbox => {
|
|
||||||
const matchingInbox =
|
|
||||||
this.inboxesList.find(item => item.id === inbox.inbox?.id) || {};
|
|
||||||
|
|
||||||
// Create merged object with a clear property order
|
|
||||||
return {
|
|
||||||
...matchingInbox,
|
|
||||||
...inbox.inbox,
|
|
||||||
sourceId: inbox.source_id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isAnEmailInbox() {
|
|
||||||
return (
|
|
||||||
this.selectedInbox &&
|
|
||||||
this.selectedInbox.inbox.channel_type === INBOX_TYPES.EMAIL
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isAnWebWidgetInbox() {
|
|
||||||
return (
|
|
||||||
this.selectedInbox &&
|
|
||||||
this.selectedInbox.inbox.channel_type === INBOX_TYPES.WEB
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isEmailOrWebWidgetInbox() {
|
|
||||||
return this.isAnEmailInbox || this.isAnWebWidgetInbox;
|
|
||||||
},
|
|
||||||
hasWhatsappTemplates() {
|
|
||||||
return !!this.selectedInbox.inbox?.message_templates;
|
|
||||||
},
|
|
||||||
hasAttachments() {
|
|
||||||
return this.attachedFiles.length;
|
|
||||||
},
|
|
||||||
inbox() {
|
|
||||||
return this.targetInbox;
|
|
||||||
},
|
|
||||||
allowedFileTypes() {
|
|
||||||
return ALLOWED_FILE_TYPES;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
message(value) {
|
|
||||||
this.hasSlashCommand = value[0] === '/' && !this.isEmailOrWebWidgetInbox;
|
|
||||||
const hasNextWord = value.includes(' ');
|
|
||||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
|
||||||
if (isShortCodeActive) {
|
|
||||||
this.cannedResponseSearchKey = value.substring(1);
|
|
||||||
this.showCannedResponseMenu = true;
|
|
||||||
} else {
|
|
||||||
this.cannedResponseSearchKey = '';
|
|
||||||
this.showCannedResponseMenu = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
targetInbox() {
|
|
||||||
this.setSignature();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setSignature();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setSignature() {
|
|
||||||
if (this.messageSignature) {
|
|
||||||
if (this.isSignatureEnabledForInbox) {
|
|
||||||
this.message = appendSignature(this.message, this.signatureToApply);
|
|
||||||
} else {
|
|
||||||
this.message = removeSignature(this.message, this.signatureToApply);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setAttachmentPayload(payload) {
|
|
||||||
this.attachedFiles.forEach(attachment => {
|
|
||||||
if (this.globalConfig.directUploadsEnabled) {
|
|
||||||
payload.files.push(attachment.blobSignedId);
|
|
||||||
} else {
|
|
||||||
payload.files.push(attachment.resource.file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
attachFile({ blob, file }) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file.file);
|
|
||||||
reader.onloadend = () => {
|
|
||||||
this.attachedFiles.push({
|
|
||||||
currentChatId: this.contact.id,
|
|
||||||
resource: blob || file,
|
|
||||||
isPrivate: this.isPrivate,
|
|
||||||
thumb: reader.result,
|
|
||||||
blobSignedId: blob ? blob.signed_id : undefined,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
removeAttachment(attachments) {
|
|
||||||
this.attachedFiles = attachments;
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
this.$emit('success');
|
|
||||||
},
|
|
||||||
replaceTextWithCannedResponse(message) {
|
|
||||||
this.message = message;
|
|
||||||
},
|
|
||||||
toggleCannedMenu(value) {
|
|
||||||
this.showCannedMenu = value;
|
|
||||||
},
|
|
||||||
prepareWhatsAppMessagePayload({ message: content, templateParams }) {
|
|
||||||
const payload = {
|
|
||||||
inboxId: this.targetInbox.id,
|
|
||||||
sourceId: this.targetInbox.sourceId,
|
|
||||||
contactId: this.contact.id,
|
|
||||||
message: { content, template_params: templateParams },
|
|
||||||
assigneeId: this.currentUser.id,
|
|
||||||
};
|
|
||||||
return payload;
|
|
||||||
},
|
|
||||||
onFormSubmit() {
|
|
||||||
const isFromWhatsApp = false;
|
|
||||||
this.v$.$touch();
|
|
||||||
if (this.v$.$invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.createConversation({
|
|
||||||
payload: this.newMessagePayload,
|
|
||||||
isFromWhatsApp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async createConversation({ payload, isFromWhatsApp }) {
|
|
||||||
try {
|
|
||||||
const data = await this.onSubmit(payload, isFromWhatsApp);
|
|
||||||
const action = {
|
|
||||||
type: 'link',
|
|
||||||
to: `/app/accounts/${data.account_id}/conversations/${data.id}`,
|
|
||||||
message: this.$t('NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'),
|
|
||||||
};
|
|
||||||
this.onSuccess();
|
|
||||||
useAlert(this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), action);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ExceptionWithMessage) {
|
|
||||||
useAlert(error.data);
|
|
||||||
} else {
|
|
||||||
useAlert(this.$t('NEW_CONVERSATION.FORM.ERROR_MESSAGE'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleWaTemplate(val) {
|
|
||||||
this.whatsappTemplateSelected = val;
|
|
||||||
},
|
|
||||||
async onSendWhatsAppReply(messagePayload) {
|
|
||||||
const isFromWhatsApp = true;
|
|
||||||
const payload = this.prepareWhatsAppMessagePayload(messagePayload);
|
|
||||||
await this.createConversation({ payload, isFromWhatsApp });
|
|
||||||
},
|
|
||||||
inboxReadableIdentifier(inbox) {
|
|
||||||
return `${inbox.name} (${inbox.channel_type})`;
|
|
||||||
},
|
|
||||||
computedInboxSource(inbox) {
|
|
||||||
if (!inbox.channel_type) return '';
|
|
||||||
const classByType = getInboxSource(
|
|
||||||
inbox.channel_type,
|
|
||||||
inbox.phone_number,
|
|
||||||
inbox
|
|
||||||
);
|
|
||||||
return classByType;
|
|
||||||
},
|
|
||||||
toggleMessageSignature() {
|
|
||||||
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
|
||||||
this.setSignature();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- eslint-disable vue/prefer-true-attribute-shorthand -->
|
|
||||||
<template>
|
|
||||||
<form class="w-full conversation--form" @submit.prevent="onFormSubmit">
|
|
||||||
<div
|
|
||||||
v-if="showNoInboxAlert"
|
|
||||||
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-none text-sm border border-solid border-yellow-500 dark:border-yellow-700 bg-yellow-200/60 dark:bg-yellow-200/20 text-slate-700 dark:text-yellow-400"
|
|
||||||
>
|
|
||||||
<p class="mb-0">
|
|
||||||
{{ $t('NEW_CONVERSATION.NO_INBOX') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex flex-row gap-2">
|
|
||||||
<div class="w-[50%]">
|
|
||||||
<label>
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="multiselect-wrap--small"
|
|
||||||
:class="{ 'has-multi-select-error': v$.targetInbox.$error }"
|
|
||||||
>
|
|
||||||
<multiselect
|
|
||||||
v-model="targetInbox"
|
|
||||||
track-by="id"
|
|
||||||
label="name"
|
|
||||||
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
|
||||||
selected-label=""
|
|
||||||
select-label=""
|
|
||||||
class="reset-base"
|
|
||||||
deselect-label=""
|
|
||||||
:max-height="160"
|
|
||||||
close-on-select
|
|
||||||
:options="[...inboxes]"
|
|
||||||
>
|
|
||||||
<template #singleLabel="{ option }">
|
|
||||||
<InboxDropdownItem
|
|
||||||
v-if="option.name"
|
|
||||||
:name="option.name"
|
|
||||||
:inbox-identifier="computedInboxSource(option)"
|
|
||||||
:channel-type="option.channel_type"
|
|
||||||
/>
|
|
||||||
<span v-else>
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.PLACEHOLDER') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #option="{ option }">
|
|
||||||
<InboxDropdownItem
|
|
||||||
:name="option.name"
|
|
||||||
:inbox-identifier="computedInboxSource(option)"
|
|
||||||
:channel-type="option.channel_type"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
<label :class="{ error: v$.targetInbox.$error }">
|
|
||||||
<span v-if="v$.targetInbox.$error" class="message">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="w-[50%]">
|
|
||||||
<label>
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.TO.LABEL') }}
|
|
||||||
<div
|
|
||||||
class="flex items-center h-[2.4735rem] rounded-sm py-1 px-2 bg-slate-25 dark:bg-slate-900 border border-solid border-slate-75 dark:border-slate-600"
|
|
||||||
>
|
|
||||||
<Thumbnail
|
|
||||||
:src="contact.thumbnail"
|
|
||||||
size="24px"
|
|
||||||
:username="contact.name"
|
|
||||||
:status="contact.availability_status"
|
|
||||||
/>
|
|
||||||
<h4
|
|
||||||
class="m-0 ml-2 mr-2 text-sm text-slate-700 dark:text-slate-100"
|
|
||||||
>
|
|
||||||
{{ contact.name }}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isAnEmailInbox" class="w-full">
|
|
||||||
<div class="w-full">
|
|
||||||
<label :class="{ error: v$.subject.$error }">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.LABEL') }}
|
|
||||||
<input
|
|
||||||
v-model="subject"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('NEW_CONVERSATION.FORM.SUBJECT.PLACEHOLDER')"
|
|
||||||
@input="v$.subject.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="v$.subject.$error" class="message">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="relative">
|
|
||||||
<CannedResponse
|
|
||||||
v-if="showCannedResponseMenu && hasSlashCommand"
|
|
||||||
:search-key="cannedResponseSearchKey"
|
|
||||||
@replace="replaceTextWithCannedResponse"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="isEmailOrWebWidgetInbox">
|
|
||||||
<label>
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<ReplyEmailHead
|
|
||||||
v-if="isAnEmailInbox"
|
|
||||||
v-model:cc-emails="ccEmails"
|
|
||||||
v-model:bcc-emails="bccEmails"
|
|
||||||
/>
|
|
||||||
<div class="editor-wrap">
|
|
||||||
<WootMessageEditor
|
|
||||||
v-model="message"
|
|
||||||
class="message-editor"
|
|
||||||
:class="{ editor_warning: v$.message.$error }"
|
|
||||||
enable-variables
|
|
||||||
:signature="signatureToApply"
|
|
||||||
allow-signature
|
|
||||||
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
|
||||||
@toggle-canned-menu="toggleCannedMenu"
|
|
||||||
@blur="v$.message.$touch"
|
|
||||||
>
|
|
||||||
<template #footer>
|
|
||||||
<MessageSignatureMissingAlert
|
|
||||||
v-if="isSignatureEnabledForInbox && !messageSignature"
|
|
||||||
class="!mx-0 mb-1"
|
|
||||||
/>
|
|
||||||
<div v-if="isAnEmailInbox" class="mt-px mb-3">
|
|
||||||
<woot-button
|
|
||||||
v-tooltip.top-end="signatureToggleTooltip"
|
|
||||||
icon="signature"
|
|
||||||
color-scheme="secondary"
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
:title="signatureToggleTooltip"
|
|
||||||
@click.prevent="toggleMessageSignature"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</WootMessageEditor>
|
|
||||||
<span v-if="v$.message.$error" class="editor-warning__message">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WhatsappTemplates
|
|
||||||
v-else-if="hasWhatsappTemplates"
|
|
||||||
:inbox-id="selectedInbox.inbox.id"
|
|
||||||
@on-select-template="toggleWaTemplate"
|
|
||||||
@on-send="onSendWhatsAppReply"
|
|
||||||
/>
|
|
||||||
<label v-else :class="{ error: v$.message.$error }">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
|
||||||
<textarea
|
|
||||||
v-model="message"
|
|
||||||
class="min-h-[5rem]"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
|
||||||
@input="v$.message.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="v$.message.$error" class="message">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div v-if="isEmailOrWebWidgetInbox" class="flex flex-col">
|
|
||||||
<FileUpload
|
|
||||||
ref="uploadAttachment"
|
|
||||||
input-id="newConversationAttachment"
|
|
||||||
:size="4096 * 4096"
|
|
||||||
:accept="allowedFileTypes"
|
|
||||||
multiple
|
|
||||||
:drop="true"
|
|
||||||
:drop-directory="false"
|
|
||||||
:data="{
|
|
||||||
direct_upload_url: '/rails/active_storage/direct_uploads',
|
|
||||||
direct_upload: true,
|
|
||||||
}"
|
|
||||||
@input-file="onFileUpload"
|
|
||||||
>
|
|
||||||
<woot-button
|
|
||||||
class-names="button--upload"
|
|
||||||
icon="attach"
|
|
||||||
emoji="📎"
|
|
||||||
color-scheme="secondary"
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.SELECT') }}
|
|
||||||
</woot-button>
|
|
||||||
<span
|
|
||||||
class="text-xs font-medium text-slate-500 ltr:ml-1 rtl:mr-1 dark:text-slate-400"
|
|
||||||
>
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.HELP_TEXT') }}
|
|
||||||
</span>
|
|
||||||
</FileUpload>
|
|
||||||
<div
|
|
||||||
v-if="hasAttachments"
|
|
||||||
class="max-h-20 overflow-y-auto mb-4 mt-1.5"
|
|
||||||
>
|
|
||||||
<AttachmentPreview
|
|
||||||
class="[&>.preview-item]:dark:bg-slate-700 flex-row flex-wrap gap-x-3 gap-y-1"
|
|
||||||
:attachments="attachedFiles"
|
|
||||||
@remove-attachment="removeAttachment"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!hasWhatsappTemplates"
|
|
||||||
class="flex flex-row justify-end w-full gap-2 px-0 py-2"
|
|
||||||
>
|
|
||||||
<button class="button clear" @click.prevent="onCancel">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.CANCEL') }}
|
|
||||||
</button>
|
|
||||||
<woot-button type="submit" :is-loading="conversationsUiFlags.isCreating">
|
|
||||||
{{ $t('NEW_CONVERSATION.FORM.SUBMIT') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition v-if="isEmailOrWebWidgetInbox" name="modal-fade">
|
|
||||||
<div
|
|
||||||
v-show="uploadAttachment && uploadAttachment.dropActive"
|
|
||||||
class="absolute top-0 bottom-0 left-0 right-0 z-30 flex flex-col items-center justify-center w-full h-full gap-2 bg-white/80 dark:bg-slate-700/80"
|
|
||||||
>
|
|
||||||
<fluent-icon icon="cloud-backup" size="40" />
|
|
||||||
<h4 class="text-2xl break-words text-slate-600 dark:text-slate-200">
|
|
||||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.conversation--form {
|
|
||||||
@apply pt-4 px-8 pb-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-editor {
|
|
||||||
@apply px-3;
|
|
||||||
|
|
||||||
::v-deep {
|
|
||||||
.ProseMirror-menubar {
|
|
||||||
@apply rounded-tl-[4px];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-uploads {
|
|
||||||
@apply text-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect-wrap--small.has-multi-select-error {
|
|
||||||
::v-deep {
|
|
||||||
.multiselect__tags {
|
|
||||||
@apply border-red-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep {
|
|
||||||
.mention--box {
|
|
||||||
@apply left-0 m-auto right-0 top-auto h-fit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ConversationForm from './ConversationForm.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ConversationForm,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
contact: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['cancel', 'update:show'],
|
|
||||||
computed: {
|
|
||||||
localShow: {
|
|
||||||
get() {
|
|
||||||
return this.show;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$emit('update:show', value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'contact.id'(id) {
|
|
||||||
this.$store.dispatch('contacts/fetchContactableInbox', id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const { id } = this.contact;
|
|
||||||
this.$store.dispatch('contacts/fetchContactableInbox', id);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onCancel() {
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
async onSubmit(params, isFromWhatsApp) {
|
|
||||||
const data = await this.$store.dispatch('contactConversations/create', {
|
|
||||||
params,
|
|
||||||
isFromWhatsApp,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<woot-modal v-model:show="localShow" :on-close="onCancel">
|
|
||||||
<div class="flex flex-col h-auto overflow-auto">
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('NEW_CONVERSATION.TITLE')"
|
|
||||||
:header-content="$t('NEW_CONVERSATION.DESC')"
|
|
||||||
/>
|
|
||||||
<ConversationForm
|
|
||||||
:contact="contact"
|
|
||||||
:on-submit="onSubmit"
|
|
||||||
@success="onSuccess"
|
|
||||||
@cancel="onCancel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</woot-modal>
|
|
||||||
</template>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<script>
|
|
||||||
import TemplatesPicker from 'dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue';
|
|
||||||
import TemplateParser from 'dashboard/components/widgets/conversation/WhatsappTemplates/TemplateParser.vue';
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
TemplatesPicker,
|
|
||||||
TemplateParser,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
inboxId: {
|
|
||||||
type: Number,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['pickTemplate', 'onSend', 'cancel'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedWaTemplate: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
pickTemplate(template) {
|
|
||||||
this.$emit('pickTemplate', true);
|
|
||||||
this.selectedWaTemplate = template;
|
|
||||||
},
|
|
||||||
onResetTemplate() {
|
|
||||||
this.$emit('pickTemplate', false);
|
|
||||||
this.selectedWaTemplate = null;
|
|
||||||
},
|
|
||||||
onSendMessage(message) {
|
|
||||||
this.$emit('onSend', message);
|
|
||||||
},
|
|
||||||
onClose() {
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-wrap mx-0">
|
|
||||||
<TemplatesPicker
|
|
||||||
v-if="!selectedWaTemplate"
|
|
||||||
:inbox-id="inboxId"
|
|
||||||
@on-select="pickTemplate"
|
|
||||||
/>
|
|
||||||
<TemplateParser
|
|
||||||
v-else
|
|
||||||
:template="selectedWaTemplate"
|
|
||||||
@reset-template="onResetTemplate"
|
|
||||||
@send-message="onSendMessage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
Reference in New Issue
Block a user