chore: Replace Thumbnail with Avatar (#12119)

This commit is contained in:
Sivin Varghese
2025-08-11 15:47:17 +05:30
committed by GitHub
parent fcc6e2b8b2
commit d908c880d2
38 changed files with 297 additions and 657 deletions

View File

@@ -76,8 +76,8 @@ const campaignStatus = computed(() => {
const inboxName = computed(() => props.inbox?.name || ''); const inboxName = computed(() => props.inbox?.name || '');
const inboxIcon = computed(() => { const inboxIcon = computed(() => {
const { phone_number: phoneNumber, channel_type: type } = props.inbox; const { medium, channel_type: type } = props.inbox;
return getInboxIconByType(type, phoneNumber); return getInboxIconByType(type, medium);
}); });
</script> </script>

View File

@@ -48,8 +48,8 @@ const inbox = computed(() => props.stateInbox);
const inboxName = computed(() => inbox.value?.name); const inboxName = computed(() => inbox.value?.name);
const inboxIcon = computed(() => { const inboxIcon = computed(() => {
const { phoneNumber, channelType } = inbox.value; const { channelType, medium } = inbox.value;
return getInboxIconByType(channelType, phoneNumber); return getInboxIconByType(channelType, medium);
}); });
const lastActivityAt = computed(() => { const lastActivityAt = computed(() => {

View File

@@ -49,8 +49,8 @@ const isUnread = computed(() => !props.inboxItem?.readAt);
const inbox = computed(() => props.stateInbox); const inbox = computed(() => props.stateInbox);
const inboxIcon = computed(() => { const inboxIcon = computed(() => {
const { phoneNumber, channelType } = inbox.value; const { channelType, medium } = inbox.value;
return getInboxIconByType(channelType, phoneNumber); return getInboxIconByType(channelType, medium);
}); });
const hasSlaThreshold = computed(() => { const hasSlaThreshold = computed(() => {

View File

@@ -36,10 +36,11 @@ const transformInbox = ({
email, email,
channelType, channelType,
phoneNumber, phoneNumber,
medium,
...rest ...rest
}) => ({ }) => ({
id, id,
icon: getInboxIconByType(channelType, phoneNumber, 'line'), icon: getInboxIconByType(channelType, medium, 'line'),
label: generateLabelForContactableInboxesList({ label: generateLabelForContactableInboxesList({
name, name,
email, email,

View File

@@ -183,7 +183,10 @@ watch(
</script> </script>
<template> <template>
<span class="relative inline-flex group/avatar z-0" :style="containerStyles"> <span
class="relative inline-flex group/avatar z-0 flex-shrink-0"
:style="containerStyles"
>
<!-- Status Badge --> <!-- Status Badge -->
<slot name="badge" :size="size"> <slot name="badge" :size="size">
<div <div

View File

@@ -57,9 +57,10 @@ const menuItems = computed(() => [
}, },
]); ]);
const icon = computed(() => const icon = computed(() => {
getInboxIconByType(props.inbox.channel_type, '', 'outline') const { medium, channel_type: type } = props.inbox;
); return getInboxIconByType(type, medium, 'outline');
});
const handleAction = ({ action, value }) => { const handleAction = ({ action, value }) => {
toggleDropdown(false); toggleDropdown(false);

View File

@@ -1,6 +1,5 @@
// [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file. // [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file.
/* eslint no-plusplus: 0 */ /* eslint no-plusplus: 0 */
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
import Code from './Code.vue'; import Code from './Code.vue';
import ColorPicker from './widgets/ColorPicker.vue'; import ColorPicker from './widgets/ColorPicker.vue';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
@@ -18,11 +17,9 @@ import Modal from './Modal.vue';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import Tabs from './ui/Tabs/Tabs.vue'; import Tabs from './ui/Tabs/Tabs.vue';
import TabsItem from './ui/Tabs/TabsItem.vue'; import TabsItem from './ui/Tabs/TabsItem.vue';
import Thumbnail from './widgets/Thumbnail.vue';
import DatePicker from './ui/DatePicker/DatePicker.vue'; import DatePicker from './ui/DatePicker/DatePicker.vue';
const WootUIKit = { const WootUIKit = {
AvatarUploader,
Code, Code,
ColorPicker, ColorPicker,
ConfirmDeleteModal, ConfirmDeleteModal,
@@ -40,7 +37,6 @@ const WootUIKit = {
Spinner, Spinner,
Tabs, Tabs,
TabsItem, TabsItem,
Thumbnail,
DatePicker, DatePicker,
install(Vue) { install(Vue) {
const keys = Object.keys(this); const keys = Object.keys(this);

View File

@@ -1,55 +0,0 @@
<script>
export default {
name: 'Avatar',
props: {
username: {
type: String,
default: '',
},
size: {
type: Number,
default: 40,
},
},
computed: {
style() {
return {
fontSize: `${Math.floor(this.size / 2.5)}px`,
};
},
userInitial() {
const parts = this.username.split(/[ -]/);
let initials = parts.reduce((acc, curr) => acc + curr.charAt(0), '');
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
initials = initials.replace(/[a-z]+/g, '');
}
initials = initials.substring(0, 2).toUpperCase();
return initials;
},
},
};
</script>
<template>
<div class="avatar-container" :style="style" aria-hidden="true">
<slot>{{ userInitial }}</slot>
</div>
</template>
<style scoped>
@tailwind components;
@layer components {
.avatar-color {
background-image: linear-gradient(to top, #c2e1ff 0%, #d6ebff 100%);
}
.dark-avatar-color {
background-image: linear-gradient(to top, #135899 0%, #135899 100%);
}
}
.avatar-container {
@apply flex leading-[100%] font-medium items-center justify-center text-center cursor-default avatar-color dark:dark-avatar-color text-n-blue-text;
}
</style>

View File

@@ -1,48 +0,0 @@
import { mount } from '@vue/test-utils';
import Avatar from './Avatar.vue';
import Thumbnail from './Thumbnail.vue';
describe('Thumbnail.vue', () => {
it('should render the agent thumbnail if valid image is passed', () => {
const wrapper = mount(Thumbnail, {
propsData: {
src: 'https://some_valid_url.com',
},
data() {
return {
hasImageLoaded: true,
imgError: false,
};
},
});
expect(wrapper.find('.user-thumbnail').exists()).toBe(true);
const avatarComponent = wrapper.findComponent(Avatar);
expect(avatarComponent.isVisible()).toBe(false);
});
it('should render the avatar component if invalid image is passed', () => {
const wrapper = mount(Thumbnail, {
propsData: {
src: 'https://some_invalid_url.com',
},
data() {
return {
hasImageLoaded: true,
imgError: true,
};
},
});
expect(wrapper.find('#image').exists()).toBe(false);
const avatarComponent = wrapper.findComponent(Avatar);
expect(avatarComponent.isVisible()).toBe(true);
});
it('should the initial of the name if no image is passed', () => {
const wrapper = mount(Avatar, {
propsData: {
username: 'Angie Rojas',
},
});
expect(wrapper.find('div').text()).toBe('AR');
});
});

View File

@@ -1,224 +0,0 @@
<script>
/**
* Thumbnail Component
* Src - source for round image
* Size - Size of the thumbnail
* Badge - Chat source indication { fb / telegram }
* Username - Username for avatar
*/
import Avatar from './Avatar.vue';
import { removeEmoji } from 'shared/helpers/emoji';
export default {
components: {
Avatar,
},
props: {
src: {
type: String,
default: '',
},
size: {
type: String,
default: '40px',
},
badge: {
type: String,
default: '',
},
username: {
type: String,
default: '',
},
status: {
type: String,
default: '',
},
hasBorder: {
type: Boolean,
default: false,
},
shouldShowStatusAlways: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
variant: {
type: String,
default: 'circle',
},
},
data() {
return {
hasImageLoaded: false,
imgError: false,
};
},
computed: {
userNameWithoutEmoji() {
return removeEmoji(this.username);
},
showStatusIndicator() {
if (this.shouldShowStatusAlways) return true;
return this.status === 'online' || this.status === 'busy';
},
avatarSize() {
return Number(this.size.replace(/\D+/g, ''));
},
badgeSrc() {
return {
instagram_direct_message: 'instagram-dm',
facebook: 'messenger',
'twitter-tweet': 'twitter-tweet',
'twitter-dm': 'twitter-dm',
whatsapp: 'whatsapp',
sms: 'sms',
'Channel::Line': 'line',
'Channel::Telegram': 'telegram',
'Channel::WebWidget': '',
}[this.badge];
},
badgeStyle() {
const size = Math.floor(this.avatarSize / 3);
const badgeSize = `${size + 2}px`;
const borderRadius = `${size / 2}px`;
return { width: badgeSize, height: badgeSize, borderRadius };
},
statusStyle() {
const statusSize = `${this.avatarSize / 4}px`;
return { width: statusSize, height: statusSize };
},
thumbnailClass() {
const className = this.hasBorder
? 'border border-solid border-white dark:border-n-weak'
: '';
const variant =
this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square';
return `user-thumbnail ${className} ${variant}`;
},
thumbnailBoxClass() {
const boxClass = this.variant === 'circle' ? 'is-rounded' : '';
return `user-thumbnail-box ${boxClass}`;
},
shouldShowImage() {
if (!this.src) {
return false;
}
if (this.hasImageLoaded) {
return !this.imgError;
}
return false;
},
},
watch: {
src(value, oldValue) {
if (value !== oldValue && this.imgError) {
this.imgError = false;
}
},
},
methods: {
onImgError() {
this.imgError = true;
},
onImgLoad() {
this.hasImageLoaded = true;
},
},
};
</script>
<template>
<div
:class="thumbnailBoxClass"
:style="{ height: size, width: size }"
:title="title"
>
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
<slot>
<img
v-show="shouldShowImage"
:src="src"
draggable="false"
:class="thumbnailClass"
@load="onImgLoad"
@error="onImgError"
/>
<Avatar
v-show="!shouldShowImage"
:username="userNameWithoutEmoji"
:class="thumbnailClass"
:size="avatarSize"
/>
</slot>
<img
v-if="badgeSrc"
class="source-badge z-20"
:style="badgeStyle"
:src="`/integrations/channels/badges/${badgeSrc}.png`"
alt="Badge"
/>
<div
v-if="showStatusIndicator"
class="z-20"
:class="`source-badge user-online-status user-online-status--${status}`"
:style="statusStyle"
/>
</div>
</template>
<style lang="scss" scoped>
.user-thumbnail-box {
flex: 0 0 auto;
max-width: 100%;
position: relative;
&.is-rounded {
border-radius: 50%;
}
.user-thumbnail {
border-radius: 50%;
&.thumbnail-square {
border-radius: 0.5625rem;
}
height: 100%;
width: 100%;
box-sizing: border-box;
object-fit: cover;
vertical-align: initial;
}
.source-badge {
border-radius: 0.1875rem;
bottom: -0.125rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
position: absolute;
right: 0;
@apply bg-n-background p-0.5 size-3;
}
.user-online-status {
@apply bottom-0.5 rounded-full;
&:after {
content: ' ';
}
}
.user-online-status--online {
@apply bg-n-teal-10;
}
.user-online-status--busy {
@apply bg-n-amber-10;
}
.user-online-status--offline {
@apply bg-n-slate-10;
}
}
</style>

View File

@@ -1,78 +1,69 @@
<script> <script setup>
import Thumbnail from './Thumbnail.vue'; import { computed } from 'vue';
import Avatar from 'next/avatar/Avatar.vue';
export default { const props = defineProps({
components: { usersList: {
Thumbnail, type: Array,
default: () => [],
}, },
props: { size: {
usersList: { type: Number,
type: Array, default: 24,
default: () => [], },
}, showMoreThumbnailsCount: {
size: { type: Boolean,
type: String, default: false,
default: '24px', },
}, moreThumbnailsText: {
showMoreThumbnailsCount: { type: String,
type: Boolean, default: '',
default: false, },
}, gap: {
moreThumbnailsText: { type: String,
type: String, default: 'normal',
default: '', validator(value) {
}, // The value must match one of these strings
gap: { return ['normal', 'tight'].includes(value);
type: String,
default: 'normal',
validator(value) {
// The value must match one of these strings
return ['normal', '', 'tight'].includes(value);
},
}, },
}, },
}; });
const gapClass = computed(() => {
if (props.gap === 'tight') {
return 'ltr:[&:not(:first-child)]:-ml-2 rtl:[&:not(:first-child)]:-mr-2';
}
return 'ltr:[&:not(:first-child)]:-ml-1 rtl:[&:not(:first-child)]:-mr-1';
});
const moreThumbnailsClass = computed(() => {
if (props.gap === 'tight') {
return 'ltr:-ml-2 rtl:-mr-2';
}
return 'ltr:-ml-1 rtl:-mr-1';
});
</script> </script>
<template> <template>
<div class="overlapping-thumbnails"> <div class="flex">
<Thumbnail <Avatar
v-for="user in usersList" v-for="user in usersList"
:key="user.id" :key="user.id"
v-tooltip="user.name" v-tooltip="user.name"
:title="user.name" :title="user.name"
:src="user.thumbnail" :src="user.thumbnail"
:username="user.name" :name="user.name"
has-border
:size="size" :size="size"
:class="`overlapping-thumbnail gap-${gap}`" class="[&>span]:outline [&>span]:outline-1 [&>span]:outline-n-background [&>span]:shadow"
:class="gapClass"
rounded-full
/> />
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text"> <span
v-if="showMoreThumbnailsCount"
class="text-n-slate-11 bg-n-slate-4 outline outline-1 outline-n-background text-xs font-medium rounded-full px-2 inline-flex items-center shadow relative"
:class="moreThumbnailsClass"
>
{{ moreThumbnailsText }} {{ moreThumbnailsText }}
</span> </span>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.overlapping-thumbnails {
display: flex;
}
.overlapping-thumbnail {
position: relative;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
&:not(:first-child) {
margin-left: -0.25rem;
}
.gap-tight {
margin-left: -0.5rem;
}
}
.thumbnail-more-text {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
@apply text-n-slate-11 bg-n-slate-4 border border-n-weak text-xs font-medium rounded-full px-2 ltr:-ml-2 rtl:-mr-2 inline-flex items-center relative;
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
defineProps({ defineProps({
user: { user: {
@@ -7,8 +7,8 @@ defineProps({
default: () => ({}), default: () => ({}),
}, },
size: { size: {
type: String, type: Number,
default: '20px', default: 20,
}, },
textClass: { textClass: {
type: String, type: String,
@@ -19,11 +19,13 @@ defineProps({
<template> <template>
<div class="flex items-center gap-1.5 text-left"> <div class="flex items-center gap-1.5 text-left">
<Thumbnail <Avatar
:src="user.thumbnail" :src="user.thumbnail"
:size="size" :size="size"
:username="user.name" :name="user.name"
:status="user.availability_status" :status="user.availability_status"
hide-offline-status
rounded-full
/> />
<span class="my-0 truncate text-capitalize" :class="textClass"> <span class="my-0 truncate text-capitalize" :class="textClass">
{{ user.name }} {{ user.name }}

View File

@@ -6,7 +6,7 @@ import { useElementSize } from '@vueuse/core';
import BackButton from '../BackButton.vue'; import BackButton from '../BackButton.vue';
import InboxName from '../InboxName.vue'; import InboxName from '../InboxName.vue';
import MoreActions from './MoreActions.vue'; import MoreActions from './MoreActions.vue';
import Thumbnail from '../Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import SLACardLabel from './components/SLACardLabel.vue'; import SLACardLabel from './components/SLACardLabel.vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { conversationListPageURL } from 'dashboard/helper/URLHelper'; import { conversationListPageURL } from 'dashboard/helper/URLHelper';
@@ -105,12 +105,13 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
:back-url="backButtonUrl" :back-url="backButtonUrl"
class="ltr:mr-2 rtl:ml-2" class="ltr:mr-2 rtl:ml-2"
/> />
<Thumbnail <Avatar
:name="currentContact.name"
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
:username="currentContact.name" :size="32"
:status="currentContact.availability_status" :status="currentContact.availability_status"
size="32px" hide-offline-status
class="flex-shrink-0" rounded-full
/> />
<div <div
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2" class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2"

View File

@@ -10,7 +10,7 @@ import { messageTimestamp } from 'shared/helpers/timeHelper';
import { downloadFile } from '@chatwoot/utils'; import { downloadFile } from '@chatwoot/utils';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue'; import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
const props = defineProps({ const props = defineProps({
@@ -186,10 +186,12 @@ onMounted(() => {
v-if="senderDetails" v-if="senderDetails"
class="flex items-center min-w-[15rem] shrink-0" class="flex items-center min-w-[15rem] shrink-0"
> >
<Thumbnail <Avatar
v-if="senderDetails.avatar" v-if="senderDetails.avatar"
:username="senderDetails.name" :name="senderDetails.name"
:src="senderDetails.avatar" :src="senderDetails.avatar"
:size="40"
rounded-full
class="flex-shrink-0" class="flex-shrink-0"
/> />
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden"> <div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">

View File

@@ -1,7 +1,7 @@
<script> <script>
// components // components
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import Avatar from '../../Avatar.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
// composables // composables
import { useAI } from 'dashboard/composables/useAI'; import { useAI } from 'dashboard/composables/useAI';
@@ -226,18 +226,16 @@ export default {
</div> </div>
</div> </div>
<div class="sender--info has-tooltip" data-original-title="null"> <div class="sender--info has-tooltip" data-original-title="null">
<woot-thumbnail <Avatar
v-tooltip.top="{ v-tooltip.top="{
content: $t('LABEL_MGMT.SUGGESTIONS.POWERED_BY'), content: $t('LABEL_MGMT.SUGGESTIONS.POWERED_BY'),
delay: { show: 600, hide: 0 }, delay: { show: 600, hide: 0 },
hideOnClick: true, hideOnClick: true,
}" }"
size="16px" :size="16"
> name="chatwoot-ai"
<Avatar class="user-thumbnail thumbnail-rounded"> icon-name="i-lucide-sparkles"
<fluent-icon class="chatwoot-ai-icon" icon="chatwoot-ai" /> />
</Avatar>
</woot-thumbnail>
</div> </div>
</div> </div>
</li> </li>
@@ -268,11 +266,6 @@ export default {
} }
} }
.chatwoot-ai-icon {
height: 0.75rem;
width: 0.75rem;
}
.label-suggestion--title { .label-suggestion--title {
@apply text-n-slate-11 mt-0.5 text-xxs; @apply text-n-slate-11 mt-0.5 text-xxs;
} }

View File

@@ -1,12 +1,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Thumbnail, Avatar,
Spinner, Spinner,
NextButton, NextButton,
}, },
@@ -123,11 +123,13 @@ export default {
</li> </li>
<li v-for="agent in filteredAgents" :key="agent.id"> <li v-for="agent in filteredAgents" :key="agent.id">
<div class="agent-list-item" @click="assignAgent(agent)"> <div class="agent-list-item" @click="assignAgent(agent)">
<Thumbnail <Avatar
:name="agent.name"
:src="agent.thumbnail" :src="agent.thumbnail"
:status="agent.availability_status" :status="agent.availability_status"
:username="agent.name" :size="22"
size="22px" hide-offline-status
rounded-full
/> />
<span class="my-0 text-n-slate-12"> <span class="my-0 text-n-slate-12">
{{ agent.name }} {{ agent.name }}

View File

@@ -1,76 +0,0 @@
<script>
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
label: {
type: String,
default: '',
},
src: {
type: String,
default: '',
},
usernameAvatar: {
type: String,
default: '',
},
deleteAvatar: {
type: Boolean,
default: false,
},
},
emits: ['onAvatarSelect', 'onAvatarDelete'],
watch: {},
methods: {
handleImageUpload(event) {
const [file] = event.target.files;
this.$emit('onAvatarSelect', {
file,
url: file ? URL.createObjectURL(file) : null,
});
},
onAvatarDelete() {
this.$refs.file.value = null;
this.$emit('onAvatarDelete');
},
},
};
</script>
<template>
<div>
<label>
<span v-if="label">{{ label }}</span>
</label>
<woot-thumbnail
v-if="src"
size="80px"
:src="src"
:username="usernameAvatar"
/>
<div v-if="src && deleteAvatar" class="my-1">
<NextButton
outline
xs
ruby
:label="$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT')"
@click="onAvatarDelete"
/>
</div>
<label>
<input
id="file"
ref="file"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
@change="handleImageUpload"
/>
<slot />
</label>
</div>
</template>

View File

@@ -139,14 +139,14 @@ export const getInboxClassByType = (type, phoneNumber) => {
} }
}; };
export const getInboxIconByType = (type, phoneNumber, variant = 'fill') => { export const getInboxIconByType = (type, medium, variant = 'fill') => {
const iconMap = const iconMap =
variant === 'fill' ? INBOX_ICON_MAP_FILL : INBOX_ICON_MAP_LINE; variant === 'fill' ? INBOX_ICON_MAP_FILL : INBOX_ICON_MAP_LINE;
const defaultIcon = const defaultIcon =
variant === 'fill' ? DEFAULT_ICON_FILL : DEFAULT_ICON_LINE; variant === 'fill' ? DEFAULT_ICON_FILL : DEFAULT_ICON_LINE;
// Special case for Twilio (whatsapp and sms) // Special case for Twilio (whatsapp and sms)
if (type === INBOX_TYPES.TWILIO && phoneNumber?.startsWith('whatsapp')) { if (type === INBOX_TYPES.TWILIO && medium === 'whatsapp') {
return iconMap[INBOX_TYPES.WHATSAPP]; return iconMap[INBOX_TYPES.WHATSAPP];
} }

View File

@@ -112,13 +112,13 @@ describe('#Inbox Helpers', () => {
describe('Twilio cases', () => { describe('Twilio cases', () => {
describe('fill variant', () => { describe('fill variant', () => {
it('returns WhatsApp icon for Twilio WhatsApp number', () => { it('returns WhatsApp icon for Twilio WhatsApp number', () => {
expect( expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp')).toBe(
getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp:+1234567890') 'i-ri-whatsapp-fill'
).toBe('i-ri-whatsapp-fill'); );
}); });
it('returns SMS icon for regular Twilio number', () => { it('returns SMS icon for regular Twilio number', () => {
expect(getInboxIconByType(INBOX_TYPES.TWILIO, '+1234567890')).toBe( expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'sms')).toBe(
'i-ri-chat-1-fill' 'i-ri-chat-1-fill'
); );
}); });
@@ -133,18 +133,14 @@ describe('#Inbox Helpers', () => {
describe('line variant', () => { describe('line variant', () => {
it('returns WhatsApp line icon for Twilio WhatsApp number', () => { it('returns WhatsApp line icon for Twilio WhatsApp number', () => {
expect( expect(
getInboxIconByType( getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp', 'line')
INBOX_TYPES.TWILIO,
'whatsapp:+1234567890',
'line'
)
).toBe('i-ri-whatsapp-line'); ).toBe('i-ri-whatsapp-line');
}); });
it('returns SMS line icon for regular Twilio number', () => { it('returns SMS line icon for regular Twilio number', () => {
expect( expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'sms', 'line')).toBe(
getInboxIconByType(INBOX_TYPES.TWILIO, '+1234567890', 'line') 'i-ri-chat-1-line'
).toBe('i-ri-chat-1-line'); );
}); });
}); });
}); });

View File

@@ -1,38 +1,33 @@
<script> <script setup>
import Thumbnail from '../../../components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
export default { defineProps({
components: { name: {
Thumbnail, type: String,
default: '',
}, },
props: { thumbnail: {
name: { type: String,
type: String, default: '',
default: '',
},
thumbnail: {
type: String,
default: '',
},
email: {
type: String,
default: '',
},
phoneNumber: {
type: String,
default: '',
},
identifier: {
type: [String, Number],
required: true,
},
}, },
}; email: {
type: String,
default: '',
},
phoneNumber: {
type: String,
default: '',
},
identifier: {
type: [String, Number],
required: true,
},
});
</script> </script>
<template> <template>
<div class="option-item--user"> <div class="option-item--user">
<Thumbnail :src="thumbnail" size="28px" :username="name" /> <Avatar :src="thumbnail" :size="28" :name="name" rounded-full />
<div class="option__user-data"> <div class="option__user-data">
<h5 class="option__title"> <h5 class="option__title">
{{ name }} {{ name }}

View File

@@ -2,7 +2,7 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue'; import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import FluentIcon from 'shared/components/FluentIcon/Index.vue';
const props = defineProps({ const props = defineProps({
@@ -40,7 +40,7 @@ const getStatusText = computed(() => {
{{ config.replyTime }} {{ config.replyTime }}
</div> </div>
</div> </div>
<Thumbnail username="C" size="34px" /> <Avatar name="C" :size="34" rounded-full />
</div> </div>
<button <button
v-if="config.isDefaultScreen" v-if="config.isDefaultScreen"

View File

@@ -10,10 +10,12 @@ import countries from 'shared/constants/countries.js';
import { isPhoneNumberValid } from 'shared/helpers/Validators'; import { isPhoneNumberValid } from 'shared/helpers/Validators';
import parsePhoneNumber from 'libphonenumber-js'; import parsePhoneNumber from 'libphonenumber-js';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import Avatar from 'next/avatar/Avatar.vue';
export default { export default {
components: { components: {
NextButton, NextButton,
Avatar,
}, },
props: { props: {
contact: { contact: {
@@ -274,18 +276,19 @@ export default {
class="w-full px-8 pt-6 pb-8 contact--form" class="w-full px-8 pt-6 pb-8 contact--form"
@submit.prevent="handleSubmit" @submit.prevent="handleSubmit"
> >
<div> <div class="flex flex-col mb-4 items-start gap-1 w-full">
<div class="w-full"> <label class="mb-0.5 text-sm font-medium text-n-slate-12">
<woot-avatar-uploader {{ $t('CONTACT_FORM.FORM.AVATAR.LABEL') }}
:label="$t('CONTACT_FORM.FORM.AVATAR.LABEL')" </label>
:src="avatarUrl" <Avatar
:username-avatar="name" :src="avatarUrl"
:delete-avatar="!!avatarUrl" :size="72"
class="settings-item" :name="contact.name"
@on-avatar-select="handleImageUpload" allow-upload
@on-avatar-delete="handleAvatarDelete" rounded-full
/> @upload="handleImageUpload"
</div> @delete="handleAvatarDelete"
/>
</div> </div>
<div> <div>
<div class="w-full"> <div class="w-full">

View File

@@ -4,7 +4,7 @@ import { useAlert } from 'dashboard/composables';
import { dynamicTime } from 'shared/helpers/timeHelper'; import { dynamicTime } from 'shared/helpers/timeHelper';
import { useAdmin } from 'dashboard/composables/useAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
import ContactInfoRow from './ContactInfoRow.vue'; import ContactInfoRow from './ContactInfoRow.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import SocialIcons from './SocialIcons.vue'; import SocialIcons from './SocialIcons.vue';
import EditContact from './EditContact.vue'; import EditContact from './EditContact.vue';
import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue'; import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue';
@@ -24,7 +24,7 @@ export default {
NextButton, NextButton,
ContactInfoRow, ContactInfoRow,
EditContact, EditContact,
Thumbnail, Avatar,
ComposeConversation, ComposeConversation,
SocialIcons, SocialIcons,
ContactMergeModal, ContactMergeModal,
@@ -179,12 +179,14 @@ export default {
<div class="relative items-center w-full p-4"> <div class="relative items-center w-full p-4">
<div class="flex flex-col w-full gap-2 text-left rtl:text-right"> <div class="flex flex-col w-full gap-2 text-left rtl:text-right">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<Thumbnail <Avatar
v-if="showAvatar" v-if="showAvatar"
:src="contact.thumbnail" :src="contact.thumbnail"
size="48px" :name="contact.name"
:username="contact.name"
:status="contact.availability_status" :status="contact.availability_status"
:size="48"
hide-offline-status
rounded-full
/> />
</div> </div>

View File

@@ -1,5 +1,5 @@
<script> <script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue'; import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import { dynamicTime } from 'shared/helpers/timeHelper'; import { dynamicTime } from 'shared/helpers/timeHelper';
@@ -8,7 +8,7 @@ import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Thumbnail, Avatar,
Spinner, Spinner,
EmptyState, EmptyState,
NextButton, NextButton,
@@ -107,11 +107,12 @@ export default {
</span> </span>
</td> </td>
<td class="thumbnail--column"> <td class="thumbnail--column">
<Thumbnail <Avatar
v-if="notificationItem.primary_actor.meta.assignee" v-if="notificationItem.primary_actor.meta.assignee"
:src="notificationItem.primary_actor.meta.assignee.thumbnail" :src="notificationItem.primary_actor.meta.assignee.thumbnail"
size="28px" :size="28"
:username="notificationItem.primary_actor.meta.assignee.name" :name="notificationItem.primary_actor.meta.assignee.name"
rounded-full
/> />
</td> </td>
<td> <td>

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { import {
useStoreGetters, useStoreGetters,
@@ -164,11 +164,13 @@ const confirmDeletion = () => {
<tr v-for="(agent, index) in agentList" :key="agent.email"> <tr v-for="(agent, index) in agentList" :key="agent.email">
<td class="py-4 ltr:pr-4 rtl:pl-4"> <td class="py-4 ltr:pr-4 rtl:pl-4">
<div class="flex flex-row items-center gap-4"> <div class="flex flex-row items-center gap-4">
<Thumbnail <Avatar
:src="agent.thumbnail" :src="agent.thumbnail"
:username="agent.name" :name="agent.name"
size="40px"
:status="agent.availability_status" :status="agent.availability_status"
:size="40"
hide-offline-status
rounded-full
/> />
<div> <div>
<span class="block font-medium capitalize"> <span class="block font-medium capitalize">

View File

@@ -2,7 +2,7 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import { useAdmin } from 'dashboard/composables/useAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
import SettingsLayout from '../SettingsLayout.vue'; import SettingsLayout from '../SettingsLayout.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue'; import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
@@ -101,16 +101,20 @@ const openDelete = inbox => {
<tr v-for="inbox in inboxesList" :key="inbox.id"> <tr v-for="inbox in inboxesList" :key="inbox.id">
<td class="py-4 ltr:pr-4 rtl:pl-4"> <td class="py-4 ltr:pr-4 rtl:pl-4">
<div class="flex items-center flex-row gap-4"> <div class="flex items-center flex-row gap-4">
<Thumbnail <div
v-if="inbox.avatar_url" v-if="inbox.avatar_url"
class="bg-n-alpha-3 rounded-full p-2 ring ring-n-solid-1 border border-n-strong shadow-sm" class="bg-n-alpha-3 rounded-full size-12 p-2 ring ring-n-solid-1 border border-n-strong shadow-sm"
:src="inbox.avatar_url" >
:username="inbox.name" <Avatar
size="48px" :src="inbox.avatar_url"
/> :name="inbox.name"
:size="30"
rounded-full
/>
</div>
<div <div
v-else v-else
class="w-[48px] h-[48px] flex justify-center items-center bg-n-alpha-3 rounded-full p-2 ring ring-n-solid-1 border border-n-strong shadow-sm" class="size-12 flex justify-center items-center bg-n-alpha-3 rounded-full p-2 ring ring-n-solid-1 border border-n-strong shadow-sm"
> >
<ChannelIcon class="size-5" :inbox="inbox" /> <ChannelIcon class="size-5" :inbox="inbox" />
</div> </div>

View File

@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
import { shouldBeUrl } from 'shared/helpers/Validators'; import { shouldBeUrl } from 'shared/helpers/Validators';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import Avatar from 'next/avatar/Avatar.vue';
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.vue'; import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.vue';
import SettingsSection from '../../../../components/SettingsSection.vue'; import SettingsSection from '../../../../components/SettingsSection.vue';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
@@ -25,6 +26,7 @@ import SenderNameExamplePreview from './components/SenderNameExamplePreview.vue'
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import { INBOX_TYPES } from 'dashboard/helper/inbox'; import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor'; import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import { getInboxIconByType } from 'dashboard/helper/inbox';
import Editor from 'dashboard/components-next/Editor/Editor.vue'; import Editor from 'dashboard/components-next/Editor/Editor.vue';
export default { export default {
@@ -48,6 +50,7 @@ export default {
WhatsappReauthorize, WhatsappReauthorize,
DuplicateInboxBanner, DuplicateInboxBanner,
Editor, Editor,
Avatar,
}, },
mixins: [inboxMixin], mixins: [inboxMixin],
setup() { setup() {
@@ -173,7 +176,10 @@ export default {
inbox() { inbox() {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId); return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
}, },
inboxIcon() {
const { medium, channel_type: type } = this.inbox;
return getInboxIconByType(type, medium);
},
inboxName() { inboxName() {
if (this.isATwilioSMSChannel || this.isATwilioWhatsAppChannel) { if (this.isATwilioSMSChannel || this.isATwilioWhatsAppChannel) {
return `${this.inbox.name} (${ return `${this.inbox.name} (${
@@ -283,8 +289,17 @@ export default {
} }
return [...selected, current]; return [...selected, current];
}, },
refreshAvatarUrlOnTabChange(index) {
// Refresh avatar URL on tab change from inbox_settings and widgetBuilder tabs, to ensure real-time updates
if (
this.inbox &&
['inbox_settings', 'widgetBuilder'].includes(this.tabs[index].key)
)
this.avatarUrl = this.inbox.avatar_url;
},
onTabChange(selectedTabIndex) { onTabChange(selectedTabIndex) {
this.selectedTabIndex = selectedTabIndex; this.selectedTabIndex = selectedTabIndex;
this.refreshAvatarUrlOnTabChange(selectedTabIndex);
}, },
fetchInboxSettings() { fetchInboxSettings() {
this.selectedTabIndex = 0; this.selectedTabIndex = 0;
@@ -435,14 +450,21 @@ export default {
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_SUB_TEXT')" :sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_SUB_TEXT')"
:show-border="false" :show-border="false"
> >
<woot-avatar-uploader <div class="flex flex-col mb-4 items-start gap-1">
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AVATAR.LABEL')" <label class="mb-0.5 text-sm font-medium text-n-slate-12">
:src="avatarUrl" {{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AVATAR.LABEL') }}
class="pb-4" </label>
delete-avatar <Avatar
@on-avatar-select="handleImageUpload" :src="avatarUrl"
@on-avatar-delete="handleAvatarDelete" :size="72"
/> :icon-name="inboxIcon"
name=""
allow-upload
rounded-full
@upload="handleImageUpload"
@delete="handleAvatarDelete"
/>
</div>
<woot-input <woot-input
v-model="selectedInboxName" v-model="selectedInboxName"
class="pb-4" class="pb-4"

View File

@@ -9,6 +9,7 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor'; import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import Avatar from 'next/avatar/Avatar.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue'; import Editor from 'dashboard/components-next/Editor/Editor.vue';
export default { export default {
@@ -17,6 +18,7 @@ export default {
InputRadioGroup, InputRadioGroup,
NextButton, NextButton,
Editor, Editor,
Avatar,
}, },
props: { props: {
inbox: { inbox: {
@@ -276,15 +278,23 @@ export default {
<div class="w-100 lg:w-[40%]"> <div class="w-100 lg:w-[40%]">
<div class="min-h-full py-4 overflow-y-scroll px-px"> <div class="min-h-full py-4 overflow-y-scroll px-px">
<form @submit.prevent="updateWidget"> <form @submit.prevent="updateWidget">
<woot-avatar-uploader <div class="flex flex-col mb-4 items-start gap-1 w-full">
:label=" <label class="mb-0.5 text-sm font-medium text-n-slate-12">
$t('INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.AVATAR.LABEL') {{
" $t('INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.AVATAR.LABEL')
:src="avatarUrl" }}
delete-avatar </label>
@on-avatar-select="handleImageUpload" <Avatar
@on-avatar-delete="handleAvatarDelete" :src="avatarUrl"
/> :size="72"
icon-name="i-ri-global-fill"
name=""
allow-upload
rounded-full
@upload="handleImageUpload"
@delete="handleAvatarDelete"
/>
</div>
<woot-input <woot-input
v-model="websiteName" v-model="websiteName"
:class="{ error: v$.websiteName.$error }" :class="{ error: v$.websiteName.$error }"

View File

@@ -1,11 +1,11 @@
<script> <script>
import PreviewCard from 'dashboard/components/ui/PreviewCard.vue'; import PreviewCard from 'dashboard/components/ui/PreviewCard.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
export default { export default {
components: { components: {
PreviewCard, PreviewCard,
Thumbnail, Avatar,
}, },
props: { props: {
senderNameType: { senderNameType: {
@@ -45,7 +45,7 @@ export default {
), ),
preview: { preview: {
senderName: '', senderName: '',
businessName: 'Chatwoot', businessName: 'Chatwoot ',
email: '<support@yourbusiness.com>', email: '<support@yourbusiness.com>',
}, },
}, },
@@ -86,7 +86,7 @@ export default {
{{ $t('INBOX_MGMT.EDIT.SENDER_NAME_SECTION.FOR_EG') }} {{ $t('INBOX_MGMT.EDIT.SENDER_NAME_SECTION.FOR_EG') }}
</span> </span>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Thumbnail :username="userName(keyOption)" size="32px" /> <Avatar :name="userName(keyOption)" :size="32" rounded-full />
<div class="flex flex-col items-start gap-1"> <div class="flex flex-col items-start gap-1">
<div class="items-center flex flex-row gap-0.5 max-w-[18rem]"> <div class="items-center flex flex-row gap-0.5 max-w-[18rem]">
<span <span

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
@@ -38,14 +38,14 @@ const visibilityLabel = computed(() => {
<td class="py-4 ltr:pr-4 rtl:pl-4 truncate">{{ macro.name }}</td> <td class="py-4 ltr:pr-4 rtl:pl-4 truncate">{{ macro.name }}</td>
<td class="py-4 ltr:pr-4 rtl:pl-4"> <td class="py-4 ltr:pr-4 rtl:pl-4">
<div v-if="macro.created_by" class="flex items-center"> <div v-if="macro.created_by" class="flex items-center">
<Thumbnail :username="createdByName" size="24px" /> <Avatar :name="createdByName" :size="24" rounded-full />
<span class="mx-2">{{ createdByName }}</span> <span class="mx-2">{{ createdByName }}</span>
</div> </div>
<div v-else>--</div> <div v-else>--</div>
</td> </td>
<td class="py-4 ltr:pr-4 rtl:pl-4"> <td class="py-4 ltr:pr-4 rtl:pl-4">
<div v-if="macro.updated_by" class="flex items-center"> <div v-if="macro.updated_by" class="flex items-center">
<Thumbnail :username="updatedByName" size="24px" /> <Avatar :name="updatedByName" :size="24" rounded-full />
<span class="mx-2">{{ updatedByName }}</span> <span class="mx-2">{{ updatedByName }}</span>
</div> </div>
<div v-else>--</div> <div v-else>--</div>

View File

@@ -3,7 +3,7 @@ import endOfDay from 'date-fns/endOfDay';
import getUnixTime from 'date-fns/getUnixTime'; import getUnixTime from 'date-fns/getUnixTime';
import startOfDay from 'date-fns/startOfDay'; import startOfDay from 'date-fns/startOfDay';
import subDays from 'date-fns/subDays'; import subDays from 'date-fns/subDays';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue'; import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue'; import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
@@ -13,7 +13,7 @@ const CUSTOM_DATE_RANGE_ID = 5;
export default { export default {
components: { components: {
WootDateRangePicker, WootDateRangePicker,
Thumbnail, Avatar,
ToggleSwitch, ToggleSwitch,
}, },
props: { props: {
@@ -184,11 +184,13 @@ export default {
> >
<template #singleLabel="props"> <template #singleLabel="props">
<div class="flex min-w-0 items-center gap-2"> <div class="flex min-w-0 items-center gap-2">
<Thumbnail <Avatar
:src="props.option.thumbnail" :src="props.option.thumbnail"
:status="props.option.availability_status" :status="props.option.availability_status"
:username="props.option.name" :name="props.option.name"
size="22px" :size="22"
hide-offline-status
rounded-full
/> />
<span class="my-0 text-n-slate-12 truncate">{{ <span class="my-0 text-n-slate-12 truncate">{{
props.option.name props.option.name
@@ -197,11 +199,13 @@ export default {
</template> </template>
<template #options="props"> <template #options="props">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Thumbnail <Avatar
:src="props.option.thumbnail" :src="props.option.thumbnail"
:status="props.option.availability_status" :status="props.option.availability_status"
:username="props.option.name" :name="props.option.name"
size="22px" :size="22"
hide-offline-status
rounded-full
/> />
<p class="my-0 text-n-slate-12"> <p class="my-0 text-n-slate-12">
{{ props.option.name }} {{ props.option.name }}

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import BaseCell from 'dashboard/components/table/BaseCell.vue'; import BaseCell from 'dashboard/components/table/BaseCell.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import { useMapGetter } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store';
defineProps({ defineProps({
@@ -19,11 +19,13 @@ const isRTL = useMapGetter('accounts/isRTL');
class="items-center flex text-left" class="items-center flex text-left"
:class="{ 'flex-row-reverse': isRTL }" :class="{ 'flex-row-reverse': isRTL }"
> >
<Thumbnail <Avatar
:src="row.original.thumbnail" :src="row.original.thumbnail"
size="32px" :name="row.original.agent"
:username="row.original.agent"
:status="row.original.status" :status="row.original.status"
:size="32"
hide-offline-status
rounded-full
/> />
<div class="items-start flex flex-col min-w-0 my-0 mx-2"> <div class="items-start flex flex-col min-w-0 my-0 mx-2">
<h6 <h6

View File

@@ -1,11 +1,11 @@
<script> <script>
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
export default { export default {
components: { components: {
NextButton, NextButton,
Thumbnail, Avatar,
}, },
props: { props: {
agentList: { agentList: {
@@ -115,11 +115,13 @@ export default {
</td> </td>
<td> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Thumbnail <Avatar
:src="agent.thumbnail" :src="agent.thumbnail"
size="24px" :name="agent.name"
:username="agent.name"
:status="agent.availability_status" :status="agent.availability_status"
:size="24"
hide-offline-status
rounded-full
/> />
<h4 class="text-base mb-0 text-n-slate-12"> <h4 class="text-base mb-0 text-n-slate-12">
{{ agent.name }} {{ agent.name }}

View File

@@ -4,7 +4,7 @@ import { OnClickOutside } from '@vueuse/components';
import { useToggle } from '@vueuse/core'; import { useToggle } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue'; import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
const props = defineProps({ const props = defineProps({
@@ -82,12 +82,14 @@ const hasValue = computed(() => {
{{ selectedItem.name }} {{ selectedItem.name }}
</h4> </h4>
</div> </div>
<Thumbnail <Avatar
v-if="hasValue && hasThumbnail" v-if="hasValue && hasThumbnail"
:src="selectedItem.thumbnail" :src="selectedItem.thumbnail"
size="24px"
:status="selectedItem.availability_status" :status="selectedItem.availability_status"
:username="selectedItem.name" :name="selectedItem.name"
:size="24"
hide-offline-status
rounded-full
/> />
</Button> </Button>
<div <div

View File

@@ -1,14 +1,14 @@
<script> <script>
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'next/avatar/Avatar.vue';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
WootDropdownItem, WootDropdownItem,
WootDropdownMenu, WootDropdownMenu,
Thumbnail, Avatar,
NextButton, NextButton,
}, },
@@ -105,13 +105,14 @@ export default {
{{ option.name }} {{ option.name }}
</span> </span>
</div> </div>
<Thumbnail <Avatar
v-if="hasThumbnail" v-if="hasThumbnail"
:src="option.thumbnail" :src="option.thumbnail"
size="24px" :name="option.name"
:username="option.name"
:status="option.availability_status" :status="option.availability_status"
has-border :size="24"
hide-offline-status
rounded-full
/> />
</NextButton> </NextButton>
</WootDropdownItem> </WootDropdownItem>

View File

@@ -6,7 +6,7 @@ import { messageStamp } from 'shared/helpers/timeHelper';
import ImageBubble from 'widget/components/ImageBubble.vue'; import ImageBubble from 'widget/components/ImageBubble.vue';
import VideoBubble from 'widget/components/VideoBubble.vue'; import VideoBubble from 'widget/components/VideoBubble.vue';
import FileBubble from 'widget/components/FileBubble.vue'; import FileBubble from 'widget/components/FileBubble.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import { MESSAGE_TYPE } from 'widget/helpers/constants'; import { MESSAGE_TYPE } from 'widget/helpers/constants';
import configMixin from '../mixins/configMixin'; import configMixin from '../mixins/configMixin';
import messageMixin from '../mixins/messageMixin'; import messageMixin from '../mixins/messageMixin';
@@ -21,7 +21,7 @@ export default {
AgentMessageBubble, AgentMessageBubble,
ImageBubble, ImageBubble,
VideoBubble, VideoBubble,
Thumbnail, Avatar,
UserMessage, UserMessage,
FileBubble, FileBubble,
MessageReplyButton, MessageReplyButton,
@@ -165,12 +165,15 @@ export default {
> >
<div v-if="!isASubmittedForm" class="agent-message"> <div v-if="!isASubmittedForm" class="agent-message">
<div class="avatar-wrap"> <div class="avatar-wrap">
<Thumbnail <div class="user-thumbnail-box">
v-if="message.showAvatar || hasRecordedResponse" <Avatar
:src="avatarUrl" v-if="message.showAvatar || hasRecordedResponse"
size="24px" :src="avatarUrl"
:username="agentName" :size="24"
/> :name="agentName"
rounded-full
/>
</div>
</div> </div>
<div class="message-wrap"> <div class="message-wrap">
<div v-if="hasReplyTo" class="flex mt-2 mb-1 text-xs"> <div v-if="hasReplyTo" class="flex mt-2 mb-1 text-xs">

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import { defineProps, computed } from 'vue'; import { defineProps, computed } from 'vue';
const props = defineProps({ const props = defineProps({
@@ -24,11 +24,12 @@ const usersToDisplay = computed(() => props.users.slice(0, props.limit));
:class="index ? 'ltr:-ml-4 rtl:-mr-4' : ''" :class="index ? 'ltr:-ml-4 rtl:-mr-4' : ''"
class="inline-block rounded-full text-white shadow-solid" class="inline-block rounded-full text-white shadow-solid"
> >
<Thumbnail <Avatar
size="36px" :name="user.name"
:username="user.name"
:src="user.avatar_url" :src="user.avatar_url"
has-border :size="36"
class="[&>span]:outline [&>span]:outline-1 [&>span]:outline-n-background"
rounded-full
/> />
</span> </span>
</div> </div>

View File

@@ -1,6 +1,6 @@
<script> <script>
import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import configMixin from '../mixins/configMixin'; import configMixin from '../mixins/configMixin';
import { isEmptyObject } from 'widget/helpers/utils'; import { isEmptyObject } from 'widget/helpers/utils';
import { import {
@@ -11,7 +11,7 @@ import { emitter } from 'shared/helpers/mitt';
export default { export default {
name: 'UnreadMessage', name: 'UnreadMessage',
components: { Thumbnail }, components: { Avatar },
mixins: [configMixin], mixins: [configMixin],
props: { props: {
message: { message: {
@@ -95,11 +95,12 @@ export default {
<div class="chat-bubble-wrap"> <div class="chat-bubble-wrap">
<button class="chat-bubble agent bg-white" @click="onClickMessage"> <button class="chat-bubble agent bg-white" @click="onClickMessage">
<div v-if="showSender" class="row--agent-block"> <div v-if="showSender" class="row--agent-block">
<Thumbnail <Avatar
:src="avatarUrl" :src="avatarUrl"
size="20px" :size="20"
:username="agentName" :name="agentName"
:status="availabilityStatus" :status="availabilityStatus"
rounded-full
/> />
<span v-dompurify-html="agentName" class="agent--name" /> <span v-dompurify-html="agentName" class="agent--name" />
<span v-dompurify-html="companyName" class="company--name" /> <span v-dompurify-html="companyName" class="company--name" />