diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue b/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
index 0bc1e376c..351cc7071 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
@@ -107,11 +107,10 @@ 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 7f9047f8d..773867de1 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 04340eb7d..1ab370901 100644
--- a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
@@ -2,13 +2,10 @@
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 {
@@ -18,22 +15,18 @@ 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,
},
- isModal: {
- type: Boolean,
- default: false,
+ align: {
+ type: String,
+ default: 'end',
},
});
@@ -42,23 +35,16 @@ const emit = defineEmits(['close']);
const searchContacts = createContactSearcher();
const store = useStore();
const { t } = useI18n();
-const { width: windowWidth } = useWindowSize();
const { fetchSignatureFlagFromUISettings } = useUISettings();
-const isSmallScreen = computed(
- () => windowWidth.value < wootConstants.SMALL_SCREEN_BREAKPOINT
-);
-
-const viewInModal = computed(() => props.isModal || isSmallScreen.value);
-
+const popoverRef = ref(null);
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: '',
@@ -95,14 +81,6 @@ 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;
@@ -172,7 +150,7 @@ const clearSelectedContact = () => {
};
const closeCompose = () => {
- showComposeNewConversation.value = false;
+ popoverRef.value?.hide();
if (!props.contactId) {
// If contactId is passed as prop
// Then don't allow to remove the selected contact
@@ -180,7 +158,6 @@ const closeCompose = () => {
}
targetInbox.value = null;
resetContacts();
- emit('close');
};
const discardCompose = () => {
@@ -213,8 +190,15 @@ const createConversation = async ({ payload, isFromWhatsApp }) => {
}
};
-const toggle = () => {
- showComposeNewConversation.value = !showComposeNewConversation.value;
+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');
};
watch(
@@ -242,64 +226,22 @@ 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);
-
+
+
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue
index 455abf996..c4111b481 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 9fd25c481..cbb3b2099 100644
--- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
+++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
@@ -10,8 +10,6 @@ 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';
@@ -184,15 +182,6 @@ 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',
@@ -734,7 +723,13 @@ const menuItems = computed(() => {
-
-
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue
index 00c944f9e..89b01cb8d 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue
@@ -41,7 +41,16 @@ 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
new file mode 100644
index 000000000..93afc30b1
--- /dev/null
+++ b/app/javascript/dashboard/composables/useDropdownPosition.js
@@ -0,0 +1,128 @@
+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
new file mode 100644
index 000000000..3bf48e184
--- /dev/null
+++ b/app/javascript/dashboard/modules/contact/ContactDeleteModal.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+ {{ $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 8db16d9e5..1a343f777 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,7 +23,6 @@ const { t } = useI18n();
const store = useStore();
const uiFlags = useMapGetter('contacts/getUIFlags');
-const dialogRef = ref(null);
const isSearching = ref(false);
const searchResults = ref([]);
@@ -35,21 +34,6 @@ 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 = [];
@@ -68,7 +52,7 @@ const onContactSearch = async query => {
}
};
-const onMergeContacts = async parentContactId => {
+const onMergeContacts = async (parentContactId, hide) => {
useTrack(CONTACTS_EVENTS.MERGED_CONTACTS);
try {
await store.dispatch('contacts/merge', {
@@ -76,7 +60,7 @@ const onMergeContacts = async parentContactId => {
parentId: parentContactId,
});
useAlert(t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
- close();
+ hide();
emit('close');
} catch (error) {
useAlert(t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
@@ -85,24 +69,31 @@ const onMergeContacts = async parentContactId => {
-
+
+
+
+
+
+
+ {{ $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 2b6001729..41c5854e0 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
@@ -12,19 +12,12 @@ 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,
@@ -34,6 +27,7 @@ export default {
ComposeConversation,
SocialIcons,
ContactMergeModal,
+ ContactDeleteModal,
VoiceCallButton,
InlineInput,
},
@@ -57,7 +51,6 @@ export default {
data() {
return {
showEditModal: false,
- showDeleteModal: false,
isEditingName: false,
editName: '',
};
@@ -99,10 +92,6 @@ export default {
telegram,
};
},
- // Delete Modal
- confirmDeleteMessage() {
- return ` ${this.contact.name}?`;
- },
},
watch: {
'contact.id': {
@@ -117,28 +106,6 @@ 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) {
@@ -151,36 +118,6 @@ 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;
@@ -354,19 +291,14 @@ export default {
-
-
+
+
@@ -387,45 +319,41 @@ export default {
sm
@click="toggleEditModal"
/>
-
-
+
+
+
+
+
+ :contact="contact"
+ @deleted="$emit('panelClose')"
+ >
+
+
+
+
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue
index 33a14fe65..eaa911a42 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue
@@ -1,74 +1,70 @@
-
-
-
-
+
+
+
+
+ {{
+ `${$t('EDIT_CONTACT.TITLE')} - ${contact.name || contact.email}`
+ }}
+
+
+ {{ $t('EDIT_CONTACT.DESC') }}
+
+
+
+
-
+