fix: Update design to fix the crowded header (#11633)

**Before:**

<img width="907" alt="Screenshot 2025-05-29 at 3 21 00 PM"
src="https://github.com/user-attachments/assets/7738f684-7e9f-40ff-ac49-d9b389eca99b"
/>

**After:**
<img width="903" alt="Screenshot 2025-05-29 at 3 20 33 PM"
src="https://github.com/user-attachments/assets/1213d832-59d8-4d04-be96-f711297a887d"
/>
This commit is contained in:
Pranav
2025-05-29 18:45:28 -06:00
committed by GitHub
parent 70c29f699c
commit aad6d655d5
11 changed files with 363 additions and 485 deletions

View File

@@ -477,7 +477,7 @@ const menuItems = computed(() => {
<template> <template>
<aside <aside
class="w-[12.5rem] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pb-1" class="w-[200px] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pb-1"
> >
<section class="grid gap-2 mt-2 mb-4"> <section class="grid gap-2 mt-2 mb-4">
<div class="flex items-center min-w-0 gap-2 px-2"> <div class="flex items-center min-w-0 gap-2 px-2">

View File

@@ -99,7 +99,6 @@ export default {
<ConversationHeader <ConversationHeader
v-if="currentChat.id" v-if="currentChat.id"
:chat="currentChat" :chat="currentChat"
:is-inbox-view="isInboxView"
:show-back-button="isOnExpandedLayout && !isInboxView" :show-back-button="isOnExpandedLayout && !isInboxView"
/> />
<woot-tabs <woot-tabs

View File

@@ -1,7 +1,9 @@
<script> <script setup>
import { mapGetters } from 'vuex'; import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
import { useElementSize } from '@vueuse/core';
import BackButton from '../BackButton.vue'; import BackButton from '../BackButton.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
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 Thumbnail from '../Thumbnail.vue';
@@ -11,21 +13,10 @@ import { conversationListPageURL } from 'dashboard/helper/URLHelper';
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers'; import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import Linear from './linear/index.vue'; import Linear from './linear/index.vue';
import { useInbox } from 'dashboard/composables/useInbox';
import { useI18n } from 'vue-i18n';
import NextButton from 'dashboard/components-next/button/Button.vue'; const props = defineProps({
export default {
components: {
BackButton,
InboxName,
MoreActions,
Thumbnail,
SLACardLabel,
Linear,
NextButton,
},
mixins: [inboxMixin],
props: {
chat: { chat: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@@ -34,26 +25,31 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isInboxView: { });
type: Boolean,
default: false, const { t } = useI18n();
}, const store = useStore();
}, const route = useRoute();
computed: { const conversationHeader = ref(null);
...mapGetters({ const { width } = useElementSize(conversationHeader);
currentChat: 'getSelectedChat', const { isAWebWidgetInbox } = useInbox();
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', const currentChat = computed(() => store.getters.getSelectedChat);
appIntegrations: 'integrations/getAppIntegrations', const accountId = computed(() => store.getters.getCurrentAccountId);
}), const isFeatureEnabledonAccount = computed(
chatMetadata() { () => store.getters['accounts/isFeatureEnabledonAccount']
return this.chat.meta; );
}, const appIntegrations = computed(
backButtonUrl() { () => store.getters['integrations/getAppIntegrations']
);
const chatMetadata = computed(() => props.chat.meta);
const backButtonUrl = computed(() => {
const { const {
params: { accountId, inbox_id: inboxId, label, teamId }, params: { inbox_id: inboxId, label, teamId },
name, name,
} = this.$route; } = route;
return conversationListPageURL({ return conversationListPageURL({
accountId, accountId,
inboxId, inboxId,
@@ -61,64 +57,61 @@ export default {
teamId, teamId,
conversationType: name === 'conversation_mentions' ? 'mention' : '', conversationType: name === 'conversation_mentions' ? 'mention' : '',
}); });
}, });
isHMACVerified() {
if (!this.isAWebWidgetInbox) { const isHMACVerified = computed(() => {
if (!isAWebWidgetInbox.value) {
return true; return true;
} }
return this.chatMetadata.hmac_verified; return chatMetadata.value.hmac_verified;
}, });
currentContact() {
return this.$store.getters['contacts/getContact']( const currentContact = computed(() =>
this.chat.meta.sender.id store.getters['contacts/getContact'](props.chat.meta.sender.id)
); );
},
isSnoozed() { const isSnoozed = computed(
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED; () => currentChat.value.status === wootConstants.STATUS_TYPE.SNOOZED
}, );
snoozedDisplayText() {
const { snoozed_until: snoozedUntil } = this.currentChat; const snoozedDisplayText = computed(() => {
const { snoozed_until: snoozedUntil } = currentChat.value;
if (snoozedUntil) { if (snoozedUntil) {
return `${this.$t( return `${t('CONVERSATION.HEADER.SNOOZED_UNTIL')} ${snoozedReopenTime(snoozedUntil)}`;
'CONVERSATION.HEADER.SNOOZED_UNTIL'
)} ${snoozedReopenTime(snoozedUntil)}`;
} }
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY'); return t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
}, });
inbox() {
const { inbox_id: inboxId } = this.chat; const inbox = computed(() => {
return this.$store.getters['inboxes/getInbox'](inboxId); const { inbox_id: inboxId } = props.chat;
}, return store.getters['inboxes/getInbox'](inboxId);
hasMultipleInboxes() { });
return this.$store.getters['inboxes/getInboxes'].length > 1;
}, const hasMultipleInboxes = computed(
hasSlaPolicyId() { () => store.getters['inboxes/getInboxes'].length > 1
return this.chat?.sla_policy_id; );
},
isLinearIntegrationEnabled() { const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
return this.appIntegrations.find(
const isLinearIntegrationEnabled = computed(() =>
appIntegrations.value.find(
integration => integration.id === 'linear' && !!integration.hooks.length integration => integration.id === 'linear' && !!integration.hooks.length
); )
}, );
isLinearFeatureEnabled() {
return this.isFeatureEnabledonAccount( const isLinearFeatureEnabled = computed(() =>
this.accountId, isFeatureEnabledonAccount.value(accountId.value, FEATURE_FLAGS.LINEAR)
FEATURE_FLAGS.LINEAR );
);
},
},
};
</script> </script>
<template> <template>
<div <div
class="flex flex-col items-center justify-between px-3 py-2 border-b bg-n-background border-n-weak md:flex-row h-12" ref="conversationHeader"
class="flex flex-col items-center justify-center flex-1 w-full min-w-0 xl:flex-row px-3 py-2 border-b bg-n-background border-n-weak h-24 xl:h-12"
> >
<div <div
class="flex flex-col items-center justify-center flex-1 w-full min-w-0" class="flex items-center justify-start w-full xl:w-auto max-w-full min-w-0"
:class="isInboxView ? 'sm:flex-row' : 'md:flex-row'"
> >
<div class="flex items-center justify-start max-w-full min-w-0 w-fit">
<BackButton <BackButton
v-if="showBackButton" v-if="showBackButton"
:back-url="backButtonUrl" :back-url="backButtonUrl"
@@ -126,7 +119,6 @@ export default {
/> />
<Thumbnail <Thumbnail
: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"
@@ -134,16 +126,12 @@ export default {
<div <div
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit" class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit"
> >
<div <div class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit">
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
>
<NextButton link slate>
<span <span
class="text-sm font-medium truncate leading-tight text-n-slate-12" class="text-sm font-medium truncate leading-tight text-n-slate-12"
> >
{{ currentContact.name }} {{ currentContact.name }}
</span> </span>
</NextButton>
<fluent-icon <fluent-icon
v-if="!isHMACVerified" v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')" v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
@@ -156,7 +144,7 @@ export default {
<div <div
class="flex items-center gap-2 overflow-hidden text-xs conversation--header--actions text-ellipsis whitespace-nowrap" class="flex items-center gap-2 overflow-hidden text-xs conversation--header--actions text-ellipsis whitespace-nowrap"
> >
<InboxName v-if="hasMultipleInboxes" :inbox="inbox" /> <InboxName v-if="hasMultipleInboxes" :inbox="inbox" class="!mx-0" />
<span v-if="isSnoozed" class="font-medium text-n-amber-10"> <span v-if="isSnoozed" class="font-medium text-n-amber-10">
{{ snoozedDisplayText }} {{ snoozedDisplayText }}
</span> </span>
@@ -164,23 +152,22 @@ export default {
</div> </div>
</div> </div>
<div <div
class="flex flex-row items-center justify-end flex-grow gap-2 mt-3 header-actions-wrap lg:mt-0" class="flex flex-row items-center justify-start xl:justify-end flex-grow gap-2 w-full xl:w-auto mt-3 header-actions-wrap xl:mt-0"
> >
<SLACardLabel v-if="hasSlaPolicyId" :chat="chat" show-extended-info /> <SLACardLabel
v-if="hasSlaPolicyId"
:chat="chat"
show-extended-info
:parent-width="width"
class="hidden lg:flex"
/>
<Linear <Linear
v-if="isLinearIntegrationEnabled && isLinearFeatureEnabled" v-if="isLinearIntegrationEnabled && isLinearFeatureEnabled"
:conversation-id="currentChat.id" :conversation-id="currentChat.id"
:parent-width="width"
class="hidden lg:flex"
/> />
<MoreActions :conversation-id="currentChat.id" /> <MoreActions :conversation-id="currentChat.id" />
</div> </div>
</div> </div>
</div>
</template> </template>
<style lang="scss" scoped>
.conversation--header--actions {
::v-deep .inbox--name {
@apply m-0;
}
}
</style>

View File

@@ -44,7 +44,7 @@ const activeTab = computed(() => {
<template> <template>
<div <div
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-80 min-w-80 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background" class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-[320px] min-w-[320px] 2xl:min-w-[360px] 2xl:w-[360px] flex flex-col bg-n-background"
> >
<div class="flex flex-1 overflow-auto"> <div class="flex flex-1 overflow-auto">
<ContactPanel <ContactPanel

View File

@@ -1,10 +1,14 @@
<script> <script setup>
import { mapGetters } from 'vuex'; import { computed, onUnmounted } from 'vue';
import { useToggle } from '@vueuse/core';
import { useStore } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import EmailTranscriptModal from './EmailTranscriptModal.vue'; import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue'; import ResolveAction from '../../buttons/ResolveAction.vue';
import ButtonV4 from 'dashboard/components-next/button/Button.vue'; import ButtonV4 from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import { import {
CMD_MUTE_CONVERSATION, CMD_MUTE_CONVERSATION,
@@ -12,97 +16,111 @@ import {
CMD_UNMUTE_CONVERSATION, CMD_UNMUTE_CONVERSATION,
} from 'dashboard/helper/commandbar/events'; } from 'dashboard/helper/commandbar/events';
export default { // No props needed as we're getting currentChat from the store directly
components: { const store = useStore();
EmailTranscriptModal, const { t } = useI18n();
ResolveAction,
ButtonV4, const [showEmailActionsModal, toggleEmailModal] = useToggle(false);
}, const [showActionsDropdown, toggleDropdown] = useToggle(false);
data() {
return { const currentChat = computed(() => store.getters.getSelectedChat);
showEmailActionsModal: false,
}; const actionMenuItems = computed(() => {
}, const items = [];
computed: {
...mapGetters({ currentChat: 'getSelectedChat' }), if (!currentChat.value.muted) {
}, items.push({
mounted() { icon: 'i-lucide-volume-off',
emitter.on(CMD_MUTE_CONVERSATION, this.mute); label: t('CONTACT_PANEL.MUTE_CONTACT'),
emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute); action: 'mute',
emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal); value: 'mute',
}, });
unmounted() { } else {
emitter.off(CMD_MUTE_CONVERSATION, this.mute); items.push({
emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute); icon: 'i-lucide-volume-1',
emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal); label: t('CONTACT_PANEL.UNMUTE_CONTACT'),
}, action: 'unmute',
methods: { value: 'unmute',
mute() { });
this.$store.dispatch('muteConversation', this.currentChat.id); }
useAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
}, items.push({
unmute() { icon: 'i-lucide-share',
this.$store.dispatch('unmuteConversation', this.currentChat.id); label: t('CONTACT_PANEL.SEND_TRANSCRIPT'),
useAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS')); action: 'send_transcript',
}, value: 'send_transcript',
toggleEmailActionsModal() { });
this.showEmailActionsModal = !this.showEmailActionsModal;
}, return items;
}, });
const handleActionClick = ({ action }) => {
toggleDropdown(false);
if (action === 'mute') {
store.dispatch('muteConversation', currentChat.value.id);
useAlert(t('CONTACT_PANEL.MUTED_SUCCESS'));
} else if (action === 'unmute') {
store.dispatch('unmuteConversation', currentChat.value.id);
useAlert(t('CONTACT_PANEL.UNMUTED_SUCCESS'));
} else if (action === 'send_transcript') {
toggleEmailModal();
}
}; };
// These functions are needed for the event listeners
const mute = () => {
store.dispatch('muteConversation', currentChat.value.id);
useAlert(t('CONTACT_PANEL.MUTED_SUCCESS'));
};
const unmute = () => {
store.dispatch('unmuteConversation', currentChat.value.id);
useAlert(t('CONTACT_PANEL.UNMUTED_SUCCESS'));
};
emitter.on(CMD_MUTE_CONVERSATION, mute);
emitter.on(CMD_UNMUTE_CONVERSATION, unmute);
emitter.on(CMD_SEND_TRANSCRIPT, toggleEmailModal);
onUnmounted(() => {
emitter.off(CMD_MUTE_CONVERSATION, mute);
emitter.off(CMD_UNMUTE_CONVERSATION, unmute);
emitter.off(CMD_SEND_TRANSCRIPT, toggleEmailModal);
});
</script> </script>
<template> <template>
<div class="relative flex items-center gap-2 actions--container"> <div class="relative flex items-center gap-2 actions--container">
<ButtonV4
v-if="!currentChat.muted"
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
size="sm"
variant="ghost"
color="slate"
icon="i-lucide-volume-off"
@click="mute"
/>
<ButtonV4
v-else
v-tooltip.left="$t('CONTACT_PANEL.UNMUTE_CONTACT')"
size="sm"
variant="ghost"
color="slate"
icon="i-lucide-volume-1"
@click="unmute"
/>
<ButtonV4
v-tooltip="$t('CONTACT_PANEL.SEND_TRANSCRIPT')"
size="sm"
variant="ghost"
color="slate"
icon="i-lucide-share"
@click="toggleEmailActionsModal"
/>
<ResolveAction <ResolveAction
:conversation-id="currentChat.id" :conversation-id="currentChat.id"
:status="currentChat.status" :status="currentChat.status"
/> />
<div
v-on-clickaway="() => toggleDropdown(false)"
class="relative flex items-center group"
>
<ButtonV4
v-tooltip="$t('CONVERSATION.HEADER.MORE_ACTIONS')"
size="sm"
variant="ghost"
color="slate"
icon="i-lucide-more-vertical"
class="rounded-md group-hover:bg-n-alpha-2"
@click="toggleDropdown()"
/>
<DropdownMenu
v-if="showActionsDropdown"
:menu-items="actionMenuItems"
class="mt-1 ltr:right-0 rtl:left-0 top-full"
@action="handleActionClick"
/>
</div>
<EmailTranscriptModal <EmailTranscriptModal
v-if="showEmailActionsModal" v-if="showEmailActionsModal"
:show="showEmailActionsModal" :show="showEmailActionsModal"
:current-chat="currentChat" :current-chat="currentChat"
@cancel="toggleEmailActionsModal" @cancel="toggleEmailModal"
/> />
</div> </div>
</template> </template>
<style scoped lang="scss">
.more--button {
@apply items-center flex ml-2 rtl:ml-0 rtl:mr-2;
}
.dropdown-pane {
@apply -right-2 top-12;
}
.icon {
@apply mr-1 rtl:mr-0 rtl:ml-1 min-w-[1rem];
}
</style>

View File

@@ -1,14 +1,10 @@
<script> <script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { evaluateSLAStatus } from '@chatwoot/utils'; import { evaluateSLAStatus } from '@chatwoot/utils';
import SLAPopoverCard from './SLAPopoverCard.vue'; import SLAPopoverCard from './SLAPopoverCard.vue';
const REFRESH_INTERVAL = 60000; const props = defineProps({
export default {
components: {
SLAPopoverCard,
},
props: {
chat: { chat: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@@ -17,116 +13,103 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
parentWidth: {
type: Number,
default: 1000,
}, },
data() { });
return {
timer: null, const REFRESH_INTERVAL = 60000;
showSlaPopover: false, const { t } = useI18n();
slaStatus: {
const timer = ref(null);
const slaStatus = ref({
threshold: null, threshold: null,
isSlaMissed: false, isSlaMissed: false,
type: null, type: null,
icon: null, icon: null,
}, });
};
},
computed: {
slaPolicyId() {
return this.chat?.sla_policy_id;
},
appliedSLA() {
return this.chat?.applied_sla;
},
slaEvents() {
return this.chat?.sla_events;
},
hasSlaThreshold() {
return this.slaStatus?.threshold;
},
isSlaMissed() {
return this.slaStatus?.isSlaMissed;
},
slaTextStyles() {
return this.isSlaMissed ? 'text-n-ruby-11' : 'text-n-amber-11';
},
slaStatusText() {
const upperCaseType = this.slaStatus?.type?.toUpperCase(); // FRT, NRT, or RT
const statusKey = this.isSlaMissed ? 'MISSED' : 'DUE';
return this.$t(`CONVERSATION.HEADER.SLA_STATUS.${upperCaseType}`, { const appliedSLA = computed(() => props.chat?.applied_sla);
status: this.$t(`CONVERSATION.HEADER.SLA_STATUS.${statusKey}`), const slaEvents = computed(() => props.chat?.sla_events);
const hasSlaThreshold = computed(() => slaStatus.value?.threshold);
const isSlaMissed = computed(() => slaStatus.value?.isSlaMissed);
const slaTextStyles = computed(() =>
isSlaMissed.value ? 'text-n-ruby-11' : 'text-n-amber-11'
);
const slaStatusText = computed(() => {
const upperCaseType = slaStatus.value?.type?.toUpperCase(); // FRT, NRT, or RT
const statusKey = isSlaMissed.value ? 'MISSED' : 'DUE';
return t(`CONVERSATION.HEADER.SLA_STATUS.${upperCaseType}`, {
status: t(`CONVERSATION.HEADER.SLA_STATUS.${statusKey}`),
}); });
}, });
showSlaPopoverCard() {
return ( const showSlaPopoverCard = computed(
this.showExtendedInfo && this.showSlaPopover && this.slaEvents.length () => props.showExtendedInfo && slaEvents.value?.length > 0
); );
},
}, const groupClass = computed(() => {
watch: { return props.showExtendedInfo
chat() { ? 'h-[26px] rounded-lg bg-n-alpha-1'
this.updateSlaStatus(); : 'rounded h-5 border border-n-strong';
}, });
},
mounted() { const updateSlaStatus = () => {
this.updateSlaStatus(); slaStatus.value = evaluateSLAStatus({
this.createTimer(); appliedSla: appliedSLA.value,
}, chat: props.chat,
unmounted() {
if (this.timer) {
clearTimeout(this.timer);
}
},
methods: {
createTimer() {
this.timer = setTimeout(() => {
this.updateSlaStatus();
this.createTimer();
}, REFRESH_INTERVAL);
},
updateSlaStatus() {
this.slaStatus = evaluateSLAStatus({
appliedSla: this.appliedSLA,
chat: this.chat,
}); });
},
openSlaPopover() {
if (!this.showExtendedInfo) return;
this.showSlaPopover = true;
},
closeSlaPopover() {
this.showSlaPopover = false;
},
},
}; };
const createTimer = () => {
timer.value = setTimeout(() => {
updateSlaStatus();
createTimer();
}, REFRESH_INTERVAL);
};
watch(
() => props.chat,
() => {
updateSlaStatus();
}
);
const slaPopoverClass = computed(() => {
return props.showExtendedInfo
? 'ltr:pr-1.5 rtl:pl-1.5 ltr:border-r rtl:border-l border-n-strong'
: '';
});
onMounted(() => {
updateSlaStatus();
createTimer();
});
onUnmounted(() => {
if (timer.value) {
clearTimeout(timer.value);
}
});
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if --> <!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<div <div
v-if="hasSlaThreshold" v-if="hasSlaThreshold"
class="relative flex items-center cursor-pointer min-w-fit" class="relative flex items-center cursor-pointer min-w-fit group"
:class=" :class="groupClass"
showExtendedInfo
? 'h-[26px] rounded-lg bg-n-alpha-1'
: 'rounded h-5 border border-n-strong'
"
> >
<div <div
v-on-clickaway="closeSlaPopover" class="flex items-center w-full truncate px-1.5"
class="flex items-center w-full truncate" :class="showExtendedInfo ? '' : 'gap-1'"
:class="showExtendedInfo ? 'px-1.5' : 'px-2 gap-1'"
@mouseover="openSlaPopover()"
>
<div
class="flex items-center gap-1"
:class="
showExtendedInfo &&
'ltr:pr-1.5 rtl:pl-1.5 ltr:border-r rtl:border-l border-n-strong'
"
> >
<div class="flex items-center gap-1" :class="slaPopoverClass">
<fluent-icon <fluent-icon
size="14" size="12"
:icon="slaStatus.icon" :icon="slaStatus.icon"
type="outline" type="outline"
:icon-lib="isSlaMissed ? 'lucide' : 'fluent'" :icon-lib="isSlaMissed ? 'lucide' : 'fluent'"
@@ -134,7 +117,7 @@ export default {
:class="slaTextStyles" :class="slaTextStyles"
/> />
<span <span
v-if="showExtendedInfo" v-if="showExtendedInfo && parentWidth > 650"
class="text-xs font-medium" class="text-xs font-medium"
:class="slaTextStyles" :class="slaTextStyles"
> >
@@ -151,7 +134,7 @@ export default {
<SLAPopoverCard <SLAPopoverCard
v-if="showSlaPopoverCard" v-if="showSlaPopoverCard"
:sla-missed-events="slaEvents" :sla-missed-events="slaEvents"
class="right-0 top-7" class="rtl:left-0 ltr:right-0 top-7 hidden group-hover:flex"
/> />
</div> </div>
</template> </template>

View File

@@ -16,6 +16,10 @@ const props = defineProps({
type: [Number, String], type: [Number, String],
required: true, required: true,
}, },
parentWidth: {
type: Number,
default: 10000,
},
}); });
defineOptions({ defineOptions({
@@ -73,6 +77,14 @@ const unlinkIssue = async linkId => {
} }
}; };
const shouldShowIssueIdentifier = computed(() => {
if (!linkedIssue.value) {
return false;
}
return props.parentWidth > 600;
});
const openIssue = () => { const openIssue = () => {
if (!linkedIssue.value) shouldShowPopup.value = true; if (!linkedIssue.value) shouldShowPopup.value = true;
shouldShow.value = true; shouldShow.value = true;
@@ -119,7 +131,10 @@ onMounted(() => {
class="text-[#5E6AD2] flex-shrink-0" class="text-[#5E6AD2] flex-shrink-0"
view-box="0 0 19 19" view-box="0 0 19 19"
/> />
<span v-if="linkedIssue" class="text-xs font-medium text-n-slate-11"> <span
v-if="shouldShowIssueIdentifier"
class="text-xs font-medium text-n-slate-11"
>
{{ linkedIssue.issue.identifier }} {{ linkedIssue.issue.identifier }}
</span> </span>
</Button> </Button>
@@ -127,7 +142,7 @@ onMounted(() => {
v-if="linkedIssue" v-if="linkedIssue"
:issue="linkedIssue.issue" :issue="linkedIssue.issue"
:link-id="linkedIssue.id" :link-id="linkedIssue.id"
class="absolute right-0 top-[36px] invisible group-hover:visible" class="absolute rtl:left-0 ltr:right-0 top-9 invisible group-hover:visible"
@unlink-issue="unlinkIssue" @unlink-issue="unlinkIssue"
/> />
<woot-modal <woot-modal

View File

@@ -1,110 +0,0 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import MoreActions from '../MoreActions.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
vi.mock('shared/helpers/mitt', () => ({
emitter: {
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
},
}));
const mockDirective = {
mounted: () => {},
};
import { emitter } from 'shared/helpers/mitt';
describe('MoveActions', () => {
let currentChat = { id: 8, muted: false };
let store = null;
let muteConversation = null;
let unmuteConversation = null;
beforeEach(() => {
muteConversation = vi.fn(() => Promise.resolve());
unmuteConversation = vi.fn(() => Promise.resolve());
store = createStore({
state: {
authenticated: true,
currentChat,
},
getters: {
getSelectedChat: () => currentChat,
},
modules: {
conversations: {
namespaced: false,
actions: { muteConversation, unmuteConversation },
},
},
});
});
const createWrapper = () =>
mount(MoreActions, {
global: {
plugins: [store],
components: {
'fluent-icon': FluentIcon,
},
directives: {
'on-clickaway': mockDirective,
},
},
});
describe('muting discussion', () => {
it('triggers "muteConversation"', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
expect(muteConversation).toHaveBeenCalledTimes(1);
expect(muteConversation).toHaveBeenCalledWith(
expect.any(Object), // First argument is the Vuex context object
currentChat.id // Second argument is the ID of the conversation
);
});
it('shows alert', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message:
'This contact is blocked successfully. You will not be notified of any future conversations.',
action: null,
});
});
});
describe('unmuting discussion', () => {
beforeEach(() => {
currentChat.muted = true;
});
it('triggers "unmuteConversation"', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
expect(unmuteConversation).toHaveBeenCalledTimes(1);
expect(unmuteConversation).toHaveBeenCalledWith(
expect.any(Object), // First argument is the Vuex context object
currentChat.id // Second argument is the ID of the conversation
);
});
it('shows alert', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message: 'This contact is unblocked successfully.',
action: null,
});
});
});
});

View File

@@ -43,7 +43,7 @@ describe('useFontSize', () => {
it('returns fontSizeOptions with correct structure', () => { it('returns fontSizeOptions with correct structure', () => {
const { fontSizeOptions } = useFontSize(); const { fontSizeOptions } = useFontSize();
expect(fontSizeOptions).toHaveLength(6); expect(fontSizeOptions).toHaveLength(5);
expect(fontSizeOptions[0]).toHaveProperty('value'); expect(fontSizeOptions[0]).toHaveProperty('value');
expect(fontSizeOptions[0]).toHaveProperty('label'); expect(fontSizeOptions[0]).toHaveProperty('label');
@@ -59,12 +59,6 @@ describe('useFontSize', () => {
label: label:
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.SMALLER', 'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.SMALLER',
}); });
expect(fontSizeOptions.find(option => option.value === '22px')).toEqual({
value: '22px',
label:
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.EXTRA_LARGE',
});
}); });
it('returns currentFontSize from UI settings', () => { it('returns currentFontSize from UI settings', () => {
@@ -84,9 +78,6 @@ describe('useFontSize', () => {
applyFontSize('14px'); applyFontSize('14px');
expect(document.documentElement.style.fontSize).toBe('14px'); expect(document.documentElement.style.fontSize).toBe('14px');
applyFontSize('22px');
expect(document.documentElement.style.fontSize).toBe('22px');
applyFontSize('16px'); applyFontSize('16px');
expect(document.documentElement.style.fontSize).toBe('16px'); expect(document.documentElement.style.fontSize).toBe('16px');
}); });
@@ -145,8 +136,6 @@ describe('useFontSize', () => {
'Smaller', 'Smaller',
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.DEFAULT': 'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.DEFAULT':
'Default', 'Default',
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.EXTRA_LARGE':
'Extra Large',
}; };
return translations[key] || key; return translations[key] || key;
}); });
@@ -160,9 +149,6 @@ describe('useFontSize', () => {
expect(fontSizeOptions.find(option => option.value === '16px').label).toBe( expect(fontSizeOptions.find(option => option.value === '16px').label).toBe(
'Default' 'Default'
); );
expect(fontSizeOptions.find(option => option.value === '22px').label).toBe(
'Extra Large'
);
// Verify translation function was called with correct keys // Verify translation function was called with correct keys
expect(mockTranslate).toHaveBeenCalledWith( expect(mockTranslate).toHaveBeenCalledWith(

View File

@@ -19,7 +19,6 @@ const FONT_SIZE_OPTIONS = {
DEFAULT: '16px', DEFAULT: '16px',
LARGE: '18px', LARGE: '18px',
LARGER: '20px', LARGER: '20px',
EXTRA_LARGE: '22px',
}; };
/** /**

View File

@@ -70,6 +70,7 @@
"RESOLVE_ACTION": "Resolve", "RESOLVE_ACTION": "Resolve",
"REOPEN_ACTION": "Reopen", "REOPEN_ACTION": "Reopen",
"OPEN_ACTION": "Open", "OPEN_ACTION": "Open",
"MORE_ACTIONS": "More actions",
"OPEN": "More", "OPEN": "More",
"CLOSE": "Close", "CLOSE": "Close",
"DETAILS": "details", "DETAILS": "details",