feat: Email forwarding
This commit is contained in:
@@ -95,6 +95,16 @@ const getInReplyToMessage = parentMessage => {
|
||||
|
||||
return replyMessage ? useCamelCase(replyMessage) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the address of the forwarded message
|
||||
* @param {Object} message - The message containing the forwarded message reference
|
||||
* @returns {Array|null} - The email addresses of the forwarded message, or null if not forwarded
|
||||
*/
|
||||
const getForwardedMessageAddress = message => {
|
||||
const { forwardedMessageId, toEmails } = message.contentAttributes || {};
|
||||
return forwardedMessageId ? toEmails : null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -105,6 +115,11 @@ const getInReplyToMessage = parentMessage => {
|
||||
v-if="firstUnreadId && message.id === firstUnreadId"
|
||||
name="unreadBadge"
|
||||
/>
|
||||
<slot
|
||||
v-if="getForwardedMessageAddress(message)"
|
||||
:address="getForwardedMessageAddress(message)"
|
||||
name="forwardedMessageAddress"
|
||||
/>
|
||||
<Message
|
||||
v-bind="message"
|
||||
:is-email-inbox="isAnEmailChannel"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownContainer from 'next/dropdown-menu/base/DropdownContainer.vue';
|
||||
import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
|
||||
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
||||
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
||||
|
||||
const emit = defineEmits(['openForward']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('CONVERSATION.MESSAGE_MENU.FORWARD_EMAIL'),
|
||||
value: 'forward',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownContainer>
|
||||
<template #trigger="{ toggle, isOpen }">
|
||||
<NextButton
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
xs
|
||||
slate
|
||||
faded
|
||||
:class="{ 'bg-n-alpha-2': isOpen }"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
<DropdownBody class="top-0 -right-6 min-w-64 z-50" strong>
|
||||
<DropdownSection class="max-h-80 overflow-scroll">
|
||||
<DropdownItem
|
||||
v-for="item in menuItems"
|
||||
:key="item.value"
|
||||
class="!items-start !gap-1 flex-col cursor-pointer"
|
||||
@click="() => emit('openForward')"
|
||||
>
|
||||
<template #label>
|
||||
<div class="items-start flex gap-1 flex-col">
|
||||
<span class="text-n-slate-12 text-sm">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownBody>
|
||||
</DropdownContainer>
|
||||
</template>
|
||||
@@ -14,7 +14,8 @@ const fromEmail = computed(() => {
|
||||
});
|
||||
|
||||
const toEmail = computed(() => {
|
||||
return contentAttributes.value?.email?.to ?? [];
|
||||
const { forwardedMessageId, toEmails, email } = contentAttributes.value;
|
||||
return forwardedMessageId ? (toEmails ?? []) : (email?.to ?? []);
|
||||
});
|
||||
|
||||
const ccEmail = computed(() => {
|
||||
@@ -66,10 +67,12 @@ const showMeta = computed(() => {
|
||||
<template>
|
||||
<section
|
||||
v-show="showMeta"
|
||||
class="space-y-1 rtl:pl-9 ltr:pr-9 text-sm break-words"
|
||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
|
||||
>
|
||||
<template v-if="showMeta">
|
||||
<div
|
||||
v-if="showMeta"
|
||||
class="space-y-1 rtl:pl-9 w-full ltr:pr-9 text-sm break-words"
|
||||
>
|
||||
<div
|
||||
v-if="fromEmail[0]"
|
||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'"
|
||||
@@ -81,7 +84,7 @@ const showMeta = computed(() => {
|
||||
<{{ fromEmail[0] }}>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ fromEmail[0] }}
|
||||
{{ $t('EMAIL_HEADER.FROM') }}: {{ fromEmail[0] }}
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="toEmail.length">
|
||||
@@ -99,6 +102,7 @@ const showMeta = computed(() => {
|
||||
{{ $t('EMAIL_HEADER.SUBJECT') }}:
|
||||
{{ subject }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -4,23 +4,26 @@ import { Letter } from 'vue-letter';
|
||||
import { allowedCssProperties } from 'lettersanitizer';
|
||||
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import MessageMenu from 'dashboard/components-next/message/MessageMenu.vue';
|
||||
import { EmailQuoteExtractor } from './removeReply.js';
|
||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
|
||||
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
|
||||
import EmailMeta from './EmailMeta.vue';
|
||||
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
|
||||
import ForwardMessageForm from 'dashboard/components-next/message/forwardMessage/ForwardMessage.vue';
|
||||
|
||||
import { useMessageContext } from '../../provider.js';
|
||||
import { MESSAGE_TYPES } from 'next/message/constants.js';
|
||||
import { MESSAGE_TYPES, MESSAGE_STATUS } from 'next/message/constants.js';
|
||||
import { useTranslations } from 'dashboard/composables/useTranslations';
|
||||
|
||||
const { content, contentAttributes, attachments, messageType } =
|
||||
const { id, status, content, contentAttributes, attachments, messageType } =
|
||||
useMessageContext();
|
||||
|
||||
const isExpandable = ref(false);
|
||||
const isExpanded = ref(false);
|
||||
const showQuotedMessage = ref(false);
|
||||
const showForwardMessageModal = ref(false);
|
||||
const renderOriginal = ref(false);
|
||||
const contentContainer = useTemplateRef('contentContainer');
|
||||
|
||||
@@ -31,6 +34,12 @@ onMounted(() => {
|
||||
const isOutgoing = computed(() => messageType.value === MESSAGE_TYPES.OUTGOING);
|
||||
const isIncoming = computed(() => !isOutgoing.value);
|
||||
|
||||
const isForwarded = computed(() => contentAttributes.value?.forwardedMessageId);
|
||||
|
||||
const showMessageMenu = computed(
|
||||
() => ![MESSAGE_STATUS.FAILED, MESSAGE_STATUS.PROGRESS].includes(status.value)
|
||||
);
|
||||
|
||||
const { hasTranslations, translationContent } =
|
||||
useTranslations(contentAttributes);
|
||||
|
||||
@@ -103,13 +112,30 @@ const handleSeeOriginal = () => {
|
||||
}"
|
||||
data-bubble-name="email"
|
||||
>
|
||||
<EmailMeta
|
||||
class="p-3"
|
||||
<div
|
||||
class="flex items-start gap-2 justify-end"
|
||||
:class="{
|
||||
'border-b border-n-strong': isIncoming,
|
||||
'border-b border-n-slate-8/20': isOutgoing,
|
||||
}"
|
||||
/>
|
||||
>
|
||||
<EmailMeta class="p-3 w-full flex justify-end items-start">
|
||||
<div
|
||||
v-if="showMessageMenu"
|
||||
class="flex gap-2 skip-context-menu flex-shrink-0 items-center relative"
|
||||
>
|
||||
<MessageMenu @open-forward="showForwardMessageModal = true" />
|
||||
<ForwardMessageForm
|
||||
v-if="showForwardMessageModal"
|
||||
:message="contentAttributes?.email"
|
||||
:message-id="id"
|
||||
class="absolute right-3 z-50 skip-context-menu top-10"
|
||||
@close="showForwardMessageModal = false"
|
||||
/>
|
||||
</div>
|
||||
</EmailMeta>
|
||||
</div>
|
||||
|
||||
<section ref="contentContainer" class="p-3">
|
||||
<div
|
||||
:class="{
|
||||
@@ -130,7 +156,7 @@ const handleSeeOriginal = () => {
|
||||
</button>
|
||||
</div>
|
||||
<FormattedContent
|
||||
v-if="isOutgoing && content"
|
||||
v-if="isOutgoing && content && !isForwarded"
|
||||
class="text-n-slate-12"
|
||||
:content="messageContent"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { EmailQuoteExtractor } from 'dashboard/components-next/message/bubbles/Email/removeReply.js';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
searchContacts,
|
||||
createNewContact,
|
||||
} from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
|
||||
|
||||
import ForwardMessageForm from './components/ForwardMessageForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
forwardType: {
|
||||
type: String,
|
||||
default: 'email',
|
||||
},
|
||||
message: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
messageId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const contacts = ref([]);
|
||||
const selectedContact = ref(null);
|
||||
const isCreatingContact = ref(false);
|
||||
const isSearching = ref(false);
|
||||
|
||||
const messageSignature = useMapGetter('getMessageSignature');
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
|
||||
const fromEmail = computed(() => props.message?.to?.[0]);
|
||||
|
||||
const fullHTML = computed(() => {
|
||||
return (
|
||||
props.message?.htmlContent?.full ??
|
||||
props.message?.textContent?.full?.replace(/\n/g, '<br>')
|
||||
);
|
||||
});
|
||||
|
||||
const unquotedHTML = computed(() =>
|
||||
EmailQuoteExtractor.extractQuotes(fullHTML.value)
|
||||
);
|
||||
|
||||
const hasQuotedMessage = computed(() =>
|
||||
EmailQuoteExtractor.hasQuotes(fullHTML.value)
|
||||
);
|
||||
|
||||
const textToShow = computed(() => {
|
||||
const text = props.message?.textContent?.full;
|
||||
return text?.replace(/\n/g, '<br>');
|
||||
});
|
||||
|
||||
const onContactSearch = debounce(
|
||||
async query => {
|
||||
isSearching.value = true;
|
||||
contacts.value = [];
|
||||
try {
|
||||
contacts.value = await searchContacts(query);
|
||||
isSearching.value = false;
|
||||
} catch (error) {
|
||||
useAlert(t('FORWARD_MESSAGE_FORM.CONTACT_SEARCH.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
isSearching.value = false;
|
||||
}
|
||||
},
|
||||
300,
|
||||
false
|
||||
);
|
||||
|
||||
const handleClickOutside = () => {
|
||||
selectedContact.value = null;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleForwardMessage = async ({ state }) => {
|
||||
try {
|
||||
const messagePayload = {
|
||||
conversationId: currentChat.value?.id,
|
||||
message: state.message,
|
||||
toEmails: selectedContact.value?.email,
|
||||
private: false,
|
||||
contentAttributes: {
|
||||
forwarded_message_id: props.messageId,
|
||||
},
|
||||
sender: {
|
||||
name: currentUser.value?.name,
|
||||
thumbnail: currentUser.value?.avatar_url,
|
||||
},
|
||||
};
|
||||
await store.dispatch('createPendingMessageAndSend', messagePayload);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
emitter.emit(BUS_EVENTS.MESSAGE_SENT);
|
||||
// Close the forward message modal after sending
|
||||
emit('close');
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.error ||
|
||||
t('FORWARD_MESSAGE_FORM.FORWARD_MESSAGE.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectedContact = async ({ value, action, ...rest }) => {
|
||||
let contact;
|
||||
if (action === 'create') {
|
||||
isCreatingContact.value = true;
|
||||
try {
|
||||
contact = await createNewContact(value);
|
||||
isCreatingContact.value = false;
|
||||
} catch (error) {
|
||||
isCreatingContact.value = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
contact = rest;
|
||||
}
|
||||
selectedContact.value = contact;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-on-click-outside="[
|
||||
handleClickOutside,
|
||||
// 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
|
||||
{ ignore: ['div.ProseMirror-prompt'] },
|
||||
]"
|
||||
>
|
||||
<ForwardMessageForm
|
||||
:forward-type="forwardType"
|
||||
:contacts="contacts"
|
||||
:selected-contact="selectedContact"
|
||||
:is-loading="isSearching"
|
||||
:is-creating-contact="isCreatingContact"
|
||||
:from-email="fromEmail"
|
||||
:message="message"
|
||||
:message-signature="messageSignature"
|
||||
:full-html="fullHTML"
|
||||
:unquoted-html="unquotedHTML"
|
||||
:text-to-show="textToShow"
|
||||
:has-quoted-message="hasQuotedMessage"
|
||||
@search-contacts="onContactSearch"
|
||||
@update-selected-contact="handleSelectedContact"
|
||||
@clear-selected-contact="selectedContact = null"
|
||||
@discard="emit('close')"
|
||||
@forward-message="handleForwardMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Letter } from 'vue-letter';
|
||||
import { allowedCssProperties } from 'lettersanitizer';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import EmailMeta from 'dashboard/components-next/message/bubbles/Email/EmailMeta.vue';
|
||||
|
||||
// import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
defineProps({
|
||||
hasQuotedMessage: { type: Boolean, default: false },
|
||||
fullHtml: { type: String, default: '' },
|
||||
unquotedHtml: { type: String, default: '' },
|
||||
textToShow: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const showQuotedMessage = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 h-full">
|
||||
<Editor
|
||||
v-model="modelValue"
|
||||
:placeholder="t('FORWARD_MESSAGE_FORM.EMAIL_EDITOR_PLACEHOLDER')"
|
||||
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4 [&>div]:!bg-transparent h-full [&_.ProseMirror-woot-style]:!max-h-[200px] [&_.ProseMirror-woot-style]:!min-h-fit"
|
||||
enable-variables
|
||||
:show-character-count="false"
|
||||
/>
|
||||
<div class="px-4 pb-4 flex flex-col gap-2">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div
|
||||
class="h-px w-20 bg-n-alpha-2 border-t border-dashed border-n-slate-12"
|
||||
/>
|
||||
<span class="font-semibold text-sm text-n-slate-12">
|
||||
{{ t('FORWARD_MESSAGE_FORM.FORWARDED_MESSAGE') }}
|
||||
</span>
|
||||
<div
|
||||
class="h-px w-20 bg-n-alpha-2 border-t border-dashed border-n-slate-12"
|
||||
/>
|
||||
</div>
|
||||
<EmailMeta />
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<Letter
|
||||
v-if="showQuotedMessage"
|
||||
class-name="prose prose-bubble !max-w-none letter-render"
|
||||
:allowed-css-properties="[
|
||||
...allowedCssProperties,
|
||||
'transform',
|
||||
'transform-origin',
|
||||
]"
|
||||
:html="fullHtml"
|
||||
:text="textToShow"
|
||||
/>
|
||||
<Letter
|
||||
v-else
|
||||
class-name="prose prose-bubble !max-w-none letter-render"
|
||||
:html="unquotedHtml"
|
||||
:allowed-css-properties="[
|
||||
...allowedCssProperties,
|
||||
'transform',
|
||||
'transform-origin',
|
||||
]"
|
||||
:text="textToShow"
|
||||
/>
|
||||
<button
|
||||
v-if="hasQuotedMessage"
|
||||
class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
|
||||
@click="showQuotedMessage = !showQuotedMessage"
|
||||
>
|
||||
<template v-if="showQuotedMessage">
|
||||
{{ t('FORWARD_MESSAGE_FORM.HIDE_QUOTED_TEXT') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('FORWARD_MESSAGE_FORM.SHOW_QUOTED_TEXT') }}
|
||||
</template>
|
||||
<Icon
|
||||
:icon="
|
||||
showQuotedMessage ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { buildContactableInboxesList } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper.js';
|
||||
import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
|
||||
import ContactSelector from 'dashboard/components-next/NewConversation/components/ContactSelector.vue';
|
||||
import ActionButtons from 'dashboard/components-next/NewConversation/components/ActionButtons.vue';
|
||||
import EmailMessageEditor from './EmailMessageEditor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
forwardType: { type: String, default: 'email' }, // eslint-disable-line vue/no-unused-properties
|
||||
contacts: { type: Array, default: () => [] },
|
||||
selectedContact: { type: Object, default: null },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
isCreatingContact: { type: Boolean, default: false },
|
||||
fromEmail: { type: String, default: null },
|
||||
messageSignature: { type: String, default: '' },
|
||||
fullHtml: { type: String, default: '' },
|
||||
unquotedHtml: { type: String, default: '' },
|
||||
textToShow: { type: String, default: '' },
|
||||
hasQuotedMessage: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'searchContacts',
|
||||
'updateSelectedContact',
|
||||
'clearSelectedContact',
|
||||
'discard',
|
||||
'forwardMessage',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
message: '',
|
||||
attachedFiles: [],
|
||||
});
|
||||
|
||||
const showContactsDropdown = ref(false);
|
||||
|
||||
const contactableInboxesList = computed(() => {
|
||||
return buildContactableInboxesList(props.selectedContact?.contactInboxes);
|
||||
});
|
||||
|
||||
const validationRules = computed(() => ({
|
||||
selectedContact: { required },
|
||||
}));
|
||||
|
||||
const v$ = useVuelidate(validationRules, {
|
||||
selectedContact: computed(() => props.selectedContact),
|
||||
});
|
||||
|
||||
const validationStates = computed(() => ({
|
||||
isContactInvalid:
|
||||
v$.value.selectedContact.$dirty && v$.value.selectedContact.$invalid,
|
||||
}));
|
||||
|
||||
const handleContactSearch = value => {
|
||||
showContactsDropdown.value = true;
|
||||
emit('searchContacts', {
|
||||
keys: ['email'],
|
||||
query: value,
|
||||
});
|
||||
};
|
||||
|
||||
const setSelectedContact = async ({ value, action, ...rest }) => {
|
||||
v$.value.$reset();
|
||||
emit('updateSelectedContact', { value, action, ...rest });
|
||||
showContactsDropdown.value = false;
|
||||
};
|
||||
|
||||
const clearSelectedContact = () => {
|
||||
emit('clearSelectedContact');
|
||||
// state.attachedFiles = [];
|
||||
};
|
||||
|
||||
const handleDropdownUpdate = (type, value) => {
|
||||
showContactsDropdown.value = value;
|
||||
};
|
||||
|
||||
const onClickInsertEmoji = emoji => {
|
||||
state.message += emoji;
|
||||
};
|
||||
|
||||
const handleAddSignature = signature => {
|
||||
state.message = appendSignature(state.message, signature);
|
||||
};
|
||||
|
||||
const handleRemoveSignature = signature => {
|
||||
state.message = removeSignature(state.message, signature);
|
||||
};
|
||||
|
||||
const handleAttachFile = files => {
|
||||
state.attachedFiles = files;
|
||||
};
|
||||
|
||||
const clearForm = () => {
|
||||
Object.assign(state, {
|
||||
message: '',
|
||||
attachedFiles: [],
|
||||
});
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
const isValid = await v$.value.$validate();
|
||||
if (!isValid) return;
|
||||
|
||||
try {
|
||||
const success = await emit('forwardMessage', { state });
|
||||
if (success) {
|
||||
clearForm();
|
||||
}
|
||||
} catch (error) {
|
||||
// Form will not be cleared if conversation creation fails
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[42rem] max-h-[31.25rem] overflow-y-scroll divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
|
||||
>
|
||||
<div class="relative flex-1 px-4 py-3 overflow-y-visible bg-n-alpha-3">
|
||||
<div class="flex items-baseline w-full gap-3 min-h-7">
|
||||
<label class="text-sm font-medium text-n-slate-11 whitespace-nowrap">
|
||||
{{ t('FORWARD_MESSAGE_FORM.FROM') }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 px-3 min-h-7 min-w-0"
|
||||
>
|
||||
<span class="text-sm truncate text-n-slate-12">
|
||||
{{ fromEmail }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContactSelector
|
||||
class="bg-n-alpha-3"
|
||||
:contacts="contacts"
|
||||
:selected-contact="selectedContact"
|
||||
:show-contacts-dropdown="showContactsDropdown"
|
||||
:is-loading="isLoading"
|
||||
:is-creating-contact="isCreatingContact"
|
||||
:contactable-inboxes-list="contactableInboxesList"
|
||||
:show-inboxes-dropdown="false"
|
||||
:has-errors="validationStates.isContactInvalid"
|
||||
@search-contacts="handleContactSearch"
|
||||
@set-selected-contact="setSelectedContact"
|
||||
@clear-selected-contact="clearSelectedContact"
|
||||
@update-dropdown="handleDropdownUpdate"
|
||||
/>
|
||||
<EmailMessageEditor
|
||||
v-model="state.message"
|
||||
class="bg-n-alpha-3"
|
||||
:has-quoted-message="hasQuotedMessage"
|
||||
:full-html="fullHtml"
|
||||
:unquoted-html="unquotedHtml"
|
||||
:text-to-show="textToShow"
|
||||
/>
|
||||
<ActionButtons
|
||||
class="bg-n-alpha-3 sticky bottom-0 backdrop-blur-[100px]"
|
||||
:attached-files="state.attachedFiles"
|
||||
is-email-or-web-widget-inbox
|
||||
channel-type="Channel::Email"
|
||||
:is-loading="false"
|
||||
:disable-send-button="false"
|
||||
has-selected-inbox
|
||||
:has-no-inbox="false"
|
||||
:is-dropdown-active="showContactsDropdown"
|
||||
:message-signature="messageSignature"
|
||||
@insert-emoji="onClickInsertEmoji"
|
||||
@add-signature="handleAddSignature"
|
||||
@remove-signature="handleRemoveSignature"
|
||||
@attach-file="handleAttachFile"
|
||||
@discard="$emit('discard')"
|
||||
@send-message="handleSendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -100,7 +100,8 @@ const MessageControl = Symbol('MessageControl');
|
||||
* @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message
|
||||
* @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user
|
||||
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
|
||||
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message
|
||||
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is different from groupWithNext, this has a bypass for a failed message
|
||||
* @property {import('vue').ComputedRef<EmailContent>} emailContent - Email content and metadata
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ import Message from './Message.vue';
|
||||
import NextMessageList from 'next/message/MessageList.vue';
|
||||
import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
// stores and apis
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -44,6 +45,7 @@ export default {
|
||||
components: {
|
||||
Message,
|
||||
NextMessageList,
|
||||
Icon,
|
||||
ReplyBox,
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
@@ -563,6 +565,16 @@ export default {
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
<template #forwardedMessageAddress="{ address }">
|
||||
<li class="flex items-center gap-1 !mt-4 !mb-2.5 ltr:pl-9 rtl:pr-9 h-5">
|
||||
<Icon icon="i-lucide-forward" class="text-n-amber-10 size-4" />
|
||||
<span class="text-n-amber-10 text-xs font-medium leading-[20px]">
|
||||
{{
|
||||
$t('CONVERSATION.FORWARDED_TO', { address: address?.join(', ') })
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
<template #after>
|
||||
<ConversationLabelSuggestion
|
||||
v-if="shouldShowLabelSuggestions"
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
|
||||
"FORWARDED_TO": "forwarded to {address}",
|
||||
"CARD": {
|
||||
"SHOW_LABELS": "Show labels",
|
||||
"HIDE_LABELS": "Hide labels"
|
||||
@@ -236,6 +237,23 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"MESSAGE_MENU": {
|
||||
"FORWARD_EMAIL": "Forward email"
|
||||
}
|
||||
},
|
||||
"FORWARD_MESSAGE_FORM": {
|
||||
"FROM": "From :",
|
||||
"EMAIL_EDITOR_PLACEHOLDER": "Write your message here...",
|
||||
"FORWARDED_MESSAGE": "Forwarded message",
|
||||
"SHOW_QUOTED_TEXT": "Show quoted text",
|
||||
"HIDE_QUOTED_TEXT": "Hide quoted text",
|
||||
"CONTACT_SEARCH": {
|
||||
"ERROR_MESSAGE": "We couldn’t complete the search. Please try again."
|
||||
},
|
||||
"FORWARD_MESSAGE": {
|
||||
"ERROR_MESSAGE": "We couldn’t able to forward the message. Please try again.",
|
||||
"SUCCESS_MESSAGE": "The message was forwarded successfully!"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
Reference in New Issue
Block a user