Merge branch 'develop' into fix/hc-editor

This commit is contained in:
Shivam Mishra
2025-02-10 20:02:49 +05:30
committed by GitHub
61 changed files with 1638 additions and 1212 deletions
@@ -1,7 +1,7 @@
<script setup>
import { computed, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { required, email, minLength } from '@vuelidate/validators';
import { required, email } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core';
import { splitName } from '@chatwoot/utils';
import countries from 'shared/constants/countries.js';
@@ -74,7 +74,7 @@ const defaultState = {
const state = reactive({ ...defaultState });
const validationRules = {
firstName: { required, minLength: minLength(2) },
firstName: { required },
email: { email },
};
@@ -59,6 +59,7 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SAVE_CONTACT')
"
color="blue"
:disabled="contactsFormRef?.isFormInvalid"
:is-loading="isCreatingContact"
@click="handleDialogConfirm"
/>
@@ -171,6 +171,10 @@ watch(
{ immediate: true, deep: true }
);
const handleClickOutside = () => {
showComposeNewConversation.value = false;
};
onMounted(() => resetContacts());
const keyboardEvents = {
@@ -188,7 +192,12 @@ useKeyboardEvents(keyboardEvents);
<template>
<div
v-on-click-outside="() => (showComposeNewConversation = false)"
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'] },
]"
class="relative"
:class="{
'z-40': showComposeNewConversation,
@@ -77,7 +77,7 @@ const toggleMessageSignature = () => {
setSignature();
};
// Added this watch to dynamically set signature.
// Added this watch to dynamically set signature on target inbox change.
// Only targetInbox has value and is Advance Editor(used by isEmailOrWebWidgetInbox)
// Set the signature only if the inbox based flag is true
watch(
@@ -86,7 +86,8 @@ watch(
nextTick(() => {
if (newValue && props.isEmailOrWebWidgetInbox) setSignature();
});
}
},
{ immediate: true }
);
const onClickInsertEmoji = emoji => {
@@ -15,18 +15,14 @@ import { useCamelCase } from 'dashboard/composables/useTransformKeys';
* @property {Array} messages - Array of all messages [These are not in camelcase]
*/
const props = defineProps({
readMessages: {
type: Array,
default: () => [],
},
unReadMessages: {
type: Array,
default: () => [],
},
currentUserId: {
type: Number,
required: true,
},
firstUnreadId: {
type: Number,
default: null,
},
isAnEmailChannel: {
type: Boolean,
default: false,
@@ -41,12 +37,8 @@ const props = defineProps({
},
});
const unread = computed(() => {
return useCamelCase(props.unReadMessages, { deep: true });
});
const read = computed(() => {
return useCamelCase(props.readMessages, { deep: true });
const allMessages = computed(() => {
return useCamelCase(props.messages, { deep: true });
});
/**
@@ -108,26 +100,18 @@ const getInReplyToMessage = parentMessage => {
<template>
<ul class="px-4 bg-n-background">
<slot name="beforeAll" />
<template v-for="(message, index) in read" :key="message.id">
<Message
v-bind="message"
:is-email-inbox="isAnEmailChannel"
:in-reply-to="getInReplyToMessage(message)"
:group-with-next="shouldGroupWithNext(index, read)"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
data-clarity-mask="True"
<template v-for="(message, index) in allMessages" :key="message.id">
<slot
v-if="firstUnreadId && message.id === firstUnreadId"
name="unreadBadge"
/>
</template>
<slot name="beforeUnread" />
<template v-for="(message, index) in unread" :key="message.id">
<Message
v-bind="message"
:is-email-inbox="isAnEmailChannel"
:in-reply-to="getInReplyToMessage(message)"
:group-with-next="shouldGroupWithNext(index, unread)"
:group-with-next="shouldGroupWithNext(index, allMessages)"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
:is-email-inbox="isAnEmailChannel"
data-clarity-mask="True"
/>
</template>
@@ -26,7 +26,18 @@ const ccEmail = computed(() => {
});
const senderName = computed(() => {
return sender.value.name ?? '';
const fromEmailAddress = fromEmail.value[0] ?? '';
const senderEmail = sender.value.email ?? '';
if (!fromEmailAddress && !senderEmail) return null;
// if the sender of the conversation and the sender of this particular
// email are the same, only then we return the sender name
if (fromEmailAddress === senderEmail) {
return sender.value.name;
}
return null;
});
const bccEmail = computed(() => {
@@ -59,11 +70,19 @@ const showMeta = computed(() => {
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
>
<template v-if="showMeta">
<div v-if="fromEmail[0]">
<span :class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'">
{{ senderName }}
</span>
&lt;{{ fromEmail[0] }}&gt;
<div
v-if="fromEmail[0]"
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'"
>
<template v-if="senderName">
<span>
{{ senderName }}
</span>
&lt;{{ fromEmail[0] }}&gt;
</template>
<template v-else>
{{ fromEmail[0] }}
</template>
</div>
<div v-if="toEmail.length">
{{ $t('EMAIL_HEADER.TO') }}: {{ toEmail.join(', ') }}
@@ -38,9 +38,10 @@ onMounted(() => {
});
const formatTime = time => {
if (!time || Number.isNaN(time)) return '00:00';
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
const toggleMute = () => {
@@ -49,7 +50,7 @@ const toggleMute = () => {
};
const onTimeUpdate = () => {
currentTime.value = audioPlayer.value.currentTime;
currentTime.value = audioPlayer.value?.currentTime;
};
const seek = event => {
@@ -1,6 +1,7 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { getFileInfo } from '@chatwoot/utils';
import FileIcon from 'next/icon/FileIcon.vue';
import Icon from 'next/icon/Icon.vue';
@@ -14,17 +15,20 @@ const { attachment } = defineProps({
const { t } = useI18n();
const fileName = computed(() => {
const url = attachment.dataUrl;
if (url) {
const filename = url.substring(url.lastIndexOf('/') + 1);
return filename || t('CONVERSATION.UNKNOWN_FILE_TYPE');
}
return t('CONVERSATION.UNKNOWN_FILE_TYPE');
const fileDetails = computed(() => {
return getFileInfo(attachment?.dataUrl || '');
});
const fileType = computed(() => {
return fileName.value.split('.').pop();
const displayFileName = computed(() => {
const { base, type } = fileDetails.value;
const truncatedName = (str, maxLength, hasExt) =>
str.length > maxLength
? `${str.substring(0, maxLength).trimEnd()}${hasExt ? '..' : '...'}`
: str;
return type
? `${truncatedName(base, 12, true)}.${type}`
: truncatedName(base, 14, false);
});
const textColorClass = computed(() => {
@@ -47,21 +51,25 @@ const textColorClass = computed(() => {
zip: 'dark:text-[#EDEEF0] text-[#2F265F]',
};
return colorMap[fileType.value] || 'text-n-slate-12';
return colorMap[fileDetails.value.type] || 'text-n-slate-12';
});
</script>
<template>
<div
class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-container"
class="h-9 bg-n-alpha-white gap-2 overflow-hidden items-center flex px-2 rounded-lg border border-n-container"
>
<FileIcon class="flex-shrink-0" :file-type="fileType" />
<span class="mr-1 max-w-32 truncate" :class="textColorClass">
{{ fileName }}
<FileIcon class="flex-shrink-0" :file-type="fileDetails.type" />
<span
class="flex-1 min-w-0 text-sm max-w-36"
:title="fileDetails.name"
:class="textColorClass"
>
{{ displayFileName }}
</span>
<a
v-tooltip="t('CONVERSATION.DOWNLOAD')"
class="flex-shrink-0 h-9 grid place-content-center cursor-pointer text-n-slate-11"
class="flex-shrink-0 size-9 grid place-content-center cursor-pointer text-n-slate-11 hover:text-n-slate-12 transition-colors"
:href="attachment.dataUrl"
rel="noreferrer noopener nofollow"
target="_blank"
@@ -151,6 +151,9 @@ const menuItems = computed(() => {
icon: 'i-lucide-inbox',
to: accountScopedRoute('inbox_view'),
activeOn: ['inbox_view', 'inbox_view_conversation'],
getterKeys: {
badge: 'notifications/getHasUnreadNotifications',
},
},
{
name: 'Conversation',
@@ -15,6 +15,7 @@ const props = defineProps({
to: { type: Object, default: null },
activeOn: { type: Array, default: () => [] },
children: { type: Array, default: undefined },
getterKeys: { type: Object, default: () => ({}) },
});
const {
@@ -143,6 +144,7 @@ onMounted(async () => {
:name
:label
:to
:getter-keys="getterKeys"
:is-active="isActive"
:has-active-child="hasActiveChild"
:expandable="hasChildren"
@@ -1,7 +1,8 @@
<script setup>
import { useMapGetter } from 'dashboard/composables/store.js';
import Icon from 'next/icon/Icon.vue';
defineProps({
const props = defineProps({
to: { type: [Object, String], default: '' },
label: { type: String, default: '' },
icon: { type: [String, Object], default: '' },
@@ -9,9 +10,12 @@ defineProps({
isExpanded: { type: Boolean, default: false },
isActive: { type: Boolean, default: false },
hasActiveChild: { type: Boolean, default: false },
getterKeys: { type: Object, default: () => ({}) },
});
const emit = defineEmits(['toggle']);
const showBadge = useMapGetter(props.getterKeys.badge);
</script>
<template>
@@ -28,7 +32,13 @@ const emit = defineEmits(['toggle']);
}"
@click.stop="emit('toggle')"
>
<Icon v-if="icon" :icon="icon" class="size-4" />
<div v-if="icon" class="relative flex items-center gap-2">
<Icon v-if="icon" :icon="icon" class="size-4" />
<span
v-if="showBadge"
class="size-2 -top-px ltr:-right-px rtl:-left-px bg-n-brand absolute rounded-full border border-n-solid-2"
/>
</div>
<span class="text-sm font-medium leading-5 flex-grow">
{{ label }}
</span>