diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue b/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
index 351cc7071..0bc1e376c 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
@@ -107,10 +107,11 @@ const closeMobileSidebar = () => {
size="sm"
/>
-
+
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
index 773867de1..7f9047f8d 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
@@ -114,8 +114,8 @@ const emit = defineEmits([
-
-
+
+
diff --git a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
index 1ab370901..04340eb7d 100644
--- a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
@@ -2,10 +2,13 @@
import { reactive, ref, computed, onMounted, watch } from 'vue';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
+import { useWindowSize } from '@vueuse/core';
import { useUISettings } from 'dashboard/composables/useUISettings';
+import { vOnClickOutside } from '@vueuse/components';
import { useAlert } from 'dashboard/composables';
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
import { debounce } from '@chatwoot/utils';
+import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { emitter } from 'shared/helpers/mitt';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
@@ -15,18 +18,22 @@ import {
processContactableInboxes,
mergeInboxDetails,
} from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
+import wootConstants from 'dashboard/constants/globals';
-import Popover from 'dashboard/components-next/popover/Popover.vue';
import ComposeNewConversationForm from 'dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue';
const props = defineProps({
+ alignPosition: {
+ type: String,
+ default: 'left',
+ },
contactId: {
type: String,
default: null,
},
- align: {
- type: String,
- default: 'end',
+ isModal: {
+ type: Boolean,
+ default: false,
},
});
@@ -35,16 +42,23 @@ const emit = defineEmits(['close']);
const searchContacts = createContactSearcher();
const store = useStore();
const { t } = useI18n();
+const { width: windowWidth } = useWindowSize();
const { fetchSignatureFlagFromUISettings } = useUISettings();
-const popoverRef = ref(null);
+const isSmallScreen = computed(
+ () => windowWidth.value < wootConstants.SMALL_SCREEN_BREAKPOINT
+);
+
+const viewInModal = computed(() => props.isModal || isSmallScreen.value);
+
const contacts = ref([]);
const selectedContact = ref(null);
const targetInbox = ref(null);
const isCreatingContact = ref(false);
const isFetchingInboxes = ref(false);
const isSearching = ref(false);
+const showComposeNewConversation = ref(false);
const formState = reactive({
message: '',
@@ -81,6 +95,14 @@ const directUploadsEnabled = computed(
const activeContact = computed(() => contactById.value(props.contactId));
+const composePopoverClass = computed(() => {
+ if (viewInModal.value) return '';
+
+ return props.alignPosition === 'right'
+ ? 'absolute ltr:left-0 ltr:right-[unset] rtl:right-0 rtl:left-[unset]'
+ : 'absolute rtl:left-0 rtl:right-[unset] ltr:right-0 ltr:left-[unset]';
+});
+
const onContactSearch = debounce(
async query => {
isSearching.value = true;
@@ -150,7 +172,7 @@ const clearSelectedContact = () => {
};
const closeCompose = () => {
- popoverRef.value?.hide();
+ showComposeNewConversation.value = false;
if (!props.contactId) {
// If contactId is passed as prop
// Then don't allow to remove the selected contact
@@ -158,6 +180,7 @@ const closeCompose = () => {
}
targetInbox.value = null;
resetContacts();
+ emit('close');
};
const discardCompose = () => {
@@ -190,15 +213,8 @@ const createConversation = async ({ payload, isFromWhatsApp }) => {
}
};
-const onPopoverShow = () => {
- // Flag to prevent triggering drag n drop,
- // When compose modal is active
- emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, true);
-};
-
-const onPopoverHide = () => {
- emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
- emit('close');
+const toggle = () => {
+ showComposeNewConversation.value = !showComposeNewConversation.value;
};
watch(
@@ -226,22 +242,64 @@ watch(
{ immediate: true, deep: true }
);
+const handleClickOutside = () => {
+ if (!showComposeNewConversation.value) return;
+
+ showComposeNewConversation.value = false;
+ emit('close');
+};
+
+const onModalBackdropClick = () => {
+ if (!viewInModal.value) return;
+ handleClickOutside();
+};
+
onMounted(() => resetContacts());
+
+const keyboardEvents = {
+ Escape: {
+ action: () => {
+ if (showComposeNewConversation.value) {
+ showComposeNewConversation.value = false;
+ emit('close');
+ emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
+ }
+ },
+ },
+};
+
+useKeyboardEvents(keyboardEvents);
-
-
-
-
-
+
+
resetContacts());
@create-conversation="createConversation"
@discard="discardCompose"
/>
-
-
+
+
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue
index c4111b481..455abf996 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue
@@ -361,7 +361,7 @@ useKeyboardEvents({
-import { ref, computed, watch, nextTick } from 'vue';
-import { vOnClickOutside } from '@vueuse/components';
-import { useBreakpoints, breakpointsTailwind } from '@vueuse/core';
-import { useDropdownPosition } from 'dashboard/composables/useDropdownPosition';
-import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
-import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
-
-const props = defineProps({
- align: {
- type: String,
- default: 'end',
- validator: v => ['start', 'end'].includes(v),
- },
-});
-
-const emit = defineEmits(['show', 'hide']);
-
-const isActive = ref(false);
-const triggerRef = ref(null);
-const popoverRef = ref(null);
-const mobileContentRef = ref(null);
-
-const breakpoints = useBreakpoints(breakpointsTailwind);
-const isMobile = breakpoints.smaller('md');
-const showPopover = computed(() => isActive.value && !isMobile.value);
-
-const { fixedPosition, updatePosition } = useDropdownPosition(
- triggerRef,
- popoverRef,
- showPopover,
- { align: props.align }
-);
-
-const show = async () => {
- isActive.value = true;
- if (!isMobile.value) {
- await nextTick();
- updatePosition();
- }
- emit('show');
-};
-
-const hide = () => {
- if (!isActive.value) return;
- isActive.value = false;
- emit('hide');
-};
-
-const toggle = async () => {
- if (isActive.value) hide();
- else await show();
-};
-
-// Recalculate position when switching from mobile to desktop while open
-watch(isMobile, async mobile => {
- if (!isActive.value || mobile) return;
- await nextTick();
- updatePosition();
-});
-
-const handleClickOutside = event => {
- if (triggerRef.value?.contains(event.target)) return;
- hide();
-};
-
-// Selectors for teleported elements that should not trigger close
-const clickOutsideIgnore = [
- 'dialog.ProseMirror-prompt-backdrop',
- '[data-popover-content]',
-];
-
-useKeyboardEvents({
- Escape: {
- action: () => isActive.value && hide(),
- allowOnFocusedInput: true,
- },
-});
-
-defineExpose({ show, hide, toggle });
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
index cbb3b2099..9fd25c481 100644
--- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
+++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
@@ -10,6 +10,8 @@ import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
import { vOnClickOutside } from '@vueuse/components';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useWindowSize, useEventListener } from '@vueuse/core';
+import { emitter } from 'shared/helpers/mitt';
+import { BUS_EVENTS } from 'shared/constants/busEvents';
import Button from 'dashboard/components-next/button/Button.vue';
import SidebarGroup from './SidebarGroup.vue';
@@ -182,6 +184,15 @@ const closeMobileSidebar = () => {
emit('closeMobileSidebar');
};
+const onComposeOpen = toggleFn => {
+ toggleFn();
+ emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, true);
+};
+
+const onComposeClose = () => {
+ emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
+};
+
const newReportRoutes = () => [
{
name: 'Reports Agent',
@@ -723,13 +734,7 @@ const menuItems = computed(() => {
-
-
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue
index 89b01cb8d..00c944f9e 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue
@@ -41,16 +41,7 @@ const closeContactPanel = () => {
';
- });
-
- describe('verticalClass (relative mode)', () => {
- it('places below when enough space', () => {
- // Trigger at y=100, dropdown 200px tall
- // Space below = 768 - 140 = 628 → fits (628 > 216)
- setTrigger({ top: 100, bottom: 140 });
- setDropdown({ height: 200 });
-
- const { position } = useDropdownPosition(ref(null), ref(null), ref(true));
- expect(position.value.class).toBe('top-full mt-2');
- });
-
- it('places above when not enough space below but enough above', () => {
- // Trigger near bottom at y=600
- // Space below = 128 → doesn't fit. Space above = 600 → fits
- setTrigger({ top: 600, bottom: 640 });
- setDropdown({ height: 200 });
-
- const { position } = useDropdownPosition(ref(null), ref(null), ref(true));
- expect(position.value.class).toBe('bottom-full mb-2');
- });
-
- it('picks the side with more space when dropdown fits neither', () => {
- // Dropdown 500px tall, won't fit above (300) or below (428)
- // Below has more room → stays below
- setTrigger({ top: 300, bottom: 340 });
- setDropdown({ height: 500 });
-
- const { position } = useDropdownPosition(ref(null), ref(null), ref(true));
- expect(position.value.class).toBe('top-full mt-2');
- });
-
- it('picks above when above has more space and neither fits', () => {
- // Dropdown 600px tall, won't fit above (500) or below (228)
- // Above has more room → flips above
- setTrigger({ top: 500, bottom: 540 });
- setDropdown({ height: 600 });
-
- const { position } = useDropdownPosition(ref(null), ref(null), ref(true));
- expect(position.value.class).toBe('bottom-full mb-2');
- });
-
- it('returns default when disabled', () => {
- setTrigger({ top: 700, bottom: 740 });
- setDropdown({ height: 200 });
-
- const { position } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(false)
- );
- expect(position.value.class).toBe('top-full mt-2');
- expect(position.value.style).toEqual({});
- });
- });
-
- describe('fixedPosition', () => {
- it('places below with correct top and maxHeight', () => {
- // Trigger at y=140, space below = 628
- // top = 140 + 8(gap) = 148
- // maxHeight = 628 - 8(gap) - 16(margin) = 604
- setTrigger({ top: 100, bottom: 140 });
- setDropdown({ height: 200, width: 200 });
-
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(true)
- );
-
- expect(fixedPosition.value.style.top).toBe('148px');
- expect(fixedPosition.value.style.bottom).toBeUndefined();
- expect(fixedPosition.value.style.maxHeight).toBe('604px');
- });
-
- it('flips above with correct bottom and maxHeight', () => {
- // Trigger near bottom at y=650, space below = 78 → doesn't fit
- // Flips above: bottom = 768 - 650 + 8 = 126
- // maxHeight = 650 - 8 - 16 = 626
- setTrigger({ top: 650, bottom: 690 });
- setDropdown({ height: 200, width: 200 });
-
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(true)
- );
-
- expect(fixedPosition.value.style.bottom).toBe('126px');
- expect(fixedPosition.value.style.top).toBeUndefined();
- expect(fixedPosition.value.style.maxHeight).toBe('626px');
- });
-
- it('constrains maxHeight to available space on short viewports', () => {
- // Short viewport (400px), trigger in the middle, dropdown 500px tall
- // Neither side fits → above (200) > below (160) → places above
- // maxHeight capped to 200 - 8 - 16 = 176
- winHeight.value = 400;
- setTrigger({ top: 200, bottom: 240 });
- setDropdown({ height: 500, width: 200 });
-
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(true)
- );
-
- expect(fixedPosition.value.style.bottom).toBeDefined();
- expect(fixedPosition.value.style.maxHeight).toBe('176px');
- });
-
- it('returns defaults when disabled', () => {
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(false)
- );
- expect(fixedPosition.value.class).toBe('fixed z-[9999]');
- expect(fixedPosition.value.style).toEqual({});
- });
- });
-
- describe('horizontal positioning (fixedPosition)', () => {
- it('anchors to the right edge by default (align=end, LTR)', () => {
- // align=end + LTR → anchorLeft=false → uses style.right
- // right = 1024 - 900 = 124
- setTrigger({ top: 100, bottom: 140, left: 800, right: 900 });
- setDropdown({ height: 100, width: 200 });
-
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(true)
- );
-
- expect(fixedPosition.value.style.right).toBe('124px');
- });
-
- it('anchors to the left edge when align=start (LTR)', () => {
- // align=start + LTR → anchorLeft=true → uses style.left
- setTrigger({ top: 100, bottom: 140, left: 100, right: 200 });
- setDropdown({ height: 100, width: 200 });
-
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(true),
- { align: 'start' }
- );
-
- expect(fixedPosition.value.style.left).toBe('100px');
- });
-
- it('shifts left when dropdown overflows right edge', () => {
- // Trigger at x=900, dropdown 300px wide → 900+300=1200 > 1024
- // Falls back to right: 16px (margin)
- setTrigger({ top: 100, bottom: 140, left: 900, right: 1000 });
- setDropdown({ height: 100, width: 300 });
-
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(true),
- { align: 'start' }
- );
-
- expect(fixedPosition.value.style.right).toBe('16px');
- });
- });
-
- describe('RTL', () => {
- beforeEach(() => {
- document.body.innerHTML = '';
- });
-
- it('flips anchor direction in RTL (align=end anchors left)', () => {
- // align=end + RTL → anchorLeft=true → uses style.left
- setTrigger({ top: 100, bottom: 140, left: 100, right: 200 });
- setDropdown({ height: 100, width: 200 });
-
- const { fixedPosition } = useDropdownPosition(
- ref(null),
- ref(null),
- ref(true)
- );
-
- expect(fixedPosition.value.style.left).toBe('100px');
- });
- });
-});
diff --git a/app/javascript/dashboard/composables/useDropdownPosition.js b/app/javascript/dashboard/composables/useDropdownPosition.js
deleted file mode 100644
index 93afc30b1..000000000
--- a/app/javascript/dashboard/composables/useDropdownPosition.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import { computed, unref, watch } from 'vue';
-import { useElementBounding, useWindowSize } from '@vueuse/core';
-
-const FALLBACK_SIZE = 200;
-const SAFE_MARGIN = 16;
-const GAP = 8;
-
-/**
- * Auto-position a floating element based on available viewport space.
- *
- * @param {Ref} triggerRef - Trigger element ref
- * @param {Ref} dropdownRef - Dropdown/popover element ref
- * @param {Ref} enabled - Whether to calculate position
- * @param {Object} options
- * @param {Ref} [options.container] - Constraining container ref
- * @param {number} [options.margin=16] - Min distance from viewport/container edges
- * @param {string} [options.align='end'] - 'start' or 'end' (flips automatically for RTL)
- */
-export function useDropdownPosition(
- triggerRef,
- dropdownRef,
- enabled,
- { container = null, margin = SAFE_MARGIN, align = 'end' } = {}
-) {
- const trigger = useElementBounding(triggerRef);
- const dropdown = useElementBounding(dropdownRef);
- const bounds = useElementBounding(container);
- const { width: winWidth, height: winHeight } = useWindowSize();
-
- const isRTL = computed(
- () => document.querySelector('#app[dir]')?.getAttribute('dir') === 'rtl'
- );
-
- // Whether to anchor to the left edge of the trigger
- const anchorLeft = computed(() => (align === 'start') !== isRTL.value);
-
- const verticalClass = computed(() => {
- if (!unref(enabled)) return 'top-full mt-2';
- const dh = dropdown.height.value || FALLBACK_SIZE;
- const spaceBelow = winHeight.value - trigger.bottom.value;
- const spaceAbove = trigger.top.value;
- // Only flip above if it fits there; otherwise stay below (more room or equal)
- if (spaceBelow >= dh + margin) return 'top-full mt-2';
- if (spaceAbove >= dh + margin) return 'bottom-full mb-2';
- return spaceBelow >= spaceAbove ? 'top-full mt-2' : 'bottom-full mb-2';
- });
-
- // Relative mode: Tailwind class + style for absolute-in-parent dropdowns
- const position = computed(() => {
- if (!unref(enabled)) return { class: 'top-full mt-2', style: {} };
-
- const dw = dropdown.width.value || FALLBACK_SIZE;
- const leftBound = container ? bounds.left.value : 0;
- const rightBound = container ? bounds.right.value : winWidth.value;
- const style = {};
-
- if (anchorLeft.value) {
- const available = rightBound - trigger.left.value;
- const overflow = dw - available;
- style.left = overflow > 0 ? `-${overflow}px` : '0px';
- } else {
- const available = trigger.right.value - leftBound;
- const overflow = dw - available;
- style.right = overflow > 0 ? `-${overflow}px` : '0px';
- }
-
- return { class: verticalClass.value, style };
- });
-
- // Fixed mode: styles for teleported popovers
- const fixedPosition = computed(() => {
- if (!unref(enabled)) return { class: 'fixed z-[9999]', style: {} };
-
- const dh = dropdown.height.value || FALLBACK_SIZE;
- const dw = dropdown.width.value || FALLBACK_SIZE;
- const spaceBelow = winHeight.value - trigger.bottom.value;
- const style = {};
-
- // Vertical: prefer below, flip above only if it fits, else pick the larger side
- const spaceAbove = trigger.top.value;
- const placeAbove =
- spaceBelow < dh + margin &&
- (spaceAbove >= dh + margin || spaceAbove > spaceBelow);
-
- if (placeAbove) {
- style.bottom = `${winHeight.value - trigger.top.value + GAP}px`;
- style.maxHeight = `${spaceAbove - GAP - margin}px`;
- } else {
- style.top = `${trigger.bottom.value + GAP}px`;
- style.maxHeight = `${spaceBelow - GAP - margin}px`;
- }
-
- // Horizontal
- if (anchorLeft.value) {
- const left = trigger.left.value;
- if (left + dw > winWidth.value - margin) {
- style.right = `${margin}px`;
- } else {
- style.left = `${Math.max(margin, left)}px`;
- }
- } else {
- const right = winWidth.value - trigger.right.value;
- if (trigger.right.value - dw < margin) {
- style.left = `${margin}px`;
- } else {
- style.right = `${right}px`;
- }
- }
-
- return { class: 'fixed z-[9999]', style };
- });
-
- const updatePosition = () => {
- trigger.update();
- dropdown.update();
- if (container) bounds.update();
- };
-
- // Update position when dropdown opens to ensure RTL state is current
- watch(
- () => unref(enabled),
- isEnabled => {
- if (isEnabled) updatePosition();
- }
- );
-
- return { position, fixedPosition, updatePosition };
-}
diff --git a/app/javascript/dashboard/modules/contact/ContactDeleteModal.vue b/app/javascript/dashboard/modules/contact/ContactDeleteModal.vue
deleted file mode 100644
index 3bf48e184..000000000
--- a/app/javascript/dashboard/modules/contact/ContactDeleteModal.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ $t('DELETE_CONTACT.CONFIRM.TITLE') }}
-
-
- {{ confirmMessage }}
-
-
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue
index 1a343f777..8db16d9e5 100644
--- a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue
+++ b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue
@@ -5,8 +5,8 @@ import { useStore } from 'vuex';
import { useAlert, useTrack } from 'dashboard/composables';
import { useMapGetter } from 'dashboard/composables/store';
-import Popover from 'dashboard/components-next/popover/Popover.vue';
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
+import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import ContactAPI from 'dashboard/api/contacts';
import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events';
@@ -23,6 +23,7 @@ const { t } = useI18n();
const store = useStore();
const uiFlags = useMapGetter('contacts/getUIFlags');
+const dialogRef = ref(null);
const isSearching = ref(false);
const searchResults = ref([]);
@@ -34,6 +35,21 @@ watch(
}
);
+const open = () => {
+ dialogRef.value?.open();
+};
+
+const close = () => {
+ dialogRef.value?.close();
+};
+
+defineExpose({ open, close });
+
+const onClose = () => {
+ close();
+ emit('close');
+};
+
const onContactSearch = async query => {
isSearching.value = true;
searchResults.value = [];
@@ -52,7 +68,7 @@ const onContactSearch = async query => {
}
};
-const onMergeContacts = async (parentContactId, hide) => {
+const onMergeContacts = async parentContactId => {
useTrack(CONTACTS_EVENTS.MERGED_CONTACTS);
try {
await store.dispatch('contacts/merge', {
@@ -60,7 +76,7 @@ const onMergeContacts = async (parentContactId, hide) => {
parentId: parentContactId,
});
useAlert(t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
- hide();
+ close();
emit('close');
} catch (error) {
useAlert(t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
@@ -69,31 +85,24 @@ const onMergeContacts = async (parentContactId, hide) => {
-
-
-
-
-
-
- {{ $t('MERGE_CONTACTS.TITLE') }}
-
-
- {{ $t('MERGE_CONTACTS.DESCRIPTION') }}
-
-
-
onMergeContacts(id, hide)"
- />
-
-
-
+
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
index 41c5854e0..2b6001729 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
@@ -12,12 +12,19 @@ import Avatar from 'next/avatar/Avatar.vue';
import SocialIcons from './SocialIcons.vue';
import EditContact from './EditContact.vue';
import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue';
-import ContactDeleteModal from 'dashboard/modules/contact/ContactDeleteModal.vue';
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
+import { BUS_EVENTS } from 'shared/constants/busEvents';
import NextButton from 'dashboard/components-next/button/Button.vue';
import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.vue';
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
+import {
+ isAConversationRoute,
+ isAInboxViewRoute,
+ getConversationDashboardRoute,
+} from '../../../../helper/routeHelpers';
+import { emitter } from 'shared/helpers/mitt';
+
export default {
components: {
NextButton,
@@ -27,7 +34,6 @@ export default {
ComposeConversation,
SocialIcons,
ContactMergeModal,
- ContactDeleteModal,
VoiceCallButton,
InlineInput,
},
@@ -51,6 +57,7 @@ export default {
data() {
return {
showEditModal: false,
+ showDeleteModal: false,
isEditingName: false,
editName: '',
};
@@ -92,6 +99,10 @@ export default {
telegram,
};
},
+ // Delete Modal
+ confirmDeleteMessage() {
+ return ` ${this.contact.name}?`;
+ },
},
watch: {
'contact.id': {
@@ -106,6 +117,28 @@ export default {
toggleEditModal() {
this.showEditModal = !this.showEditModal;
},
+ openComposeConversationModal(toggleFn) {
+ toggleFn();
+ // Flag to prevent triggering drag n drop,
+ // When compose modal is active
+ emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, true);
+ },
+ closeComposeConversationModal() {
+ // Flag to enable drag n drop,
+ // When compose modal is closed
+ emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
+ },
+ toggleDeleteModal() {
+ this.showDeleteModal = !this.showDeleteModal;
+ },
+ confirmDeletion() {
+ this.deleteContact(this.contact);
+ this.closeDelete();
+ },
+ closeDelete() {
+ this.showDeleteModal = false;
+ this.showEditModal = false;
+ },
findCountryFlag(countryCode, cityAndCountry) {
try {
if (!countryCode) {
@@ -118,6 +151,36 @@ export default {
return '';
}
},
+ async deleteContact({ id }) {
+ try {
+ await this.$store.dispatch('contacts/delete', id);
+ this.$emit('panelClose');
+ useAlert(this.$t('DELETE_CONTACT.API.SUCCESS_MESSAGE'));
+
+ if (isAConversationRoute(this.$route.name)) {
+ this.$router.push({
+ name: getConversationDashboardRoute(this.$route.name),
+ });
+ } else if (isAInboxViewRoute(this.$route.name)) {
+ this.$router.push({
+ name: 'inbox_view',
+ });
+ } else if (this.$route.name !== 'contacts_dashboard') {
+ this.$router.push({
+ name: 'contacts_dashboard',
+ });
+ }
+ } catch (error) {
+ useAlert(
+ error.message
+ ? error.message
+ : this.$t('DELETE_CONTACT.API.ERROR_MESSAGE')
+ );
+ }
+ },
+ openMergeModal() {
+ this.$refs.mergeModal?.open();
+ },
startEditingName() {
this.editName = this.contact.name || '';
this.isEditingName = true;
@@ -291,14 +354,19 @@ export default {
-
-
+
+
@@ -319,41 +387,45 @@ export default {
sm
@click="toggleEditModal"
/>
-
-
-
-
-
-
+
-
-
-
-
+ v-tooltip.top-end="$t('DELETE_CONTACT.BUTTON_LABEL')"
+ icon="i-ph-trash"
+ slate
+ faded
+ sm
+ ruby
+ :disabled="uiFlags.isDeleting"
+ @click="toggleDeleteModal"
+ />
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue
index eaa911a42..33a14fe65 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue
@@ -1,70 +1,74 @@
-
-
-
-
-
-
- {{
- `${$t('EDIT_CONTACT.TITLE')} - ${contact.name || contact.email}`
- }}
-
-
- {{ $t('EDIT_CONTACT.DESC') }}
-
-
-
-
+
+
-
+