chore: Replace Thumbnail with Avatar (#12119)
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 }"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user