fix: Styles issues with conversation card (#12107)

# Pull Request Template

## Description

This PR includes the following changes:

1. Fixes a couple of UI issues.
2. Moves all styles to inline classes.
3. Migrates the `ConversationCard.vue` component from the Options API to
the Composition API.


## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Screenshots
**Before**
<img width="353" height="550" alt="image"
src="https://github.com/user-attachments/assets/070c9bf1-6077-48d8-832d-79037b794f42"
/>



**After**
<img width="353" height="541" alt="image"
src="https://github.com/user-attachments/assets/c54bf69c-d175-45cf-a6fe-9c7ab6f66226"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Sivin Varghese 2025-08-06 15:28:38 +05:30 committed by GitHub
parent d5286c9535
commit 304c938260
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 249 additions and 323 deletions

View File

@ -38,11 +38,13 @@ const handleClose = () => emit('close');
<template> <template>
<div <div
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6" class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] rounded-xl border border-n-weak shadow-md max-h-[80vh] overflow-y-auto"
> >
<h3 class="text-base font-medium text-n-slate-12"> <div class="p-6 flex flex-col gap-6">
<h3 class="text-base font-medium text-n-slate-12 flex-shrink-0">
{{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }} {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
</h3> </h3>
<WhatsAppCampaignForm @submit="handleSubmit" @cancel="handleClose" /> <WhatsAppCampaignForm @submit="handleSubmit" @cancel="handleClose" />
</div> </div>
</div>
</template> </template>

View File

@ -1,12 +1,12 @@
<script> <script setup>
import { mapGetters } from 'vuex'; import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useMapGetter } from 'dashboard/composables/store';
import { getLastMessage } from 'dashboard/helper/conversationHelper'; import { getLastMessage } from 'dashboard/helper/conversationHelper';
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
import Thumbnail from '../Thumbnail.vue'; import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue'; import MessagePreview from './MessagePreview.vue';
import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName.vue'; import InboxName from '../InboxName.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue'; import ConversationContextMenu from './contextMenu/Index.vue';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue'; import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue'; import CardLabels from './conversationCardComponents/CardLabels.vue';
@ -14,66 +14,22 @@ import PriorityMark from './PriorityMark.vue';
import SLACardLabel from './components/SLACardLabel.vue'; import SLACardLabel from './components/SLACardLabel.vue';
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue'; import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
export default { const props = defineProps({
components: { activeLabel: { type: String, default: '' },
CardLabels, chat: { type: Object, default: () => ({}) },
InboxName, hideInboxName: { type: Boolean, default: false },
Thumbnail, hideThumbnail: { type: Boolean, default: false },
ConversationContextMenu, teamId: { type: [String, Number], default: 0 },
TimeAgo, foldersId: { type: [String, Number], default: 0 },
MessagePreview, showAssignee: { type: Boolean, default: false },
PriorityMark, conversationType: { type: String, default: '' },
SLACardLabel, selected: { type: Boolean, default: false },
ContextMenu, compact: { type: Boolean, default: false },
}, enableContextMenu: { type: Boolean, default: false },
mixins: [inboxMixin], allowedContextMenuOptions: { type: Array, default: () => [] },
props: { });
activeLabel: {
type: String, const emit = defineEmits([
default: '',
},
chat: {
type: Object,
default: () => {},
},
hideInboxName: {
type: Boolean,
default: false,
},
hideThumbnail: {
type: Boolean,
default: false,
},
teamId: {
type: [String, Number],
default: 0,
},
foldersId: {
type: [String, Number],
default: 0,
},
showAssignee: {
type: Boolean,
default: false,
},
conversationType: {
type: String,
default: '',
},
selected: {
type: Boolean,
default: false,
},
enableContextMenu: {
type: Boolean,
default: false,
},
allowedContextMenuOptions: {
type: Array,
default: () => [],
},
},
emits: [
'contextMenuToggle', 'contextMenuToggle',
'assignAgent', 'assignAgent',
'assignLabel', 'assignLabel',
@ -83,96 +39,98 @@ export default {
'assignPriority', 'assignPriority',
'updateConversationStatus', 'updateConversationStatus',
'deleteConversation', 'deleteConversation',
], 'selectConversation',
data() { 'deSelectConversation',
return { ]);
hovered: false,
showContextMenu: false, const router = useRouter();
contextMenu: {
const hovered = ref(false);
const showContextMenu = ref(false);
const contextMenu = ref({
x: null, x: null,
y: null, y: null,
}, });
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
inboxesList: 'inboxes/getInboxes',
activeInbox: 'getSelectedInbox',
accountId: 'getCurrentAccountId',
}),
chatMetadata() {
return this.chat.meta || {};
},
assignee() { const currentChat = useMapGetter('getSelectedChat');
return this.chatMetadata.assignee || {}; const inboxesList = useMapGetter('inboxes/getInboxes');
}, const activeInbox = useMapGetter('getSelectedInbox');
const accountId = useMapGetter('getCurrentAccountId');
const contactById = useMapGetter('contacts/getContact');
const inboxById = useMapGetter('inboxes/getInbox');
currentContact() { const chatMetadata = computed(() => props.chat.meta || {});
return this.$store.getters['contacts/getContact'](
this.chatMetadata.sender.id
);
},
isActiveChat() { const assignee = computed(() => chatMetadata.value.assignee || {});
return this.currentChat.id === this.chat.id;
},
unreadCount() { const currentContact = computed(() => {
return this.chat.unread_count; return contactById.value(chatMetadata.value.sender.id);
}, });
hasUnread() { const isActiveChat = computed(() => {
return this.unreadCount > 0; return currentChat.value.id === props.chat.id;
}, });
isInboxNameVisible() { const unreadCount = computed(() => props.chat.unread_count);
return !this.activeInbox;
},
lastMessageInChat() { const hasUnread = computed(() => unreadCount.value > 0);
return getLastMessage(this.chat);
},
inbox() { const isInboxNameVisible = computed(() => !activeInbox.value);
const { inbox_id: inboxId } = this.chat;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId); const lastMessageInChat = computed(() => getLastMessage(props.chat));
const inbox = computed(() => {
const { inbox_id: inboxId } = props.chat;
const stateInbox = inboxById.value(inboxId);
return stateInbox; return stateInbox;
}, });
showInboxName() { const showInboxName = computed(() => {
return ( return (
!this.hideInboxName && !props.hideInboxName &&
this.isInboxNameVisible && isInboxNameVisible.value &&
this.inboxesList.length > 1 inboxesList.value.length > 1
); );
}, });
inboxName() {
const stateInbox = this.inbox; const showMetaSection = computed(() => {
return stateInbox.name || ''; return (
}, showInboxName.value ||
hasSlaPolicyId() { (props.showAssignee && assignee.value.name) ||
return this.chat?.sla_policy_id; props.chat.priority
}, );
conversationPath() { });
const { activeInbox, chat } = this;
const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
const showLabelsSection = computed(() => {
return props.chat.labels?.length > 0 || hasSlaPolicyId.value;
});
const messagePreviewClass = computed(() => {
return [
hasUnread.value ? 'font-medium text-n-slate-12' : 'text-n-slate-11',
!props.compact && hasUnread.value ? 'ltr:pr-4 rtl:pl-4' : '',
props.compact && hasUnread.value ? 'ltr:pr-6 rtl:pl-6' : '',
];
});
const conversationPath = computed(() => {
return frontendURL( return frontendURL(
conversationUrl({ conversationUrl({
accountId: this.accountId, accountId: accountId.value,
activeInbox, activeInbox: activeInbox.value,
id: chat.id, id: props.chat.id,
label: this.activeLabel, label: props.activeLabel,
teamId: this.teamId, teamId: props.teamId,
foldersId: this.foldersId, conversationType: props.conversationType,
conversationType: this.conversationType, foldersId: props.foldersId,
}) })
); );
}, });
},
methods: { const onCardClick = e => {
onCardClick(e) { const path = conversationPath.value;
const path = this.conversationPath;
if (!path) return; if (!path) return;
// Handle Ctrl/Cmd + Click for new tab // Handle Ctrl/Cmd + Click for new tab
@ -187,84 +145,93 @@ export default {
} }
// Skip if already active // Skip if already active
if (this.isActiveChat) return; if (isActiveChat.value) return;
router.push({ path }); router.push({ path });
}, };
onThumbnailHover() {
this.hovered = !this.hideThumbnail; const onThumbnailHover = () => {
}, hovered.value = !props.hideThumbnail;
onThumbnailLeave() { };
this.hovered = false;
}, const onThumbnailLeave = () => {
onSelectConversation(checked) { hovered.value = false;
const action = checked ? 'selectConversation' : 'deSelectConversation'; };
this.$emit(action, this.chat.id, this.inbox.id);
}, const onSelectConversation = checked => {
openContextMenu(e) { if (checked) {
if (!this.enableContextMenu) return; emit('selectConversation', props.chat.id, inbox.value.id);
} else {
emit('deSelectConversation', props.chat.id, inbox.value.id);
}
};
const openContextMenu = e => {
if (!props.enableContextMenu) return;
e.preventDefault(); e.preventDefault();
this.$emit('contextMenuToggle', true); emit('contextMenuToggle', true);
this.contextMenu.x = e.pageX || e.clientX; contextMenu.value.x = e.pageX || e.clientX;
this.contextMenu.y = e.pageY || e.clientY; contextMenu.value.y = e.pageY || e.clientY;
this.showContextMenu = true; showContextMenu.value = true;
}, };
closeContextMenu() {
this.$emit('contextMenuToggle', false); const closeContextMenu = () => {
this.showContextMenu = false; emit('contextMenuToggle', false);
this.contextMenu.x = null; showContextMenu.value = false;
this.contextMenu.y = null; contextMenu.value.x = null;
}, contextMenu.value.y = null;
onUpdateConversation(status, snoozedUntil) { };
this.closeContextMenu();
this.$emit( const onUpdateConversation = (status, snoozedUntil) => {
'updateConversationStatus', closeContextMenu();
this.chat.id, emit('updateConversationStatus', props.chat.id, status, snoozedUntil);
status, };
snoozedUntil
); const onAssignAgent = agent => {
}, emit('assignAgent', agent, [props.chat.id]);
async onAssignAgent(agent) { closeContextMenu();
this.$emit('assignAgent', agent, [this.chat.id]); };
this.closeContextMenu();
}, const onAssignLabel = label => {
async onAssignLabel(label) { emit('assignLabel', [label.title], [props.chat.id]);
this.$emit('assignLabel', [label.title], [this.chat.id]); closeContextMenu();
this.closeContextMenu(); };
},
async onAssignTeam(team) { const onAssignTeam = team => {
this.$emit('assignTeam', team, this.chat.id); emit('assignTeam', team, props.chat.id);
this.closeContextMenu(); closeContextMenu();
}, };
async markAsUnread() {
this.$emit('markAsUnread', this.chat.id); const markAsUnread = () => {
this.closeContextMenu(); emit('markAsUnread', props.chat.id);
}, closeContextMenu();
async markAsRead() { };
this.$emit('markAsRead', this.chat.id);
this.closeContextMenu(); const markAsRead = () => {
}, emit('markAsRead', props.chat.id);
async assignPriority(priority) { closeContextMenu();
this.$emit('assignPriority', priority, this.chat.id); };
this.closeContextMenu();
}, const assignPriority = priority => {
async deleteConversation() { emit('assignPriority', priority, props.chat.id);
this.$emit('deleteConversation', this.chat.id); closeContextMenu();
this.closeContextMenu(); };
},
}, const deleteConversation = () => {
emit('deleteConversation', props.chat.id);
closeContextMenu();
}; };
</script> </script>
<template> <template>
<div <div
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-3 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group" class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full py-0 border-t-0 border-b-0 border-l-0 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
:class="{ :class="{
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 border-n-weak': 'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 border-n-weak':
isActiveChat, isActiveChat,
'unread-chat': hasUnread, 'bg-n-slate-2 dark:bg-n-slate-3': selected,
'has-inbox-name': showInboxName, 'px-0': compact,
'conversation-selected': selected, 'px-3': !compact,
}" }"
@click="onCardClick" @click="onCardClick"
@contextmenu="openContextMenu($event)" @contextmenu="openContextMenu($event)"
@ -276,13 +243,14 @@ export default {
> >
<label <label
v-if="hovered || selected" v-if="hovered || selected"
class="checkbox-wrapper absolute inset-0 z-20 backdrop-blur-[2px]" class="flex items-center justify-center rounded-full cursor-pointer absolute inset-0 z-20 backdrop-blur-[2px]"
:class="!showInboxName ? 'mt-4' : 'mt-8'"
@click.stop @click.stop
> >
<input <input
:value="selected" :value="selected"
:checked="selected" :checked="selected"
class="checkbox" class="!m-0 cursor-pointer"
type="checkbox" type="checkbox"
@change="onSelectConversation($event.target.checked)" @change="onSelectConversation($event.target.checked)"
/> />
@ -290,22 +258,30 @@ export default {
<Thumbnail <Thumbnail
v-if="!hideThumbnail" v-if="!hideThumbnail"
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
:badge="inboxBadge"
:username="currentContact.name" :username="currentContact.name"
:status="currentContact.availability_status" :status="currentContact.availability_status"
size="32px" size="32px"
:class="!showInboxName ? 'mt-4' : 'mt-8'"
/> />
</div> </div>
<div <div
class="px-0 py-3 border-b group-hover:border-transparent flex-1 border-n-slate-3 w-[calc(100%-40px)]" class="px-0 py-3 border-b group-hover:border-transparent flex-1 border-n-slate-3 min-w-0"
>
<div
v-if="showMetaSection"
class="flex items-center min-w-0 gap-1"
:class="{
'ltr:ml-2 rtl:mr-2': !compact,
'mx-2': compact,
}"
>
<InboxName v-if="showInboxName" :inbox="inbox" class="flex-1 min-w-0" />
<div
class="flex items-center gap-2 flex-shrink-0"
:class="{
'flex-1 justify-between': !showInboxName,
}"
> >
<div class="flex items-center conversation-card--meta min-w-0">
<InboxName
v-if="showInboxName"
:inbox="inbox"
class="flex-1 min-w-0 mx-2"
/>
<div class="flex items-center gap-2 flex-shrink-0">
<span <span
v-if="showAssignee && assignee.name" v-if="showAssignee && assignee.name"
class="text-n-slate-11 text-xs font-medium leading-3 py-0.5 px-0 inline-flex items-center truncate" class="text-n-slate-11 text-xs font-medium leading-3 py-0.5 px-0 inline-flex items-center truncate"
@ -317,7 +293,7 @@ export default {
</div> </div>
</div> </div>
<h4 <h4
class="conversation--user text-sm my-0 mx-2 capitalize pt-0.5 text-ellipsis overflow-hidden whitespace-nowrap w-[calc(100%-70px)] text-n-slate-12" class="conversation--user text-sm my-0 mx-2 capitalize pt-0.5 text-ellipsis overflow-hidden whitespace-nowrap flex-1 min-w-0 ltr:pr-16 rtl:pl-16 text-n-slate-12"
:class="hasUnread ? 'font-semibold' : 'font-medium'" :class="hasUnread ? 'font-semibold' : 'font-medium'"
> >
{{ currentContact.name }} {{ currentContact.name }}
@ -325,24 +301,27 @@ export default {
<MessagePreview <MessagePreview
v-if="lastMessageInChat" v-if="lastMessageInChat"
:message="lastMessageInChat" :message="lastMessageInChat"
class="conversation--message my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm" class="my-0 mx-2 leading-6 h-6 flex-1 min-w-0 text-sm"
:class="hasUnread ? 'font-medium text-n-slate-12' : 'text-n-slate-11'" :class="messagePreviewClass"
/> />
<p <p
v-else v-else
class="conversation--message text-n-slate-11 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap" class="text-n-slate-11 text-sm my-0 mx-2 leading-6 h-6 flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap"
:class="hasUnread ? 'font-medium text-n-slate-12' : 'text-n-slate-11'" :class="messagePreviewClass"
> >
<fluent-icon <fluent-icon
size="16" size="16"
class="-mt-0.5 align-middle inline-block text-n-slate-10" class="-mt-0.5 align-middle inline-block text-n-slate-10"
icon="info" icon="info"
/> />
<span> <span class="mx-0.5">
{{ $t(`CHAT_LIST.NO_MESSAGES`) }} {{ $t(`CHAT_LIST.NO_MESSAGES`) }}
</span> </span>
</p> </p>
<div class="absolute flex flex-col mt-4 ltr:right-4 rtl:left-4 top-4"> <div
class="absolute flex flex-col ltr:right-3 rtl:left-3"
:class="showMetaSection ? 'top-8' : 'top-4'"
>
<span class="ml-auto font-normal leading-4 text-xxs"> <span class="ml-auto font-normal leading-4 text-xxs">
<TimeAgo <TimeAgo
:last-activity-timestamp="chat.timestamp" :last-activity-timestamp="chat.timestamp"
@ -350,12 +329,17 @@ export default {
/> />
</span> </span>
<span <span
class="unread shadow-lg rounded-full hidden text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9" class="shadow-lg rounded-full text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
:class="hasUnread ? 'block' : 'hidden'"
> >
{{ unreadCount > 9 ? '9+' : unreadCount }} {{ unreadCount > 9 ? '9+' : unreadCount }}
</span> </span>
</div> </div>
<CardLabels :conversation-labels="chat.labels" class="mt-0.5 mx-2 mb-0"> <CardLabels
v-if="showLabelsSection"
:conversation-labels="chat.labels"
class="mt-0.5 mx-2 mb-0"
>
<template v-if="hasSlaPolicyId" #before> <template v-if="hasSlaPolicyId" #before>
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" /> <SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
</template> </template>
@ -388,55 +372,3 @@ export default {
</ContextMenu> </ContextMenu>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.conversation {
&.unread-chat {
.unread {
@apply block;
}
}
&.compact {
@apply pl-0;
.conversation-card--meta {
@apply ltr:pr-4 rtl:pl-4;
}
.conversation--details {
@apply rounded-sm ml-0 pl-5 pr-2;
}
}
&::v-deep .user-thumbnail-box {
@apply mt-4;
}
&.conversation-selected {
@apply bg-n-slate-2 dark:bg-n-slate-3;
}
&.has-inbox-name {
&::v-deep .user-thumbnail-box {
@apply mt-8;
}
.checkbox-wrapper {
@apply mt-8;
}
.conversation--meta {
@apply mt-4;
}
}
.checkbox-wrapper {
@apply flex items-center justify-center rounded-full cursor-pointer mt-4;
input[type='checkbox'] {
@apply m-0 cursor-pointer;
}
}
}
</style>

View File

@ -263,7 +263,7 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.bulk-action__container { .bulk-action__container {
@apply p-4 relative border-b border-solid border-n-strong dark:border-n-weak; @apply p-3 relative border-b border-solid border-n-strong dark:border-n-weak;
} }
.bulk-action__panel { .bulk-action__panel {

View File

@ -61,8 +61,8 @@ export default {
:hide-inbox-name="false" :hide-inbox-name="false"
hide-thumbnail hide-thumbnail
enable-context-menu enable-context-menu
compact
:allowed-context-menu-options="['open-new-tab', 'copy-link']" :allowed-context-menu-options="['open-new-tab', 'copy-link']"
class="compact"
/> />
</div> </div>
</div> </div>
@ -75,12 +75,4 @@ export default {
.no-label-message { .no-label-message {
@apply text-n-slate-11 mb-4; @apply text-n-slate-11 mb-4;
} }
::v-deep .conversation {
@apply pr-0;
.conversation--details {
@apply pl-2;
}
}
</style> </style>