chore: Replace message menu with context menu
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, ref, toRefs } from 'vue';
|
||||
import { onMounted, computed, ref, toRefs, useTemplateRef } from 'vue';
|
||||
import { useTimeoutFn } from '@vueuse/core';
|
||||
import { provideMessageContext } from './provider.js';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
@@ -133,6 +133,7 @@ const props = defineProps({
|
||||
const contextMenuPosition = ref({});
|
||||
const showBackgroundHighlight = ref(false);
|
||||
const showContextMenu = ref(false);
|
||||
const emailBubbleRef = useTemplateRef('emailBubbleRef');
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
@@ -342,6 +343,11 @@ const contextMenuEnabledOptions = computed(() => {
|
||||
delete: hasText || hasAttachments,
|
||||
cannedResponse: isOutgoing && hasText,
|
||||
replyTo: !props.private && props.inboxSupportsReplyTo.outgoing,
|
||||
// Forward email is enabled only when the message is not in progress and is not private and is an email inbox
|
||||
forwardEmail:
|
||||
props.isEmailInbox &&
|
||||
!props.private &&
|
||||
props.status !== MESSAGE_STATUS.PROGRESS,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -362,6 +368,9 @@ const shouldRenderMessage = computed(() => {
|
||||
});
|
||||
|
||||
function openContextMenu(e) {
|
||||
// Close forward modal, when opening context menu
|
||||
emailBubbleRef.value?.closeForwardModal();
|
||||
|
||||
const shouldSkipContextMenu =
|
||||
e.target?.classList.contains('skip-context-menu') ||
|
||||
e.target?.tagName.toLowerCase() === 'a';
|
||||
@@ -437,6 +446,11 @@ const setupHighlightTimer = () => {
|
||||
}, HIGHLIGHT_TIMER);
|
||||
};
|
||||
|
||||
const openForwardModal = (event = null) => {
|
||||
// Open forward modal, with the event from context menu
|
||||
emailBubbleRef.value.openForwardModal(event);
|
||||
};
|
||||
|
||||
onMounted(setupHighlightTimer);
|
||||
|
||||
provideMessageContext({
|
||||
@@ -497,7 +511,12 @@ provideMessageContext({
|
||||
}"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
>
|
||||
<Component :is="componentToRender" />
|
||||
<component
|
||||
:is="componentToRender"
|
||||
:ref="
|
||||
componentToRender === EmailBubble ? 'emailBubbleRef' : undefined
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<MessageError
|
||||
v-if="contentAttributes.externalError"
|
||||
@@ -517,6 +536,7 @@ provideMessageContext({
|
||||
@open="openContextMenu"
|
||||
@close="closeContextMenu"
|
||||
@reply-to="handleReplyTo"
|
||||
@forward-email="openForwardModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownContainer from 'next/dropdown-menu/base/DropdownContainer.vue';
|
||||
import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
|
||||
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
||||
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
||||
|
||||
const emit = defineEmits(['openForward']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('CONVERSATION.MESSAGE_MENU.FORWARD_EMAIL'),
|
||||
value: 'forward',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownContainer>
|
||||
<template #trigger="{ toggle, isOpen }">
|
||||
<NextButton
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
xs
|
||||
slate
|
||||
faded
|
||||
:class="{ 'bg-n-alpha-2': isOpen }"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
<DropdownBody class="top-0 -right-6 min-w-64 z-50" strong>
|
||||
<DropdownSection class="max-h-80 overflow-scroll">
|
||||
<DropdownItem
|
||||
v-for="item in menuItems"
|
||||
:key="item.value"
|
||||
class="!items-start !gap-1 flex-col cursor-pointer"
|
||||
@click="event => emit('openForward', event)"
|
||||
>
|
||||
<template #label>
|
||||
<div class="items-start flex gap-1 flex-col">
|
||||
<span class="text-n-slate-12 text-sm">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownBody>
|
||||
</DropdownContainer>
|
||||
</template>
|
||||
@@ -62,18 +62,15 @@ const showMeta = computed(() => {
|
||||
subject.value
|
||||
);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
showMeta,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
v-show="showMeta"
|
||||
class="space-y-1 rtl:pl-9 ltr:pr-9 text-sm break-words"
|
||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
|
||||
>
|
||||
<div class="space-y-1 rtl:pl-9 w-full ltr:pr-9 text-sm break-words">
|
||||
<template v-if="showMeta">
|
||||
<div
|
||||
v-if="fromEmail[0]"
|
||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'"
|
||||
@@ -103,7 +100,6 @@ defineExpose({
|
||||
{{ $t('EMAIL_HEADER.SUBJECT') }}:
|
||||
{{ subject }}
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -9,10 +9,9 @@ import {
|
||||
} from 'vue';
|
||||
import { Letter } from 'vue-letter';
|
||||
import { allowedCssProperties } from 'lettersanitizer';
|
||||
import { useScrollLock, useToggle } from '@vueuse/core';
|
||||
import { useScrollLock, useWindowSize, useToggle } from '@vueuse/core';
|
||||
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import MessageMenu from 'dashboard/components-next/message/MessageMenu.vue';
|
||||
import { EmailQuoteExtractor } from './removeReply.js';
|
||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
|
||||
@@ -23,10 +22,11 @@ import ForwardMessageForm from 'dashboard/components-next/message/forwardMessage
|
||||
|
||||
import { useMessageContext } from '../../provider.js';
|
||||
import { useInbox } from 'dashboard/composables/useInbox';
|
||||
import { MESSAGE_TYPES, MESSAGE_STATUS } from 'next/message/constants.js';
|
||||
import { MESSAGE_TYPES } from 'next/message/constants.js';
|
||||
import { useTranslations } from 'dashboard/composables/useTranslations';
|
||||
import { calculatePosition } from 'dashboard/helper/position.js';
|
||||
|
||||
const { id, status, content, contentAttributes, attachments, messageType } =
|
||||
const { id, content, contentAttributes, attachments, messageType } =
|
||||
useMessageContext();
|
||||
|
||||
const { inbox } = useInbox();
|
||||
@@ -36,12 +36,12 @@ const isExpanded = ref(false);
|
||||
const showQuotedMessage = ref(false);
|
||||
const renderOriginal = ref(false);
|
||||
const contentContainer = useTemplateRef('contentContainer');
|
||||
const emailMeta = useTemplateRef('emailMeta');
|
||||
|
||||
// Forward form
|
||||
// Forward form - managed locally but can be triggered by parent
|
||||
const [showForwardMessageModal, toggleForwardModal] = useToggle();
|
||||
const forwardFormPosition = reactive({ top: 0, right: 0 });
|
||||
const conversationPanelScrollLock = ref(null);
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
|
||||
onMounted(() => {
|
||||
isExpandable.value = contentContainer.value?.scrollHeight > 400;
|
||||
@@ -52,12 +52,6 @@ const isIncoming = computed(() => !isOutgoing.value);
|
||||
|
||||
const isForwarded = computed(() => contentAttributes.value?.forwardedMessageId);
|
||||
|
||||
const showMessageMenu = computed(
|
||||
() => ![MESSAGE_STATUS.FAILED, MESSAGE_STATUS.PROGRESS].includes(status.value)
|
||||
);
|
||||
|
||||
const showMeta = computed(() => emailMeta.value?.showMeta);
|
||||
|
||||
const { hasTranslations, translationContent } =
|
||||
useTranslations(contentAttributes);
|
||||
|
||||
@@ -126,16 +120,36 @@ const closeForwardModal = () => {
|
||||
conversationPanelScrollLock.value.value = false;
|
||||
};
|
||||
|
||||
const openForwardModal = event => {
|
||||
const openForwardModal = (event = null) => {
|
||||
// Lock conversation panel scroll
|
||||
// To prevent the conversation from scrolling when the forward form is opened
|
||||
const panel = document.querySelector('.conversation-panel');
|
||||
if (panel) conversationPanelScrollLock.value = useScrollLock(panel, true);
|
||||
// Set position from event
|
||||
if (event?.target) {
|
||||
const buttonRect = event.target.getBoundingClientRect();
|
||||
forwardFormPosition.top = buttonRect.top - 9;
|
||||
forwardFormPosition.right = window.innerWidth - buttonRect.right - 9;
|
||||
|
||||
// Form dimensions
|
||||
const [formWidth, formHeight] = [672, 500];
|
||||
const { value: winWidth } = windowWidth;
|
||||
const { value: winHeight } = windowHeight;
|
||||
|
||||
// Position calculation
|
||||
const rect = event?.target?.getBoundingClientRect?.();
|
||||
if (rect) {
|
||||
// Calculate position based on click location of context menu forward button
|
||||
const { left, top } = calculatePosition(
|
||||
rect.left,
|
||||
rect.top,
|
||||
formWidth,
|
||||
formHeight,
|
||||
winWidth,
|
||||
winHeight
|
||||
);
|
||||
|
||||
forwardFormPosition.top = top;
|
||||
forwardFormPosition.right = winWidth - left - formWidth;
|
||||
} else {
|
||||
// Center the form when no trigger event
|
||||
forwardFormPosition.top = Math.max(0, (winHeight - formHeight) / 2);
|
||||
forwardFormPosition.right = Math.max(0, (winWidth - formWidth) / 2);
|
||||
}
|
||||
|
||||
toggleForwardModal(true);
|
||||
@@ -145,6 +159,11 @@ onUnmounted(() => {
|
||||
if (conversationPanelScrollLock.value)
|
||||
conversationPanelScrollLock.value.value = false;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
openForwardModal,
|
||||
closeForwardModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -156,39 +175,13 @@ onUnmounted(() => {
|
||||
}"
|
||||
data-bubble-name="email"
|
||||
>
|
||||
<div
|
||||
v-show="showMeta"
|
||||
<EmailMeta
|
||||
class="p-3"
|
||||
:class="{
|
||||
'border-b border-n-strong': isIncoming,
|
||||
'border-b border-n-slate-8/20': isOutgoing,
|
||||
}"
|
||||
>
|
||||
<EmailMeta ref="emailMeta" class="w-full flex justify-end items-start">
|
||||
<div
|
||||
v-if="showMessageMenu"
|
||||
class="flex gap-2 skip-context-menu flex-shrink-0 items-center relative"
|
||||
>
|
||||
<MessageMenu @open-forward="openForwardModal" />
|
||||
<Teleport to="body">
|
||||
<ForwardMessageForm
|
||||
v-if="showForwardMessageModal"
|
||||
:message="contentAttributes?.email"
|
||||
:content="content"
|
||||
:inbox="inbox"
|
||||
:attachments="attachments"
|
||||
:message-id="id"
|
||||
class="fixed z-50 skip-context-menu"
|
||||
:style="{
|
||||
top: `${forwardFormPosition.top}px`,
|
||||
right: `${forwardFormPosition.right}px`,
|
||||
}"
|
||||
@close="closeForwardModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</EmailMeta>
|
||||
</div>
|
||||
/>
|
||||
|
||||
<section ref="contentContainer" class="p-3">
|
||||
<div
|
||||
@@ -273,6 +266,22 @@ onUnmounted(() => {
|
||||
>
|
||||
<AttachmentChips :attachments="attachments" class="gap-1" />
|
||||
</section>
|
||||
|
||||
<Teleport v-if="showForwardMessageModal" to="body">
|
||||
<ForwardMessageForm
|
||||
:message="contentAttributes?.email"
|
||||
:content="content"
|
||||
:inbox="inbox"
|
||||
:attachments="attachments"
|
||||
:message-id="id"
|
||||
class="fixed z-50 skip-context-menu"
|
||||
:style="{
|
||||
top: `${forwardFormPosition.top}px`,
|
||||
right: `${forwardFormPosition.right}px`,
|
||||
}"
|
||||
@close="closeForwardModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
|
||||
+24
-20
@@ -130,9 +130,11 @@ const handleSendMessage = async () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[42rem] max-h-[31.25rem] overflow-y-scroll divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
|
||||
class="w-[42rem] max-h-[31.25rem] divide-y divide-n-strong transition-all duration-300 ease-in-out top-full justify-between flex flex-col border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl overflow-hidden"
|
||||
>
|
||||
<div class="relative flex-1 px-4 py-3 overflow-y-visible bg-n-alpha-3">
|
||||
<div
|
||||
class="relative flex-1 rounded-t-xl px-4 py-3 overflow-y-visible bg-n-alpha-3"
|
||||
>
|
||||
<div class="flex items-baseline w-full gap-3 min-h-7">
|
||||
<label class="text-sm font-medium text-n-slate-11 whitespace-nowrap">
|
||||
{{ t('FORWARD_MESSAGE_FORM.FROM') }}
|
||||
@@ -162,25 +164,27 @@ const handleSendMessage = async () => {
|
||||
@clear-selected-contact="clearSelectedContact"
|
||||
@update-dropdown="handleDropdownUpdate"
|
||||
/>
|
||||
<EmailMessageEditor
|
||||
v-model="state.message"
|
||||
class="bg-n-alpha-3"
|
||||
:content="content"
|
||||
:is-plain-email="isPlainEmail"
|
||||
:has-quoted-message="hasQuotedMessage"
|
||||
:full-html="fullHtml"
|
||||
:unquoted-html="unquotedHtml"
|
||||
:text-to-show="textToShow"
|
||||
/>
|
||||
<section
|
||||
v-if="Array.isArray(attachments) && attachments.length"
|
||||
class="px-4 pb-4 pt-2 !border-t-0 space-y-2 bg-n-alpha-3"
|
||||
>
|
||||
<AttachmentChips
|
||||
:attachments="attachments"
|
||||
class="gap-1 !justify-start"
|
||||
<div class="overflow-y-scroll">
|
||||
<EmailMessageEditor
|
||||
v-model="state.message"
|
||||
class="bg-n-alpha-3"
|
||||
:content="content"
|
||||
:is-plain-email="isPlainEmail"
|
||||
:has-quoted-message="hasQuotedMessage"
|
||||
:full-html="fullHtml"
|
||||
:unquoted-html="unquotedHtml"
|
||||
:text-to-show="textToShow"
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
v-if="Array.isArray(attachments) && attachments.length"
|
||||
class="px-4 pb-4 pt-2 !border-t-0 space-y-2 bg-n-alpha-3"
|
||||
>
|
||||
<AttachmentChips
|
||||
:attachments="attachments"
|
||||
class="gap-1 !justify-start"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<AttachmentPreviews
|
||||
v-if="state.attachedFiles.length > 0"
|
||||
:attachments="state.attachedFiles"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
|
||||
import { useWindowSize, useElementBounding } from '@vueuse/core';
|
||||
import { calculatePosition } from 'dashboard/helper/position.js';
|
||||
|
||||
const props = defineProps({
|
||||
x: { type: Number, default: 0 },
|
||||
@@ -14,25 +15,6 @@ const menuRef = useTemplateRef('menuRef');
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
|
||||
|
||||
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
|
||||
// Initial position
|
||||
let left = x;
|
||||
let top = y;
|
||||
|
||||
// Boundary checks
|
||||
const isOverflowingRight = left + menuW > windowW;
|
||||
const isOverflowingBottom = top + menuH > windowH;
|
||||
|
||||
// Adjust position if overflowing
|
||||
if (isOverflowingRight) left = windowW - menuW;
|
||||
if (isOverflowingBottom) top = windowH - menuH;
|
||||
|
||||
return {
|
||||
left: Math.max(0, left),
|
||||
top: Math.max(0, top),
|
||||
};
|
||||
};
|
||||
|
||||
const position = computed(() => {
|
||||
if (!menuRef.value) return { top: `${props.y}px`, left: `${props.x}px` };
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Calculates the optimal position for a popup element to ensure it remains within viewport bounds
|
||||
*
|
||||
* @param {number} x - The initial x-coordinate (left position) for the element
|
||||
* @param {number} y - The initial y-coordinate (top position) for the element
|
||||
* @param {number} menuW - The width of the element to position
|
||||
* @param {number} menuH - The height of the element to position
|
||||
* @param {number} windowW - The width of the viewport
|
||||
* @param {number} windowH - The height of the viewport
|
||||
* @param {number} [padding=16] - Minimum padding from viewport edges (in pixels)
|
||||
* @returns {Object} An object containing the adjusted { left, top } coordinates
|
||||
*/
|
||||
export const calculatePosition = (
|
||||
x,
|
||||
y,
|
||||
menuW,
|
||||
menuH,
|
||||
windowW,
|
||||
windowH,
|
||||
padding = 16
|
||||
) => {
|
||||
// Initial position
|
||||
let left = x;
|
||||
let top = y;
|
||||
|
||||
// Boundary checks
|
||||
const isOverflowingRight = left + menuW > windowW - padding;
|
||||
const isOverflowingBottom = top + menuH > windowH - padding;
|
||||
|
||||
// Adjust position if overflowing
|
||||
if (isOverflowingRight) left = windowW - menuW - padding;
|
||||
if (isOverflowingBottom) top = windowH - menuH - padding;
|
||||
|
||||
return {
|
||||
left: Math.max(padding, left),
|
||||
top: Math.max(padding, top),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
import { calculatePosition } from '../position';
|
||||
|
||||
describe('calculatePosition', () => {
|
||||
const WINDOW_WIDTH = 1024;
|
||||
const WINDOW_HEIGHT = 768;
|
||||
const MENU_WIDTH = 400;
|
||||
const MENU_HEIGHT = 300;
|
||||
const DEFAULT_PADDING = 16;
|
||||
|
||||
test('returns the original position when element fits within viewport', () => {
|
||||
const x = 100;
|
||||
const y = 100;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: x, top: y });
|
||||
});
|
||||
|
||||
test('adjusts position when overflowing right edge', () => {
|
||||
const x = WINDOW_WIDTH - 100; // Too far right
|
||||
const y = 100;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
const expectedLeft = WINDOW_WIDTH - MENU_WIDTH - DEFAULT_PADDING;
|
||||
expect(result).toEqual({ left: expectedLeft, top: y });
|
||||
});
|
||||
|
||||
test('adjusts position when overflowing bottom edge', () => {
|
||||
const x = 100;
|
||||
const y = WINDOW_HEIGHT - 100; // Too far down
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
const expectedTop = WINDOW_HEIGHT - MENU_HEIGHT - DEFAULT_PADDING;
|
||||
expect(result).toEqual({ left: x, top: expectedTop });
|
||||
});
|
||||
|
||||
test('adjusts position when overflowing both right and bottom edges', () => {
|
||||
const x = WINDOW_WIDTH - 100; // Too far right
|
||||
const y = WINDOW_HEIGHT - 100; // Too far down
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
const expectedLeft = WINDOW_WIDTH - MENU_WIDTH - DEFAULT_PADDING;
|
||||
const expectedTop = WINDOW_HEIGHT - MENU_HEIGHT - DEFAULT_PADDING;
|
||||
expect(result).toEqual({ left: expectedLeft, top: expectedTop });
|
||||
});
|
||||
|
||||
test('ensures minimum padding from left edge', () => {
|
||||
const x = -50; // Position would be off-screen to the left
|
||||
const y = 100;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: DEFAULT_PADDING, top: y });
|
||||
});
|
||||
|
||||
test('ensures minimum padding from top edge', () => {
|
||||
const x = 100;
|
||||
const y = -50; // Position would be off-screen at the top
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: x, top: DEFAULT_PADDING });
|
||||
});
|
||||
|
||||
test('handles case when element is larger than viewport width', () => {
|
||||
const x = 100;
|
||||
const y = 100;
|
||||
const largeWidth = WINDOW_WIDTH + 200; // Wider than window
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
largeWidth,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
// Should position at minimum padding
|
||||
expect(result).toEqual({ left: DEFAULT_PADDING, top: y });
|
||||
});
|
||||
|
||||
test('handles case when element is larger than viewport height', () => {
|
||||
const x = 100;
|
||||
const y = 100;
|
||||
const largeHeight = WINDOW_HEIGHT + 200; // Taller than window
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
largeHeight,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
// Should position at minimum padding
|
||||
expect(result).toEqual({ left: x, top: DEFAULT_PADDING });
|
||||
});
|
||||
|
||||
test('accepts custom padding value', () => {
|
||||
const x = WINDOW_WIDTH - 100;
|
||||
const y = WINDOW_HEIGHT - 100;
|
||||
const customPadding = 32;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT,
|
||||
customPadding
|
||||
);
|
||||
|
||||
const expectedLeft = WINDOW_WIDTH - MENU_WIDTH - customPadding;
|
||||
const expectedTop = WINDOW_HEIGHT - MENU_HEIGHT - customPadding;
|
||||
expect(result).toEqual({ left: expectedLeft, top: expectedTop });
|
||||
});
|
||||
|
||||
test('ensures custom minimum padding from edges', () => {
|
||||
const x = -50;
|
||||
const y = -50;
|
||||
const customPadding = 32;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT,
|
||||
customPadding
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: customPadding, top: customPadding });
|
||||
});
|
||||
});
|
||||
@@ -227,6 +227,7 @@
|
||||
"TRANSLATE": "Translate",
|
||||
"COPY_PERMALINK": "Copy link to the message",
|
||||
"LINK_COPIED": "Message URL copied to the clipboard",
|
||||
"FORWARD_EMAIL": "Forward email",
|
||||
"DELETE_CONFIRMATION": {
|
||||
"TITLE": "Are you sure you want to delete this message?",
|
||||
"MESSAGE": "You cannot undo this action",
|
||||
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['open', 'close', 'replyTo'],
|
||||
emits: ['open', 'close', 'replyTo', 'forwardEmail'],
|
||||
setup() {
|
||||
const { getPlainText } = useMessageFormatter();
|
||||
return {
|
||||
@@ -115,6 +115,10 @@ export default {
|
||||
handleClose(e) {
|
||||
this.$emit('close', e);
|
||||
},
|
||||
openForwardModal(event) {
|
||||
this.handleClose();
|
||||
this.$emit('forwardEmail', event);
|
||||
},
|
||||
handleTranslate() {
|
||||
const { locale } = this.getAccount(this.currentAccountId);
|
||||
this.$store.dispatch('translateMessage', {
|
||||
@@ -238,6 +242,15 @@ export default {
|
||||
variant="icon"
|
||||
@click.stop="showCannedResponseModal"
|
||||
/>
|
||||
<MenuItem
|
||||
v-if="enabledOptions['forwardEmail']"
|
||||
:option="{
|
||||
icon: 'forward',
|
||||
label: $t('CONVERSATION.CONTEXT_MENU.FORWARD_EMAIL'),
|
||||
}"
|
||||
variant="icon"
|
||||
@click.stop="openForwardModal"
|
||||
/>
|
||||
<hr v-if="enabledOptions['delete']" />
|
||||
<MenuItem
|
||||
v-if="enabledOptions['delete']"
|
||||
|
||||
@@ -285,5 +285,6 @@
|
||||
"M9.60364 9.20645C9.60364 8.67008 10.0385 8.23523 10.5749 8.23523C11.1113 8.23523 11.5461 8.67008 11.5461 9.20645V11.4511C11.5461 11.9875 11.1113 12.4223 10.5749 12.4223C10.0385 12.4223 9.60364 11.9875 9.60364 11.4511V9.20645Z",
|
||||
"M17.1442 5.57049C13.5275 5.06019 10.5793 5.04007 6.88135 5.56825C5.9466 5.70176 5.32812 5.79197 4.85654 5.92976C4.41928 6.05757 4.17061 6.20994 3.96492 6.43984C3.539 6.91583 3.48286 7.45419 3.4248 9.33184C3.36775 11.1772 3.48076 12.831 3.69481 14.6918C3.80887 15.6834 3.88736 16.3526 4.01268 16.8613C4.13155 17.3439 4.27532 17.6034 4.47513 17.802C4.67654 18.0023 4.93467 18.1435 5.40841 18.2581C5.90952 18.3793 6.56702 18.4526 7.5442 18.5592C10.7045 18.904 13.0702 18.9022 16.2423 18.561C17.2313 18.4546 17.8995 18.3813 18.4081 18.2609C18.8913 18.1465 19.1511 18.0063 19.3497 17.8118C19.5442 17.6213 19.6928 17.3587 19.8217 16.852C19.9561 16.3234 20.0476 15.624 20.18 14.5966C20.4162 12.7633 20.5863 11.1533 20.5929 9.3896C20.5999 7.50391 20.5613 6.96737 20.1306 6.46971C19.9226 6.22932 19.6696 6.0713 19.2224 5.93968C18.7395 5.79754 18.1042 5.70594 17.1442 5.57049ZM6.65555 3.98715C10.5078 3.43695 13.6072 3.45849 17.3674 3.98902L17.4224 3.99678C18.3127 4.12235 19.0648 4.22844 19.6733 4.40753C20.33 4.60078 20.8792 4.89417 21.3382 5.4245C22.2041 6.42482 22.1984 7.6117 22.1909 9.18858C22.1905 9.25686 22.1902 9.32584 22.19 9.3956C22.183 11.2604 22.0026 12.949 21.764 14.8006L21.7577 14.8496C21.6332 15.8159 21.5307 16.6121 21.3695 17.2458C21.2 17.9121 20.9467 18.4833 20.4672 18.9529C19.9919 19.4183 19.4302 19.6602 18.776 19.8151C18.1582 19.9613 17.3895 20.044 16.4629 20.1436L16.4131 20.149C13.1283 20.5023 10.6472 20.5043 7.37097 20.1469L7.32043 20.1414C6.40679 20.0417 5.64604 19.9587 5.03292 19.8104C4.38112 19.6527 3.82317 19.406 3.34911 18.9347C2.87346 18.4618 2.62363 17.8999 2.46191 17.2433C2.30938 16.6241 2.22071 15.8531 2.11393 14.9246L2.10815 14.8743C1.88863 12.9659 1.76823 11.23 1.82845 9.28246C1.83063 9.2118 1.83272 9.14191 1.83479 9.07281C1.8816 7.50776 1.91671 6.33374 2.7747 5.37486C3.22992 4.86612 3.76798 4.58399 4.40853 4.39678C5.00257 4.22316 5.73505 4.11858 6.60207 3.99479C6.61981 3.99225 6.63764 3.9897 6.65555 3.98715Z"
|
||||
],
|
||||
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0"
|
||||
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0",
|
||||
"forward-outline": "M14.72 6.28a.75.75 0 0 1 1.06-1.06l5 5a.75.75 0 0 1 0 1.06l-5 5a.75.75 0 1 1-1.06-1.06l3.72-3.72h-7.69a6.25 6.25 0 0 0-6.25 6.25v.5a.75.75 0 0 1-1.5 0v-.5A7.75 7.75 0 0 1 10.75 10h7.69z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user