chore: Update feedback fixes
This commit is contained in:
@@ -6,6 +6,10 @@ import LabelItem from 'dashboard/components-next/label/LabelItem.vue';
|
||||
import AddLabel from 'dashboard/components-next/label/AddLabel.vue';
|
||||
|
||||
const props = defineProps({
|
||||
wrapperRef: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
@@ -117,10 +121,7 @@ const handleLabelHover = labelId => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 ltr:mr-10 rtl:ml-10"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2" @mouseleave="handleMouseLeave">
|
||||
<LabelItem
|
||||
v-for="label in savedLabels"
|
||||
:key="label.id"
|
||||
@@ -132,6 +133,7 @@ const handleLabelHover = labelId => {
|
||||
/>
|
||||
<AddLabel
|
||||
:label-menu-items="labelMenuItems"
|
||||
:wrapper-ref="wrapperRef"
|
||||
@update-label="handleLabelAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -126,7 +126,7 @@ const closeMobileSidebar = () => {
|
||||
<!-- Desktop sidebar -->
|
||||
<div
|
||||
v-if="slots.sidebar"
|
||||
class="hidden lg:block overflow-y-auto justify-end min-w-52 w-full py-5 max-w-md border-l border-n-weak bg-n-surface-2"
|
||||
class="hidden lg:block overflow-y-auto justify-end min-w-52 w-full py-5 max-w-md ltr:border-l rtl:border-r border-n-weak bg-n-surface-2"
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,8 @@ const emit = defineEmits(['goToContactsList']);
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const wrapperRef = ref(null);
|
||||
|
||||
const confirmDeleteContactDialogRef = ref(null);
|
||||
|
||||
const avatarFile = ref(null);
|
||||
@@ -123,7 +125,7 @@ const handleAvatarDelete = async () => {
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start gap-6 pb-6 w-full">
|
||||
<div class="flex flex-col items-start gap-3 w-full">
|
||||
<div ref="wrapperRef" class="flex flex-col items-start gap-3 w-full">
|
||||
<Avatar
|
||||
:src="avatarSrc || ''"
|
||||
:name="selectedContact?.name || ''"
|
||||
@@ -173,7 +175,10 @@ const handleAvatarDelete = async () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ContactLabels :contact-id="selectedContact?.id" />
|
||||
<ContactLabels
|
||||
:wrapper-ref="wrapperRef"
|
||||
:contact-id="selectedContact?.id"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-6 w-full">
|
||||
<ContactsForm
|
||||
|
||||
+3
-3
@@ -116,7 +116,7 @@ const labelsCountText = computed(() => {
|
||||
});
|
||||
}
|
||||
if (!showAllLabels.value && hiddenLabelsCount.value > 0) {
|
||||
return `+${hiddenLabelsCount.value}`;
|
||||
return hiddenLabelsCount.value;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
@@ -181,8 +181,8 @@ const onShowLabels = e => {
|
||||
xs
|
||||
slate
|
||||
:no-animation="disableToggle"
|
||||
:icon="labelsCountText ? '' : 'i-lucide-chevron-left'"
|
||||
class="!py-0 !px-1.5 flex-shrink-0 !rounded-md !bg-n-button-color -outline-offset-1"
|
||||
:icon="labelsCountText ? 'i-lucide-plus' : 'i-lucide-chevron-left'"
|
||||
class="!py-0 !px-1.5 flex-shrink-0 !rounded-md !bg-n-button-color -outline-offset-1 !gap-0.5 [&>span:first-child]:!text-n-slate-10 [&>span:last-child]:!text-n-slate-11"
|
||||
:class="{ 'cursor-default': disableToggle }"
|
||||
@click="onShowLabels"
|
||||
/>
|
||||
|
||||
-5
@@ -48,11 +48,6 @@ const showSection = computed(() => {
|
||||
>
|
||||
<div class="flex-1 min-w-0 flex items-center gap-0.5">
|
||||
<InboxName v-if="showInboxName" :inbox="inbox" class="min-w-0" />
|
||||
<div
|
||||
v-if="showInboxName && hasSlaPolicyId"
|
||||
class="rounded-lg h-3 w-px bg-n-strong mx-1"
|
||||
/>
|
||||
<SLACardLabel v-if="hasSlaPolicyId" ref="slaCardLabel" :chat="chat" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<CardPriorityIcon
|
||||
|
||||
+16
-2
@@ -1,11 +1,12 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { getLastMessage } from 'dashboard/helper/conversationHelper';
|
||||
import CardMetaSection from './CardMetaSection.vue';
|
||||
import CardAvatar from './CardAvatar.vue';
|
||||
import CardHeader from './CardHeader.vue';
|
||||
import CardContent from './CardContent.vue';
|
||||
import CardLabels from './CardLabels.vue';
|
||||
import SLACardLabel from 'dashboard/components-next/Conversation/Sla/SLACardLabel.vue';
|
||||
|
||||
const props = defineProps({
|
||||
chat: { type: Object, required: true },
|
||||
@@ -24,6 +25,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['selectConversation', 'click', 'contextmenu']);
|
||||
|
||||
const slaCardLabel = useTemplateRef('slaCardLabel');
|
||||
|
||||
const lastMessageInChat = computed(() => getLastMessage(props.chat));
|
||||
const showLabelsSection = computed(() => props.chat.labels?.length > 0);
|
||||
const showExpandedPreview = computed(
|
||||
@@ -37,6 +40,10 @@ const voiceCallData = computed(() => ({
|
||||
|
||||
const unreadCount = computed(() => props.chat?.unread_count);
|
||||
|
||||
const hasSlaPolicyId = computed(
|
||||
() => props.chat?.sla_policy_id || slaCardLabel.value?.hasSlaThreshold
|
||||
);
|
||||
|
||||
const onSelectConversation = checked => {
|
||||
emit('selectConversation', checked);
|
||||
};
|
||||
@@ -103,8 +110,9 @@ const onSelectConversation = checked => {
|
||||
v-if="showLabelsSection || isInboxView"
|
||||
:labels="chat.labels"
|
||||
>
|
||||
<template v-if="isInboxView" #before>
|
||||
<template #before>
|
||||
<CardMetaSection
|
||||
v-if="isInboxView"
|
||||
:chat="chat"
|
||||
:inbox="inbox"
|
||||
:show-assignee="showAssignee"
|
||||
@@ -112,6 +120,12 @@ const onSelectConversation = checked => {
|
||||
:is-labels-empty="chat.labels.length === 0"
|
||||
inline
|
||||
/>
|
||||
<SLACardLabel
|
||||
v-else-if="hasSlaPolicyId"
|
||||
ref="slaCardLabel"
|
||||
data-before-slot
|
||||
:chat="chat"
|
||||
/>
|
||||
</template>
|
||||
</CardLabels>
|
||||
</div>
|
||||
|
||||
+16
-4
@@ -116,11 +116,23 @@ const selectedModel = computed({
|
||||
<InboxName v-if="showInboxName" :inbox="inbox" class="min-w-0" />
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="text-label-small outline outline-1 -outline-offset-1 outline-n-weak px-1.5 rounded-md h-6 text-n-slate-11 min-w-12 flex-shrink-0 inline-flex items-center justify-center truncate"
|
||||
<div class="w-px h-3 bg-n-slate-6 flex-shrink-0" />
|
||||
|
||||
<div
|
||||
v-tooltip.top="{
|
||||
content: chat.id,
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
class="h-6 flex items-center gap-1 max-w-20 w-full min-w-0"
|
||||
>
|
||||
{{ chat.id }}
|
||||
</span>
|
||||
<Icon
|
||||
icon="i-woot-hash"
|
||||
class="size-3.5 text-n-slate-10 flex-shrink-0"
|
||||
/>
|
||||
<span class="text-label-small text-n-slate-11 truncate">
|
||||
{{ chat.id }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<CardAvatar
|
||||
:contact="currentContact"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
|
||||
import SLAPopoverCard from './SLAPopoverCard.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Label from 'dashboard/components-next/label/Label.vue';
|
||||
|
||||
const props = defineProps({
|
||||
chat: {
|
||||
@@ -35,9 +36,6 @@ const appliedSLA = computed(() => props.chat?.applied_sla);
|
||||
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 conversation = computed(() => useCamelCase(props.chat, { deep: true }));
|
||||
|
||||
@@ -83,16 +81,15 @@ defineExpose({
|
||||
v-bind="$attrs"
|
||||
class="relative flex items-center cursor-pointer min-w-fit group"
|
||||
>
|
||||
<div class="flex items-center w-full truncate gap-1" :class="slaTextStyles">
|
||||
<Icon
|
||||
icon="i-lucide-flame"
|
||||
class="flex-shrink-0 size-3"
|
||||
:class="slaTextStyles"
|
||||
/>
|
||||
<span class="text-label-small" :class="slaTextStyles">
|
||||
{{ slaStatus.threshold }}
|
||||
</span>
|
||||
</div>
|
||||
<Label
|
||||
:label="slaStatus.threshold"
|
||||
:color="isSlaMissed ? 'ruby' : 'amber'"
|
||||
compact
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="i-lucide-flame" class="flex-shrink-0 size-3.5" />
|
||||
</template>
|
||||
</Label>
|
||||
<SLAPopoverCard
|
||||
v-if="showSlaPopoverCard"
|
||||
:sla-missed-events="slaEvents"
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useDropdownPosition } from 'dashboard/composables/useDropdownPosition';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
labelMenuItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
wrapperRef: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['updateLabel']);
|
||||
@@ -22,15 +28,54 @@ const showDropdown = ref(false);
|
||||
const triggerRef = ref(null);
|
||||
const dropdownRef = ref(null);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
const { position, updatePosition } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
showDropdown
|
||||
showDropdown,
|
||||
{ container: props.wrapperRef }
|
||||
);
|
||||
|
||||
const openDropdown = async () => {
|
||||
showDropdown.value = !showDropdown.value;
|
||||
if (showDropdown.value) {
|
||||
await nextTick();
|
||||
updatePosition();
|
||||
}
|
||||
};
|
||||
|
||||
const updateLabel = label => {
|
||||
emit('updateLabel', label);
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
showDropdown.value = false;
|
||||
};
|
||||
|
||||
// Watch labelMenuItems and update position when they change
|
||||
watch(
|
||||
() => props.labelMenuItems,
|
||||
async () => {
|
||||
if (showDropdown.value) {
|
||||
await nextTick();
|
||||
updatePosition();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Watch wrapper container for size changes (hover effects from parent)
|
||||
useResizeObserver(props.wrapperRef, () => {
|
||||
if (showDropdown.value) {
|
||||
updatePosition();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div
|
||||
v-on-click-outside="[closeDropdown, { ignore: [triggerRef] }]"
|
||||
class="relative"
|
||||
>
|
||||
<Button
|
||||
ref="triggerRef"
|
||||
:label="t('LABEL.TAG_BUTTON')"
|
||||
@@ -39,17 +84,17 @@ const { positionClasses } = useDropdownPosition(
|
||||
:variant="showDropdown ? 'faded' : 'solid'"
|
||||
class="font-460 !-outline-offset-1"
|
||||
icon="i-lucide-plus"
|
||||
@click="showDropdown = !showDropdown"
|
||||
@click="openDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showDropdown"
|
||||
ref="dropdownRef"
|
||||
v-on-clickaway="() => (showDropdown = false)"
|
||||
:menu-items="labelMenuItems"
|
||||
show-search
|
||||
class="z-[100] w-48 overflow-y-auto max-h-52"
|
||||
:class="positionClasses"
|
||||
@action="emit('updateLabel', $event)"
|
||||
:class="position.class"
|
||||
:style="position.style"
|
||||
@action="updateLabel($event)"
|
||||
>
|
||||
<template #thumbnail="{ item }">
|
||||
<div
|
||||
|
||||
@@ -47,10 +47,10 @@ const colorClasses = computed(() => COLOR_CLASSES[props.color]);
|
||||
<template>
|
||||
<div
|
||||
:title="labelDescription"
|
||||
class="gap-1.5 rounded-lg -outline-offset-1 outline outline-1 inline-flex items-center flex-shrink-0"
|
||||
class="rounded-lg -outline-offset-1 outline outline-1 inline-flex items-center flex-shrink-0"
|
||||
:class="[
|
||||
colorClasses,
|
||||
compact ? 'px-1.5 h-6 gap-1 rounded-md' : 'px-2.5 h-8 rounded-lg',
|
||||
compact ? 'px-1.5 h-6 gap-1 rounded-md' : 'px-2.5 h-8 gap-1.5 rounded-lg',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -24,9 +24,11 @@ const onToggle = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-sm py-4 grid gap-2">
|
||||
<div
|
||||
class="text-sm mx-4 grid gap-2 py-3 rounded-[0.625rem] outline outline-1 outline-n-weak -outline-offset-1"
|
||||
>
|
||||
<button
|
||||
class="flex items-center select-none w-full m-0 cursor-grab justify-between drag-handle px-4 py-0 focus-visible:outline-none outline-none !transition-none"
|
||||
class="flex items-center select-none w-full m-0 cursor-grab justify-between drag-handle px-3 py-0 focus-visible:outline-none outline-none !transition-none"
|
||||
@click.stop="onToggle"
|
||||
>
|
||||
<h5 class="text-n-slate-12 text-heading-3 mb-0 py-0 pr-2 pl-0">
|
||||
@@ -47,8 +49,42 @@ const onToggle = () => {
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div v-if="isOpen" :class="compact ? 'px-1' : 'px-4'">
|
||||
<slot />
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-[grid-template-rows,opacity,transform] duration-[400ms] ease-[cubic-bezier(0.23,1,0.32,1)]"
|
||||
enter-from-class="grid-rows-[0fr] opacity-0 scale-[0.96] -translate-y-2"
|
||||
enter-to-class="grid-rows-[1fr] opacity-100 scale-100 translate-y-0"
|
||||
leave-active-class="transition-[grid-template-rows,opacity,transform] duration-[250ms] ease-[cubic-bezier(0.4,0,1,1)]"
|
||||
leave-from-class="grid-rows-[1fr] opacity-100 scale-100"
|
||||
leave-to-class="grid-rows-[0fr] opacity-0 scale-[0.96] -translate-y-1"
|
||||
@before-enter="
|
||||
el => {
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.willChange = 'grid-template-rows, opacity, transform';
|
||||
}
|
||||
"
|
||||
@after-enter="
|
||||
el => {
|
||||
el.style.overflow = 'visible';
|
||||
el.style.willChange = 'auto';
|
||||
}
|
||||
"
|
||||
@before-leave="
|
||||
el => {
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.willChange = 'grid-template-rows, opacity, transform';
|
||||
}
|
||||
"
|
||||
@after-leave="
|
||||
el => {
|
||||
el.style.willChange = 'auto';
|
||||
}
|
||||
"
|
||||
>
|
||||
<div v-if="isOpen" class="grid origin-top">
|
||||
<div class="min-h-0" :class="[compact ? 'px-0' : 'px-3']">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ const maxVisibleFilters = computed(() => {
|
||||
<div
|
||||
id="saveFilterTeleportTarget"
|
||||
class="absolute z-50 top-6"
|
||||
:class="{ 'ltr:right-0 rtl:left-0': isOnExpandedLayout }"
|
||||
:class="{ 'ltr:left-0 rtl:right-0': isOnExpandedLayout }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -86,22 +86,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="hasIssues" class="pt-3 px-3 pb-2">
|
||||
<NextButton
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-plus"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON')"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 px-3">
|
||||
<div v-if="isLoading" class="flex justify-center p-8">
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
<div v-else-if="!hasIssues" class="mt-2 px-3 w-full">
|
||||
<div v-else-if="!hasIssues" class="mt-2 w-full">
|
||||
<SidePanelEmptyState
|
||||
:message="$t('INTEGRATION_SETTINGS.LINEAR.NO_LINKED_ISSUES')"
|
||||
/>
|
||||
@@ -115,16 +105,27 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[300px] overflow-y-auto px-2">
|
||||
<div v-else class="max-h-[300px] overflow-y-auto">
|
||||
<LinearIssueItem
|
||||
v-for="linkedIssue in linkedIssues"
|
||||
:key="linkedIssue.id"
|
||||
class="px-1 pt-3 pb-4 border-b border-n-weak last:border-b-0"
|
||||
class="border-b border-n-weak last:border-b-0"
|
||||
:linked-issue="linkedIssue"
|
||||
@unlink-issue="unlinkIssue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="hasIssues" class="pt-3 w-full">
|
||||
<NextButton
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-plus"
|
||||
class="w-full"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON')"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<woot-modal
|
||||
v-model:show="shouldShowCreateModal"
|
||||
:on-close="closeCreateModal"
|
||||
|
||||
@@ -44,7 +44,7 @@ const unlinkIssue = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4 py-3">
|
||||
<div class="flex flex-col w-full">
|
||||
<IssueHeader
|
||||
:identifier="issue.identifier"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { useDropdownPosition } from '../useDropdownPosition';
|
||||
|
||||
// Mock VueUse composables
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useElementBounding: vi.fn(elementRef => {
|
||||
// Create reactive refs that update based on element
|
||||
const top = ref(0);
|
||||
const right = ref(0);
|
||||
const bottom = ref(100);
|
||||
@@ -13,8 +11,7 @@ vi.mock('@vueuse/core', () => ({
|
||||
const height = ref(50);
|
||||
|
||||
const update = vi.fn(() => {
|
||||
// Update bounds from element when called
|
||||
if (elementRef.value?.getBoundingClientRect) {
|
||||
if (elementRef && elementRef.value?.getBoundingClientRect) {
|
||||
const rect = elementRef.value.getBoundingClientRect();
|
||||
top.value = rect.top;
|
||||
right.value = rect.right;
|
||||
@@ -25,20 +22,9 @@ vi.mock('@vueuse/core', () => ({
|
||||
}
|
||||
});
|
||||
|
||||
// Initial update
|
||||
update();
|
||||
|
||||
return {
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left,
|
||||
width,
|
||||
height,
|
||||
x: left,
|
||||
y: top,
|
||||
update,
|
||||
};
|
||||
return { top, right, bottom, left, width, height, x: left, y: top, update };
|
||||
}),
|
||||
useWindowSize: vi.fn(() => ({
|
||||
width: ref(1024),
|
||||
@@ -53,7 +39,6 @@ describe('useDropdownPosition', () => {
|
||||
let querySelectorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock DOM elements
|
||||
triggerRef = ref({
|
||||
getBoundingClientRect: () => ({
|
||||
top: 100,
|
||||
@@ -78,7 +63,6 @@ describe('useDropdownPosition', () => {
|
||||
|
||||
enabled = ref(true);
|
||||
|
||||
// Spy on document.querySelector for RTL check
|
||||
querySelectorSpy = vi.spyOn(document, 'querySelector').mockReturnValue({
|
||||
getAttribute: () => 'ltr',
|
||||
});
|
||||
@@ -88,74 +72,49 @@ describe('useDropdownPosition', () => {
|
||||
querySelectorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return default position classes when disabled', () => {
|
||||
it('should return default position when disabled', () => {
|
||||
enabled.value = false;
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
expect(positionClasses.value).toBe('top-full mt-2 ltr:left-0 rtl:right-0');
|
||||
expect(position.value.class).toBe('top-full mt-2');
|
||||
expect(position.value.style).toEqual({});
|
||||
});
|
||||
|
||||
it('should return default position classes when refs are null', () => {
|
||||
it('should return default position when refs are null', () => {
|
||||
triggerRef.value = null;
|
||||
dropdownRef.value = null;
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
expect(positionClasses.value).toBe('top-full mt-2 ltr:left-0 rtl:right-0');
|
||||
expect(position.value.class).toBe('top-full mt-2');
|
||||
expect(position.value.style).toEqual({ left: '0px' });
|
||||
});
|
||||
|
||||
it('should calculate position when enabled and refs exist', () => {
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
// Should return some position classes (not the default)
|
||||
expect(positionClasses.value).toBeTruthy();
|
||||
expect(typeof positionClasses.value).toBe('string');
|
||||
expect(position.value.class).toBeTruthy();
|
||||
expect(typeof position.value.class).toBe('string');
|
||||
});
|
||||
|
||||
it('should position dropdown below trigger when enough space', () => {
|
||||
// Trigger at top of screen, plenty of space below
|
||||
// Window height: 768, Trigger bottom: 150
|
||||
// Window height: 768, Trigger bottom: 150, Dropdown height: 200, Margin: 16
|
||||
// Space below: 768 - 150 = 618px
|
||||
// Dropdown height: 200px + SAFE_MARGIN (16px) = 216px
|
||||
// 618 > 216 = true, so should position below (top-full)
|
||||
triggerRef.value.getBoundingClientRect = () => ({
|
||||
top: 100,
|
||||
right: 250,
|
||||
bottom: 150,
|
||||
left: 50,
|
||||
width: 200,
|
||||
height: 50,
|
||||
});
|
||||
// Required: 200 + 16 = 216px
|
||||
// 618 >= 216, so position below
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
|
||||
expect(positionClasses.value).toContain('top-full');
|
||||
expect(positionClasses.value).not.toContain('bottom-full');
|
||||
expect(position.value.class).toContain('top-full');
|
||||
expect(position.value.class).not.toContain('bottom-full');
|
||||
});
|
||||
|
||||
it('should position dropdown above trigger when insufficient space below', () => {
|
||||
// Trigger near bottom of screen
|
||||
// Window height: 768, Trigger bottom: 750
|
||||
// Window height: 768, Trigger bottom: 750, Dropdown height: 200, Margin: 16
|
||||
// Space below: 768 - 750 = 18px
|
||||
// Dropdown height: 200px + SAFE_MARGIN (16px) = 216px
|
||||
// 18 < 216 = true, so should position above (bottom-full)
|
||||
// Required: 200 + 16 = 216px
|
||||
// 18 < 216, so position above
|
||||
triggerRef.value.getBoundingClientRect = () => ({
|
||||
top: 700,
|
||||
right: 250,
|
||||
@@ -165,42 +124,27 @@ describe('useDropdownPosition', () => {
|
||||
height: 50,
|
||||
});
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
expect(positionClasses.value).toContain('bottom-full');
|
||||
expect(positionClasses.value).not.toContain('top-full');
|
||||
expect(position.value.class).toContain('bottom-full');
|
||||
expect(position.value.class).not.toContain('top-full');
|
||||
});
|
||||
|
||||
it('should align dropdown to left by default (LTR)', () => {
|
||||
// LTR layout, trigger at left: 50
|
||||
// Window width: 1024, Dropdown width: 250
|
||||
// Trigger left: 50, Dropdown width: 250, SAFE_MARGIN: 16
|
||||
// Would overflow: 50 + 250 + 16 = 316 < 1024 = false
|
||||
// So should align left (ltr:left-0)
|
||||
querySelectorSpy.mockReturnValue({
|
||||
getAttribute: () => 'ltr',
|
||||
});
|
||||
// Window width: 1024, Trigger left: 50, Dropdown width: 250
|
||||
// Available right: 1024 - 50 = 974px
|
||||
// Overflow: 250 - 974 = -724 (no overflow)
|
||||
// Result: left: 0px
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
|
||||
expect(positionClasses.value).toContain('ltr:left-0');
|
||||
expect(positionClasses.value).not.toContain('ltr:right-0');
|
||||
expect(position.value.style).toEqual({ left: '0px' });
|
||||
});
|
||||
|
||||
it('should align dropdown to right when would overflow (LTR)', () => {
|
||||
// Trigger near right edge
|
||||
// Window width: 1024, Dropdown width: 250
|
||||
// Trigger left: 800, Dropdown width: 250, SAFE_MARGIN: 16
|
||||
// Would overflow: 800 + 250 + 16 = 1066 > 1024 = true
|
||||
// So should align right (ltr:right-0)
|
||||
it('should shift left when dropdown would overflow right edge (LTR)', () => {
|
||||
// Window width: 1024, Trigger left: 800, Dropdown width: 250
|
||||
// Available right: 1024 - 800 = 224px
|
||||
// Overflow: 250 - 224 = 26px
|
||||
// Result: left: -26px
|
||||
triggerRef.value.getBoundingClientRect = () => ({
|
||||
top: 100,
|
||||
right: 1000,
|
||||
@@ -210,14 +154,9 @@ describe('useDropdownPosition', () => {
|
||||
height: 50,
|
||||
});
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
expect(positionClasses.value).toContain('ltr:right-0');
|
||||
expect(positionClasses.value).not.toContain('ltr:left-0');
|
||||
expect(position.value.style).toEqual({ left: '-26px' });
|
||||
});
|
||||
|
||||
it('should handle RTL layout', () => {
|
||||
@@ -225,24 +164,16 @@ describe('useDropdownPosition', () => {
|
||||
getAttribute: () => 'rtl',
|
||||
});
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
expect(positionClasses.value).toContain('rtl:');
|
||||
expect(position.value.style).toHaveProperty('right');
|
||||
});
|
||||
|
||||
it('should reactively update when enabled changes', async () => {
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, enabled);
|
||||
|
||||
// Initially enabled should calculate position
|
||||
const initialValue = positionClasses.value;
|
||||
const initialValue = position.value.class;
|
||||
expect(initialValue).toBeTruthy();
|
||||
expect(typeof initialValue).toBe('string');
|
||||
|
||||
@@ -250,14 +181,15 @@ describe('useDropdownPosition', () => {
|
||||
enabled.value = false;
|
||||
await nextTick();
|
||||
|
||||
expect(positionClasses.value).toBe('top-full mt-2 ltr:left-0 rtl:right-0');
|
||||
expect(position.value.class).toBe('top-full mt-2');
|
||||
expect(position.value.style).toEqual({});
|
||||
|
||||
// Re-enable - should calculate again
|
||||
enabled.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(positionClasses.value).toBeTruthy();
|
||||
expect(typeof positionClasses.value).toBe('string');
|
||||
expect(position.value.class).toBeTruthy();
|
||||
expect(typeof position.value.class).toBe('string');
|
||||
});
|
||||
|
||||
it('should provide updatePosition function', () => {
|
||||
@@ -276,9 +208,9 @@ describe('useDropdownPosition', () => {
|
||||
|
||||
it('should handle optional enabled parameter', () => {
|
||||
// Should work without enabled parameter (backward compatibility)
|
||||
const { positionClasses } = useDropdownPosition(triggerRef, dropdownRef);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef);
|
||||
|
||||
// Should default to enabled (not return default classes)
|
||||
expect(positionClasses.value).toBeTruthy();
|
||||
expect(position.value.class).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,63 +1,82 @@
|
||||
import { computed } from 'vue';
|
||||
import { computed, unref, watch } from 'vue';
|
||||
import { useElementBounding, useWindowSize } from '@vueuse/core';
|
||||
|
||||
const FALLBACK_SIZE = 200;
|
||||
const RTL_DIRECTION = 'rtl';
|
||||
|
||||
/**
|
||||
* Auto-position dropdown based on available space
|
||||
* Auto-position dropdown based on available space within container
|
||||
* Returns reactive position object containing class and style
|
||||
*
|
||||
* @param {Ref} triggerRef - Trigger element ref
|
||||
* @param {Ref} dropdownRef - Dropdown element ref
|
||||
* @param {Ref} enabled - Whether to calculate position
|
||||
* @returns {Object} { positionClasses, updatePosition }
|
||||
* @param {Object} options - { container: Ref, margin: Number }
|
||||
*/
|
||||
export function useDropdownPosition(triggerRef, dropdownRef, enabled) {
|
||||
const SAFE_MARGIN = 16;
|
||||
const RTL = 'rtl';
|
||||
export function useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
enabled,
|
||||
{ container = null, margin = 16 } = {}
|
||||
) {
|
||||
const trigger = useElementBounding(triggerRef);
|
||||
const dropdown = useElementBounding(dropdownRef);
|
||||
const bounds = useElementBounding(container);
|
||||
const { width: winWidth, height: winHeight } = useWindowSize();
|
||||
|
||||
const triggerBounds = useElementBounding(triggerRef);
|
||||
const dropdownBounds = useElementBounding(dropdownRef);
|
||||
const { height: windowHeight, width: windowWidth } = useWindowSize();
|
||||
const isRTL = computed(
|
||||
() =>
|
||||
document.querySelector('#app[dir]')?.getAttribute('dir') === RTL_DIRECTION
|
||||
);
|
||||
|
||||
const positionClasses = computed(() => {
|
||||
// Don't calculate if not enabled
|
||||
if (!enabled?.value) {
|
||||
return 'top-full mt-2 ltr:left-0 rtl:right-0';
|
||||
const position = computed(() => {
|
||||
// Default fallback if not enabled or refs aren't ready
|
||||
if (!unref(enabled)) {
|
||||
return { class: 'top-full mt-2', style: {} };
|
||||
}
|
||||
|
||||
if (!triggerRef.value || !dropdownRef.value) {
|
||||
return 'top-full mt-2 ltr:left-0 rtl:right-0';
|
||||
}
|
||||
const dropdownHeight = dropdown.height.value || FALLBACK_SIZE;
|
||||
const dropdownWidth = dropdown.width.value || FALLBACK_SIZE;
|
||||
|
||||
const spaceBelow = windowHeight.value - triggerBounds.bottom.value;
|
||||
const dropdownHeight = dropdownBounds.height.value || 200;
|
||||
const dropdownWidth = dropdownBounds.width.value || 200;
|
||||
|
||||
// Check if RTL by checking app div with dir attribute
|
||||
const appDiv = document.querySelector('#app[dir]');
|
||||
const isRTL = appDiv?.getAttribute('dir') === RTL;
|
||||
|
||||
// Use appropriate edge based on text direction
|
||||
const triggerEdge = isRTL
|
||||
? triggerBounds.right.value
|
||||
: triggerBounds.left.value;
|
||||
const wouldOverflow = isRTL
|
||||
? triggerEdge - dropdownWidth - SAFE_MARGIN < 0
|
||||
: triggerEdge + dropdownWidth + SAFE_MARGIN > windowWidth.value;
|
||||
|
||||
const vertical =
|
||||
spaceBelow >= dropdownHeight + SAFE_MARGIN
|
||||
/* ---------- Vertical (Tailwind Classes) ---------- */
|
||||
const spaceBelow = winHeight.value - trigger.bottom.value;
|
||||
const className =
|
||||
spaceBelow >= dropdownHeight + margin
|
||||
? 'top-full mt-2'
|
||||
: 'bottom-full mb-2';
|
||||
|
||||
const horizontal = wouldOverflow
|
||||
? 'ltr:right-0 rtl:left-0'
|
||||
: 'ltr:left-0 rtl:right-0';
|
||||
/* ---------- Horizontal (Inline Styles for precise alignment) ---------- */
|
||||
const leftBound = container ? bounds.left.value : 0;
|
||||
const rightBound = container ? bounds.right.value : winWidth.value;
|
||||
|
||||
return `${vertical} ${horizontal}`;
|
||||
const style = {};
|
||||
|
||||
if (isRTL.value) {
|
||||
const availableLeft = trigger.right.value - leftBound;
|
||||
const overflow = dropdownWidth - availableLeft;
|
||||
style.right = overflow > 0 ? `-${overflow}px` : '0px';
|
||||
} else {
|
||||
const availableRight = rightBound - trigger.left.value;
|
||||
const overflow = dropdownWidth - availableRight;
|
||||
style.left = overflow > 0 ? `-${overflow}px` : '0px';
|
||||
}
|
||||
|
||||
return { class: className, style };
|
||||
});
|
||||
|
||||
const updatePosition = () => {
|
||||
triggerBounds.update();
|
||||
dropdownBounds.update();
|
||||
trigger.update();
|
||||
dropdown.update();
|
||||
if (container) bounds.update();
|
||||
};
|
||||
|
||||
return { positionClasses, updatePosition };
|
||||
// Update position when dropdown opens to ensure RTL state is current
|
||||
watch(
|
||||
() => unref(enabled),
|
||||
isEnabled => {
|
||||
if (isEnabled) updatePosition();
|
||||
}
|
||||
);
|
||||
|
||||
return { position, updatePosition };
|
||||
}
|
||||
|
||||
@@ -339,6 +339,8 @@
|
||||
"SELF_ASSIGN": "Self-assign",
|
||||
"TEAM_LABEL": "Team",
|
||||
"PARTICIPANTS_LABEL": "Participants",
|
||||
"LABELS": "Labels",
|
||||
"LABELS_EMPTY_STATE": "No labels added yet",
|
||||
"SELECT": {
|
||||
"PLACEHOLDER": "None"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ const dropdownRef = ref(null);
|
||||
|
||||
const [openAgentsList, toggleAgentsList] = useToggle(false);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
const { position } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
openAgentsList
|
||||
@@ -231,8 +231,8 @@ const handleAgentAction = ({ action, value }) => {
|
||||
:search-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.AGENT')
|
||||
"
|
||||
class="z-[100] w-52 overflow-y-auto max-h-60"
|
||||
:class="positionClasses"
|
||||
class="z-[100] w-52 overflow-y-auto max-h-60 ltr:left-0 rtl:right-0"
|
||||
:class="position.class"
|
||||
@action="handleAgentAction"
|
||||
>
|
||||
<template #icon="{ item }">
|
||||
|
||||
@@ -143,18 +143,18 @@ onMounted(() => {
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div
|
||||
class="ltr:pl-1.5 ltr:pr-2 rtl:pl-2 rtl:pr-1.5 h-6 rounded-md bg-n-button-color outline outline-1 outline-n-container flex items-center"
|
||||
class="ltr:pl-1.5 ltr:pr-2 rtl:pl-2 rtl:pr-1.5 h-6 rounded-md bg-n-label-color outline outline-1 outline-n-label-border -outline-offset-1 flex items-center"
|
||||
>
|
||||
<InboxName
|
||||
v-if="inbox?.id"
|
||||
:inbox="inbox"
|
||||
class="gap-1 [&>span]:text-n-slate-12"
|
||||
class="gap-1 [&>span:last-child]:text-n-slate-12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="px-1.5 h-6 rounded-md bg-n-button-color outline outline-1 outline-n-container flex items-center gap-0.6"
|
||||
class="px-1.5 h-6 rounded-md bg-n-label-color outline outline-1 outline-n-label-border -outline-offset-1 flex items-center gap-1"
|
||||
>
|
||||
<Icon icon="i-lucide-hash" class="size-3.5 text-n-slate-10" />
|
||||
<Icon icon="i-woot-hash" class="size-3.5 text-n-slate-10" />
|
||||
<span class="text-label-small">{{ conversationId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@ onMounted(() => {
|
||||
ghost-class="ghost"
|
||||
handle=".drag-handle"
|
||||
item-key="name"
|
||||
class="flex flex-col divide-y divide-n-weak"
|
||||
class="flex flex-col gap-3 my-4"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
|
||||
@@ -31,28 +31,28 @@ const conversationUiFlags = useMapGetter('conversationLabels/getUIFlags');
|
||||
|
||||
const triggerRef = ref(null);
|
||||
const dropdownRef = ref(null);
|
||||
const wrapperRef = ref(null);
|
||||
const searchQuery = ref('');
|
||||
|
||||
const [openLabelsList, toggleLabels] = useToggle(false);
|
||||
const [createModalVisible, toggleCreateModal] = useToggle(false);
|
||||
const [hasEmptySearchResults, setEmptySearchResults] = useToggle(false);
|
||||
|
||||
const { positionClasses, updatePosition } = useDropdownPosition(
|
||||
const { position, updatePosition } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
openLabelsList
|
||||
openLabelsList,
|
||||
{ container: wrapperRef }
|
||||
);
|
||||
|
||||
// Update position of dropdown when labels change
|
||||
// Update position when labels change
|
||||
watch(
|
||||
activeLabels,
|
||||
async () => {
|
||||
() => activeLabels.value.length,
|
||||
() => {
|
||||
if (openLabelsList.value) {
|
||||
await nextTick();
|
||||
updatePosition();
|
||||
nextTick(() => updatePosition());
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
}
|
||||
);
|
||||
|
||||
const keyboardEvents = {
|
||||
@@ -119,18 +119,24 @@ const hideCreateModal = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-3 w-full items-start pt-3">
|
||||
<div ref="wrapperRef" class="flex flex-wrap gap-2 w-full items-center mb-1">
|
||||
<div class="h-9 flex items-center">
|
||||
<span class="text-sm font-420 text-n-slate-11 truncate whitespace-nowrap">
|
||||
{{ $t('CONVERSATION_SIDEBAR.LABELS') }}
|
||||
</span>
|
||||
</div>
|
||||
<Spinner
|
||||
v-if="conversationUiFlags.isFetching"
|
||||
:size="22"
|
||||
class="text-n-slate-10"
|
||||
/>
|
||||
<div v-else class="flex flex-wrap gap-2.5">
|
||||
<div v-else class="flex flex-wrap gap-2">
|
||||
<Label
|
||||
v-for="(label, index) in activeLabels"
|
||||
:key="label ? label.id : index"
|
||||
data-label
|
||||
:label="label"
|
||||
compact
|
||||
/>
|
||||
<div
|
||||
v-on-click-outside="() => toggleLabels(false)"
|
||||
@@ -140,10 +146,10 @@ const hideCreateModal = () => {
|
||||
ref="triggerRef"
|
||||
:label="$t('CONTACT_PANEL.LABELS.CONVERSATION.ADD_BUTTON')"
|
||||
slate
|
||||
sm
|
||||
icon="i-lucide-plus"
|
||||
xs
|
||||
icon="i-lucide-tag"
|
||||
:variant="openLabelsList ? 'faded' : 'solid'"
|
||||
class="font-460 !-outline-offset-1"
|
||||
class="!-outline-offset-1"
|
||||
@click="toggleLabels()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
@@ -155,7 +161,8 @@ const hideCreateModal = () => {
|
||||
$t('CONTACT_PANEL.LABELS.LABEL_SELECT.PLACEHOLDER')
|
||||
"
|
||||
class="z-[100] w-56 overflow-y-auto max-h-60"
|
||||
:class="positionClasses"
|
||||
:class="position.class"
|
||||
:style="position.style"
|
||||
@action="handleLabelAction"
|
||||
@search="handleSearchUpdate"
|
||||
@empty="handleEmptyResults"
|
||||
|
||||
@@ -94,6 +94,7 @@ onMounted(() => {
|
||||
ghost-class="ghost"
|
||||
handle=".drag-handle"
|
||||
item-key="id"
|
||||
class="px-3"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
|
||||
@@ -29,11 +29,7 @@ const showPreview = ref(false);
|
||||
const triggerRef = ref(null);
|
||||
const dropdownRef = ref(null);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
showPreview
|
||||
);
|
||||
const { position } = useDropdownPosition(triggerRef, dropdownRef, showPreview);
|
||||
|
||||
const executeMacro = async macro => {
|
||||
try {
|
||||
@@ -62,7 +58,7 @@ const closeMacroPreview = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex items-center justify-between leading-4 rounded-md h-10 ltr:pl-1.5 ltr:pr-3 rtl:pl-3 rtl:pr-1.5"
|
||||
class="relative flex items-center justify-between leading-4 rounded-md h-10"
|
||||
:class="showPreview ? 'cursor-default' : 'drag-handle cursor-grab'"
|
||||
>
|
||||
<div class="flex items-center justify-start gap-1">
|
||||
@@ -96,8 +92,8 @@ const closeMacroPreview = () => {
|
||||
ref="dropdownRef"
|
||||
v-on-clickaway="closeMacroPreview"
|
||||
:macro="macro"
|
||||
class="ltr:ml-8 rtl:mr-8 !-mt-8"
|
||||
:class="positionClasses"
|
||||
class="ltr:ml-8 rtl:mr-8 !-mt-8 ltr:left-1 rtl:right-1"
|
||||
:class="position.class"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ const dropdownRef = ref(null);
|
||||
|
||||
const [openParticipantsList, toggleParticipantsList] = useToggle(false);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
const { position } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
openParticipantsList
|
||||
@@ -241,8 +241,8 @@ onMounted(() => {
|
||||
:search-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.AGENT')
|
||||
"
|
||||
class="z-[100] w-52 overflow-y-auto max-h-60"
|
||||
:class="positionClasses"
|
||||
class="z-[100] w-52 overflow-y-auto max-h-60 ltr:left-0 rtl:right-0"
|
||||
:class="position.class"
|
||||
@action="handleParticipantAction"
|
||||
>
|
||||
<template #trailing-icon="{ item }">
|
||||
|
||||
@@ -24,7 +24,7 @@ const dropdownRef = ref(null);
|
||||
|
||||
const [openPriorityList, togglePriorityList] = useToggle(false);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
const { position } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
openPriorityList
|
||||
@@ -171,8 +171,8 @@ const handlePriorityAction = ({ value }) => {
|
||||
:search-placeholder="
|
||||
$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.INPUT_PLACEHOLDER')
|
||||
"
|
||||
class="z-[100] w-48 overflow-y-auto max-h-60"
|
||||
:class="positionClasses"
|
||||
class="z-[100] w-48 overflow-y-auto max-h-60 ltr:left-0 rtl:right-0"
|
||||
:class="position.class"
|
||||
@action="handlePriorityAction"
|
||||
>
|
||||
<template #icon="{ item }">
|
||||
|
||||
@@ -28,7 +28,7 @@ const dropdownRef = ref(null);
|
||||
|
||||
const [openTeamsList, toggleTeamsList] = useToggle(false);
|
||||
|
||||
const { positionClasses } = useDropdownPosition(
|
||||
const { position } = useDropdownPosition(
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
openTeamsList
|
||||
@@ -173,8 +173,8 @@ const handleTeamAction = ({ action, value }) => {
|
||||
:search-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.TEAM')
|
||||
"
|
||||
class="z-[100] w-52 overflow-y-auto max-h-60"
|
||||
:class="positionClasses"
|
||||
class="z-[100] w-52 overflow-y-auto max-h-60 ltr:left-0 rtl:right-0"
|
||||
:class="position.class"
|
||||
@action="handleTeamAction"
|
||||
>
|
||||
<template #trailing-icon="{ item }">
|
||||
|
||||
@@ -98,18 +98,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="notes.length" class="px-3 pt-3 pb-2">
|
||||
<NextButton
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-plus"
|
||||
:label="$t('CONTACTS_LAYOUT.SIDEBAR.NOTES.ADD_NOTE')"
|
||||
:disabled="!contactId || isFetchingNotes"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<div
|
||||
v-if="isFetchingNotes"
|
||||
class="flex items-center justify-center py-8 text-n-slate-11"
|
||||
@@ -146,6 +135,18 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="notes.length" class="pt-3 px-3 w-full">
|
||||
<NextButton
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-plus"
|
||||
:label="$t('CONTACTS_LAYOUT.SIDEBAR.NOTES.ADD_NOTE')"
|
||||
:disabled="!contactId || isFetchingNotes"
|
||||
class="w-full"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<woot-modal
|
||||
v-model:show="shouldShowCreateModal"
|
||||
:on-close="closeCreateModal"
|
||||
|
||||
+6
-1
@@ -174,6 +174,11 @@ export const icons = {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
hash: {
|
||||
body: `<g fill="currentColor" stroke="none"><path d="M11.8125 8.3125H9.1875V5.6875H11.8125C11.9285 5.6875 12.0398 5.64141 12.1219 5.55936C12.2039 5.47731 12.25 5.36603 12.25 5.25C12.25 5.13397 12.2039 5.02269 12.1219 4.94064C12.0398 4.85859 11.9285 4.8125 11.8125 4.8125H9.1875V2.1875C9.1875 2.07147 9.14141 1.96019 9.05936 1.87814C8.97731 1.79609 8.86603 1.75 8.75 1.75C8.63397 1.75 8.52269 1.79609 8.44064 1.87814C8.35859 1.96019 8.3125 2.07147 8.3125 2.1875V4.8125H5.6875V2.1875C5.6875 2.07147 5.64141 1.96019 5.55936 1.87814C5.47731 1.79609 5.36603 1.75 5.25 1.75C5.13397 1.75 5.02269 1.79609 4.94064 1.87814C4.85859 1.96019 4.8125 2.07147 4.8125 2.1875V4.8125H2.1875C2.07147 4.8125 1.96019 4.85859 1.87814 4.94064C1.79609 5.02269 1.75 5.13397 1.75 5.25C1.75 5.36603 1.79609 5.47731 1.87814 5.55936C1.96019 5.64141 2.07147 5.6875 2.1875 5.6875H4.8125V8.3125H2.1875C2.07147 8.3125 1.96019 8.35859 1.87814 8.44064C1.79609 8.52269 1.75 8.63397 1.75 8.75C1.75 8.86603 1.79609 8.97731 1.87814 9.05936C1.96019 9.14141 2.07147 9.1875 2.1875 9.1875H4.8125V11.8125C4.8125 11.9285 4.85859 12.0398 4.94064 12.1219C5.02269 12.2039 5.13397 12.25 5.25 12.25C5.36603 12.25 5.47731 12.2039 5.55936 12.1219C5.64141 12.0398 5.6875 11.9285 5.6875 11.8125V9.1875H8.3125V11.8125C8.3125 11.9285 8.35859 12.0398 8.44064 12.1219C8.52269 12.2039 8.63397 12.25 8.75 12.25C8.86603 12.25 8.97731 12.2039 9.05936 12.1219C9.14141 12.0398 9.1875 11.9285 9.1875 11.8125V9.1875H11.8125C11.9285 9.1875 12.0398 9.14141 12.1219 9.05936C12.2039 8.97731 12.25 8.86603 12.25 8.75C12.25 8.63397 12.2039 8.52269 12.1219 8.44064C12.0398 8.35859 11.9285 8.3125 11.8125 8.3125ZM5.6875 8.3125V5.6875H8.3125V8.3125H5.6875Z" fill="currentColor"/></g>`,
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
|
||||
/** Conversation Status Starts */
|
||||
'status-empty': {
|
||||
@@ -197,7 +202,7 @@ export const icons = {
|
||||
height: 24,
|
||||
},
|
||||
'status-resolved': {
|
||||
body: `<mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><path fill="#d9d9d9" d="M0 0h24v24H0z"/></mask><g mask="url(#a)"><path d="M12 17q2.075 0 3.537-1.463Q17 14.075 17 12t-1.463-3.537Q14.075 7 12 7T8.463 8.463 7 12t1.463 3.537T12 17m0 5a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20" fill="#0d9b8a"/></g>`,
|
||||
body: `<path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="#0D9B8A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M9.2998 12L11.0998 13.8L14.6998 10.2" stroke="#0D9B8A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user