Merge branch 'develop' into feat/v5-ui-redesign

This commit is contained in:
Sivin Varghese
2025-12-17 22:54:42 +05:30
committed by GitHub
94 changed files with 2759 additions and 136 deletions
@@ -44,6 +44,7 @@ const SOCIAL_CONFIG = {
LINKEDIN: 'i-ri-linkedin-box-fill',
FACEBOOK: 'i-ri-facebook-circle-fill',
INSTAGRAM: 'i-ri-instagram-line',
TIKTOK: 'i-ri-tiktok-fill',
TWITTER: 'i-ri-twitter-x-fill',
GITHUB: 'i-ri-github-fill',
};
@@ -65,6 +66,7 @@ const defaultState = {
facebook: '',
github: '',
instagram: '',
tiktok: '',
linkedin: '',
twitter: '',
},
@@ -0,0 +1,88 @@
<script setup>
import Button from 'dashboard/components-next/button/Button.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import AttributeBadge from 'dashboard/components-next/CustomAttributes/AttributeBadge.vue';
import { computed } from 'vue';
const props = defineProps({
attribute: {
type: Object,
required: true,
},
badges: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['edit', 'delete']);
const iconByType = {
text: 'i-lucide-align-justify',
checkbox: 'i-lucide-circle-check-big',
list: 'i-lucide-list',
date: 'i-lucide-calendar',
link: 'i-lucide-link',
number: 'i-lucide-hash',
};
const attributeIcon = computed(() => {
const typeKey = props.attribute.type?.toLowerCase();
return iconByType[typeKey] || 'i-lucide-align-justify';
});
</script>
<template>
<div
class="flex flex-col gap-2 p-4 bg-n-solid-1 rounded-2xl outline outline-1 outline-n-container"
>
<div class="flex flex-wrap gap-2 justify-between items-center">
<div class="flex flex-wrap gap-2 items-center min-w-0">
<h4 class="text-sm font-medium truncate text-n-slate-12">
{{ attribute.label }}
</h4>
<div class="w-px h-3 bg-n-strong" />
<div class="flex gap-2 items-center text-sm text-n-slate-11">
<div class="flex items-center gap-1.5 text-n-slate-11">
<Icon :icon="attributeIcon" class="size-4" />
<span class="text-sm">{{ attribute.type }}</span>
</div>
<div class="w-px h-3 bg-n-weak" />
<div class="flex items-center gap-1.5 text-n-slate-11">
<Icon icon="i-lucide-key-round" class="size-4" />
<span class="line-clamp-1 text-sm">{{ attribute.value }}</span>
</div>
</div>
</div>
<div class="flex gap-2 items-center">
<AttributeBadge
v-for="badge in badges"
:key="badge.type"
:type="badge.type"
/>
<div
v-if="badges.length > 0"
class="w-px h-3 bg-n-strong ltr:ml-1.5 rtl:mr-1.5"
/>
<Button
icon="i-lucide-pencil-line"
size="sm"
color="slate"
ghost
@click="emit('edit', attribute)"
/>
<div class="w-px h-3 bg-n-strong" />
<Button
icon="i-lucide-trash"
size="sm"
color="slate"
ghost
@click="emit('delete', attribute)"
/>
</div>
</div>
<p class="mb-0 text-sm text-n-slate-11">
{{ attribute.attribute_description || attribute.description || '' }}
</p>
</div>
</template>
@@ -0,0 +1,42 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
type: {
type: String,
default: 'resolution',
validator: value => ['pre-chat', 'resolution'].includes(value),
},
});
const { t } = useI18n();
const attributeConfig = {
'pre-chat': {
colorClass: 'text-n-blue-11',
icon: 'i-lucide-message-circle',
labelKey: 'ATTRIBUTES_MGMT.BADGES.PRE_CHAT',
},
resolution: {
colorClass: 'text-n-teal-11',
icon: 'i-lucide-circle-check-big',
labelKey: 'ATTRIBUTES_MGMT.BADGES.RESOLUTION',
},
};
const config = computed(
() => attributeConfig[props.type] || attributeConfig.resolution
);
</script>
<template>
<div
class="flex gap-1 justify-center items-center px-1.5 py-1 rounded-md shadow outline-1 outline outline-n-container bg-n-solid-2"
>
<Icon :icon="config.icon" class="size-4" :class="config.colorClass" />
<span class="text-xs" :class="config.colorClass">{{
t(config.labelKey)
}}</span>
</div>
</template>
@@ -14,6 +14,7 @@ export function useChannelIcon(inbox) {
'Channel::Whatsapp': 'i-woot-whatsapp',
'Channel::Instagram': 'i-woot-instagram',
'Channel::Voice': 'i-woot-voice',
'Channel::Tiktok': 'i-woot-tiktok',
};
const providerIconMap = {
@@ -61,6 +61,12 @@ describe('useChannelIcon', () => {
expect(icon).toBe('i-woot-instagram');
});
it('returns correct icon for TikTok channel', () => {
const inbox = { channel_type: 'Channel::Tiktok' };
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-woot-tiktok');
});
describe('TwilioSms channel', () => {
it('returns chat icon for regular Twilio SMS channel', () => {
const inbox = { channel_type: 'Channel::TwilioSms' };
@@ -28,6 +28,7 @@ import ImageBubble from './bubbles/Image.vue';
import FileBubble from './bubbles/File.vue';
import AudioBubble from './bubbles/Audio.vue';
import VideoBubble from './bubbles/Video.vue';
import EmbedBubble from './bubbles/Embed.vue';
import InstagramStoryBubble from './bubbles/InstagramStory.vue';
import EmailBubble from './bubbles/Email/Index.vue';
import UnsupportedBubble from './bubbles/Unsupported.vue';
@@ -317,6 +318,7 @@ const componentToRender = computed(() => {
if (fileType === ATTACHMENT_TYPES.AUDIO) return AudioBubble;
if (fileType === ATTACHMENT_TYPES.VIDEO) return VideoBubble;
if (fileType === ATTACHMENT_TYPES.IG_REEL) return VideoBubble;
if (fileType === ATTACHMENT_TYPES.EMBED) return EmbedBubble;
if (fileType === ATTACHMENT_TYPES.LOCATION) return LocationBubble;
}
// Attachment content is the name of the contact
@@ -20,6 +20,7 @@ const {
isAWhatsAppChannel,
isAnEmailChannel,
isAnInstagramChannel,
isATiktokChannel,
} = useInbox();
const {
@@ -60,7 +61,8 @@ const isSent = computed(() => {
isAFacebookInbox.value ||
isASmsInbox.value ||
isATelegramChannel.value ||
isAnInstagramChannel.value
isAnInstagramChannel.value ||
isATiktokChannel.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
}
@@ -78,7 +80,8 @@ const isDelivered = computed(() => {
isAWhatsAppChannel.value ||
isATwilioChannel.value ||
isASmsInbox.value ||
isAFacebookInbox.value
isAFacebookInbox.value ||
isATiktokChannel.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED;
}
@@ -100,7 +103,8 @@ const isRead = computed(() => {
isAWhatsAppChannel.value ||
isATwilioChannel.value ||
isAFacebookInbox.value ||
isAnInstagramChannel.value
isAnInstagramChannel.value ||
isATiktokChannel.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.READ;
}
@@ -0,0 +1,30 @@
<script setup>
import { computed } from 'vue';
import BaseBubble from './Base.vue';
import { useMessageContext } from '../provider.js';
import { useI18n } from 'vue-i18n';
const { attachments } = useMessageContext();
const { t } = useI18n();
const attachment = computed(() => {
return attachments.value[0];
});
</script>
<template>
<BaseBubble class="overflow-hidden p-3" data-bubble-name="embed">
<div
class="w-full max-w-[360px] sm:max-w-[420px] min-h-[520px] h-[70vh] max-h-[680px]"
>
<iframe
class="w-full h-full border-0 rounded-lg"
:title="t('CHAT_LIST.ATTACHMENTS.embed.CONTENT')"
:src="attachment.dataUrl"
loading="lazy"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen
/>
</div>
</BaseBubble>
</template>
@@ -49,6 +49,7 @@ export const ATTACHMENT_TYPES = {
STORY_MENTION: 'story_mention',
CONTACT: 'contact',
IG_REEL: 'ig_reel',
EMBED: 'embed',
IG_POST: 'ig_post',
IG_STORY: 'ig_story',
};