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
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">
{{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }} <h3 class="text-base font-medium text-n-slate-12 flex-shrink-0">
</h3> {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
<WhatsAppCampaignForm @submit="handleSubmit" @cancel="handleClose" /> </h3>
<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,257 +14,224 @@ 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,
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',
'assignAgent',
'assignLabel',
'assignTeam',
'markAsUnread',
'markAsRead',
'assignPriority',
'updateConversationStatus',
'deleteConversation',
],
data() {
return {
hovered: false,
showContextMenu: false,
contextMenu: {
x: null,
y: null,
},
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
inboxesList: 'inboxes/getInboxes',
activeInbox: 'getSelectedInbox',
accountId: 'getCurrentAccountId',
}),
chatMetadata() {
return this.chat.meta || {};
},
assignee() { const emit = defineEmits([
return this.chatMetadata.assignee || {}; 'contextMenuToggle',
}, 'assignAgent',
'assignLabel',
'assignTeam',
'markAsUnread',
'markAsRead',
'assignPriority',
'updateConversationStatus',
'deleteConversation',
'selectConversation',
'deSelectConversation',
]);
currentContact() { const router = useRouter();
return this.$store.getters['contacts/getContact'](
this.chatMetadata.sender.id
);
},
isActiveChat() { const hovered = ref(false);
return this.currentChat.id === this.chat.id; const showContextMenu = ref(false);
}, const contextMenu = ref({
x: null,
y: null,
});
unreadCount() { const currentChat = useMapGetter('getSelectedChat');
return this.chat.unread_count; const inboxesList = useMapGetter('inboxes/getInboxes');
}, const activeInbox = useMapGetter('getSelectedInbox');
const accountId = useMapGetter('getCurrentAccountId');
const contactById = useMapGetter('contacts/getContact');
const inboxById = useMapGetter('inboxes/getInbox');
hasUnread() { const chatMetadata = computed(() => props.chat.meta || {});
return this.unreadCount > 0;
},
isInboxNameVisible() { const assignee = computed(() => chatMetadata.value.assignee || {});
return !this.activeInbox;
},
lastMessageInChat() { const currentContact = computed(() => {
return getLastMessage(this.chat); return contactById.value(chatMetadata.value.sender.id);
}, });
inbox() { const isActiveChat = computed(() => {
const { inbox_id: inboxId } = this.chat; return currentChat.value.id === props.chat.id;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId); });
return stateInbox;
},
showInboxName() { const unreadCount = computed(() => props.chat.unread_count);
return (
!this.hideInboxName &&
this.isInboxNameVisible &&
this.inboxesList.length > 1
);
},
inboxName() {
const stateInbox = this.inbox;
return stateInbox.name || '';
},
hasSlaPolicyId() {
return this.chat?.sla_policy_id;
},
conversationPath() {
const { activeInbox, chat } = this;
return frontendURL(
conversationUrl({
accountId: this.accountId,
activeInbox,
id: chat.id,
label: this.activeLabel,
teamId: this.teamId,
foldersId: this.foldersId,
conversationType: this.conversationType,
})
);
},
},
methods: {
onCardClick(e) {
const path = this.conversationPath;
if (!path) return;
// Handle Ctrl/Cmd + Click for new tab const hasUnread = computed(() => unreadCount.value > 0);
if (e.metaKey || e.ctrlKey) {
e.preventDefault();
window.open(
`${window.chatwootConfig.hostURL}${path}`,
'_blank',
'noopener,noreferrer'
);
return;
}
// Skip if already active const isInboxNameVisible = computed(() => !activeInbox.value);
if (this.isActiveChat) return;
router.push({ path }); const lastMessageInChat = computed(() => getLastMessage(props.chat));
},
onThumbnailHover() { const inbox = computed(() => {
this.hovered = !this.hideThumbnail; const { inbox_id: inboxId } = props.chat;
}, const stateInbox = inboxById.value(inboxId);
onThumbnailLeave() { return stateInbox;
this.hovered = false; });
},
onSelectConversation(checked) { const showInboxName = computed(() => {
const action = checked ? 'selectConversation' : 'deSelectConversation'; return (
this.$emit(action, this.chat.id, this.inbox.id); !props.hideInboxName &&
}, isInboxNameVisible.value &&
openContextMenu(e) { inboxesList.value.length > 1
if (!this.enableContextMenu) return; );
e.preventDefault(); });
this.$emit('contextMenuToggle', true);
this.contextMenu.x = e.pageX || e.clientX; const showMetaSection = computed(() => {
this.contextMenu.y = e.pageY || e.clientY; return (
this.showContextMenu = true; showInboxName.value ||
}, (props.showAssignee && assignee.value.name) ||
closeContextMenu() { props.chat.priority
this.$emit('contextMenuToggle', false); );
this.showContextMenu = false; });
this.contextMenu.x = null;
this.contextMenu.y = null; const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
},
onUpdateConversation(status, snoozedUntil) { const showLabelsSection = computed(() => {
this.closeContextMenu(); return props.chat.labels?.length > 0 || hasSlaPolicyId.value;
this.$emit( });
'updateConversationStatus',
this.chat.id, const messagePreviewClass = computed(() => {
status, return [
snoozedUntil 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' : '',
async onAssignAgent(agent) { ];
this.$emit('assignAgent', agent, [this.chat.id]); });
this.closeContextMenu();
}, const conversationPath = computed(() => {
async onAssignLabel(label) { return frontendURL(
this.$emit('assignLabel', [label.title], [this.chat.id]); conversationUrl({
this.closeContextMenu(); accountId: accountId.value,
}, activeInbox: activeInbox.value,
async onAssignTeam(team) { id: props.chat.id,
this.$emit('assignTeam', team, this.chat.id); label: props.activeLabel,
this.closeContextMenu(); teamId: props.teamId,
}, conversationType: props.conversationType,
async markAsUnread() { foldersId: props.foldersId,
this.$emit('markAsUnread', this.chat.id); })
this.closeContextMenu(); );
}, });
async markAsRead() {
this.$emit('markAsRead', this.chat.id); const onCardClick = e => {
this.closeContextMenu(); const path = conversationPath.value;
}, if (!path) return;
async assignPriority(priority) {
this.$emit('assignPriority', priority, this.chat.id); // Handle Ctrl/Cmd + Click for new tab
this.closeContextMenu(); if (e.metaKey || e.ctrlKey) {
}, e.preventDefault();
async deleteConversation() { window.open(
this.$emit('deleteConversation', this.chat.id); `${window.chatwootConfig.hostURL}${path}`,
this.closeContextMenu(); '_blank',
}, 'noopener,noreferrer'
}, );
return;
}
// Skip if already active
if (isActiveChat.value) return;
router.push({ path });
};
const onThumbnailHover = () => {
hovered.value = !props.hideThumbnail;
};
const onThumbnailLeave = () => {
hovered.value = false;
};
const onSelectConversation = checked => {
if (checked) {
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();
emit('contextMenuToggle', true);
contextMenu.value.x = e.pageX || e.clientX;
contextMenu.value.y = e.pageY || e.clientY;
showContextMenu.value = true;
};
const closeContextMenu = () => {
emit('contextMenuToggle', false);
showContextMenu.value = false;
contextMenu.value.x = null;
contextMenu.value.y = null;
};
const onUpdateConversation = (status, snoozedUntil) => {
closeContextMenu();
emit('updateConversationStatus', props.chat.id, status, snoozedUntil);
};
const onAssignAgent = agent => {
emit('assignAgent', agent, [props.chat.id]);
closeContextMenu();
};
const onAssignLabel = label => {
emit('assignLabel', [label.title], [props.chat.id]);
closeContextMenu();
};
const onAssignTeam = team => {
emit('assignTeam', team, props.chat.id);
closeContextMenu();
};
const markAsUnread = () => {
emit('markAsUnread', props.chat.id);
closeContextMenu();
};
const markAsRead = () => {
emit('markAsRead', props.chat.id);
closeContextMenu();
};
const assignPriority = priority => {
emit('assignPriority', priority, props.chat.id);
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 class="flex items-center conversation-card--meta min-w-0"> <div
<InboxName v-if="showMetaSection"
v-if="showInboxName" class="flex items-center min-w-0 gap-1"
:inbox="inbox" :class="{
class="flex-1 min-w-0 mx-2" 'ltr:ml-2 rtl:mr-2': !compact,
/> 'mx-2': compact,
<div class="flex items-center gap-2 flex-shrink-0"> }"
>
<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,
}"
>
<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>