Merge branch 'develop' into fix/CW-6859

This commit is contained in:
Sivin Varghese
2026-04-20 14:06:50 +05:30
committed by GitHub
55 changed files with 3018 additions and 1231 deletions
@@ -1,49 +1,131 @@
<script>
<script setup>
import { computed, ref, watch, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
import {
isOnMentionsView,
isOnUnattendedView,
isOnFoldersView,
} from 'dashboard/store/modules/conversations/helpers/actionHelpers';
import ConversationCard from 'dashboard/components/widgets/conversation/ConversationCard.vue';
import { mapGetters } from 'vuex';
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
import ConversationContextMenu from 'dashboard/components/widgets/conversation/contextMenu/Index.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
export default {
components: {
ConversationCard,
Spinner,
},
props: {
contactId: {
type: [String, Number],
required: true,
},
conversationId: {
type: [String, Number],
required: true,
},
},
computed: {
conversations() {
return this.$store.getters['contactConversations/getContactConversation'](
this.contactId
);
},
previousConversations() {
return this.conversations.filter(
conversation => conversation.id !== Number(this.conversationId)
);
},
...mapGetters({
uiFlags: 'contactConversations/getUIFlags',
}),
},
watch: {
contactId(newContactId, prevContactId) {
if (newContactId && newContactId !== prevContactId) {
this.$store.dispatch('contactConversations/get', newContactId);
}
},
},
mounted() {
this.$store.dispatch('contactConversations/get', this.contactId);
},
const props = defineProps({
contactId: { type: [String, Number], required: true },
conversationId: { type: [String, Number], required: true },
});
const store = useStore();
const route = useRoute();
const router = useRouter();
const currentChat = useMapGetter('getSelectedChat');
const uiFlags = useMapGetter('contactConversations/getUIFlags');
const contactGetter = useMapGetter('contacts/getContact');
const inboxGetter = useMapGetter('inboxes/getInbox');
const activeInbox = useMapGetter('getSelectedInbox');
const inboxesList = useMapGetter('inboxes/getInboxes');
const showInboxName = computed(
() => !activeInbox.value && inboxesList.value.length > 1
);
const contactConversationGetter = useMapGetter(
'contactConversations/getContactConversation'
);
const conversations = computed(() =>
contactConversationGetter.value(props.contactId)
);
const previousConversations = computed(() =>
conversations.value.filter(c => c.id !== Number(props.conversationId))
);
const activeContextChat = ref(null);
const showContextMenu = ref(false);
const contextMenu = ref({ x: null, y: null });
const buildConversationUrl = conversationId => {
const {
params: { accountId, inbox_id: inboxId, label, teamId },
name,
} = route;
let conversationType = '';
if (isOnMentionsView({ route: { name } })) {
conversationType = 'mention';
} else if (isOnUnattendedView({ route: { name } })) {
conversationType = 'unattended';
}
return frontendURL(
conversationUrl({
accountId,
activeInbox: inboxId,
id: conversationId,
label,
teamId,
foldersId: isOnFoldersView({ route: { name } }) ? route.params.id : 0,
conversationType,
})
);
};
const conversationPath = computed(() => {
if (!activeContextChat.value) return '';
return buildConversationUrl(activeContextChat.value.id);
});
const onCardClick = (conversation, e) => {
const path = buildConversationUrl(conversation.id);
if (!path) return;
if (e.metaKey || e.ctrlKey) {
e.preventDefault();
window.open(
`${window.chatwootConfig.hostURL}${path}`,
'_blank',
'noopener,noreferrer'
);
return;
}
router.push({ path });
};
const openContextMenu = (conversation, e) => {
e.preventDefault();
activeContextChat.value = conversation;
contextMenu.value.x = e.pageX || e.clientX;
contextMenu.value.y = e.pageY || e.clientY;
showContextMenu.value = true;
};
const closeContextMenu = () => {
showContextMenu.value = false;
contextMenu.value.x = null;
contextMenu.value.y = null;
activeContextChat.value = null;
};
watch(
() => props.contactId,
(newId, oldId) => {
if (newId && newId !== oldId) {
showContextMenu.value = false;
activeContextChat.value = null;
store.dispatch('contactConversations/get', newId);
}
}
);
onMounted(() => {
store.dispatch('contactConversations/get', props.contactId);
});
</script>
<template>
@@ -53,18 +135,43 @@ export default {
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
</span>
</div>
<div v-else class="contact-conversation--list">
<div
v-else
class="contact-conversation--list [&>.conversation:last-child]:!border-b-0 [&>.conversation:last-child:hover]:!border-b-0 [&>.conversation:last-child]:!rounded-b-lg"
>
<ConversationCard
v-for="conversation in previousConversations"
:key="conversation.id"
:chat="conversation"
:hide-inbox-name="false"
:current-contact="contactGetter(conversation.meta?.sender?.id) || {}"
:assignee="conversation.meta?.assignee || {}"
:inbox="inboxGetter(conversation.inbox_id) || {}"
:is-active-chat="currentChat.id === conversation.id"
:show-inbox-name="showInboxName"
hide-thumbnail
enable-context-menu
compact
:allowed-context-menu-options="['open-new-tab', 'copy-link']"
@click="onCardClick(conversation, $event)"
@contextmenu="openContextMenu(conversation, $event)"
/>
</div>
<ContextMenu
v-if="showContextMenu && activeContextChat"
:x="contextMenu.x"
:y="contextMenu.y"
@close="closeContextMenu"
>
<ConversationContextMenu
:status="activeContextChat.status"
:inbox-id="activeContextChat.inbox_id"
:priority="activeContextChat.priority"
:chat-id="activeContextChat.id"
:has-unread-messages="activeContextChat.unread_count > 0"
:conversation-labels="activeContextChat.labels"
:conversation-url="conversationPath"
:allowed-options="['open-new-tab', 'copy-link']"
@close="closeContextMenu"
/>
</ContextMenu>
</div>
<div v-else class="flex items-center justify-center py-5">
<Spinner />
@@ -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 {
</div>
</div>
<div class="flex items-center w-full mt-0.5 gap-2">
<ComposeConversation
:contact-id="String(contact.id)"
is-modal
@close="closeComposeConversationModal"
>
<template #trigger="{ toggle }">
<ComposeConversation :contact-id="String(contact.id)">
<template #trigger>
<NextButton
v-tooltip.top-end="$t('CONTACT_PANEL.NEW_MESSAGE')"
icon="i-ph-chat-circle-dots"
slate
faded
sm
@click="openComposeConversationModal(toggle)"
/>
</template>
</ComposeConversation>
@@ -387,45 +319,41 @@ export default {
sm
@click="toggleEditModal"
/>
<NextButton
v-tooltip.top-end="$t('CONTACT_PANEL.MERGE_CONTACT')"
icon="i-ph-arrows-merge"
slate
faded
sm
:disabled="uiFlags.isMerging"
@click="openMergeModal"
/>
<NextButton
<ContactMergeModal :primary-contact="contact">
<template #trigger>
<NextButton
v-tooltip.top-end="$t('CONTACT_PANEL.MERGE_CONTACT')"
icon="i-ph-arrows-merge"
slate
faded
sm
:disabled="uiFlags.isMerging"
/>
</template>
</ContactMergeModal>
<ContactDeleteModal
v-if="isAdmin"
v-tooltip.top-end="$t('DELETE_CONTACT.BUTTON_LABEL')"
icon="i-ph-trash"
slate
faded
sm
ruby
:disabled="uiFlags.isDeleting"
@click="toggleDeleteModal"
/>
:contact="contact"
@deleted="$emit('panelClose')"
>
<template #trigger>
<NextButton
v-tooltip.top-end="$t('DELETE_CONTACT.BUTTON_LABEL')"
icon="i-ph-trash"
slate
faded
sm
ruby
:disabled="uiFlags.isDeleting"
/>
</template>
</ContactDeleteModal>
</div>
<EditContact
v-if="showEditModal"
:show="showEditModal"
:contact="contact"
@cancel="toggleEditModal"
/>
<ContactMergeModal ref="mergeModal" :primary-contact="contact" />
</div>
<woot-delete-modal
v-if="showDeleteModal"
v-model:show="showDeleteModal"
:on-close="closeDelete"
:on-confirm="confirmDeletion"
:title="$t('DELETE_CONTACT.CONFIRM.TITLE')"
:message="$t('DELETE_CONTACT.CONFIRM.MESSAGE')"
:message-value="confirmDeleteMessage"
:confirm-text="$t('DELETE_CONTACT.CONFIRM.YES')"
:reject-text="$t('DELETE_CONTACT.CONFIRM.NO')"
/>
</div>
</template>
@@ -1,74 +1,70 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { useStore } from 'dashboard/composables/store';
import { useMapGetter } from 'dashboard/composables/store';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import ContactForm from './ContactForm.vue';
import Button from 'dashboard/components-next/button/Button.vue';
export default {
components: {
ContactForm,
},
props: {
show: {
type: Boolean,
default: false,
},
contact: {
type: Object,
default: () => ({}),
},
},
emits: ['cancel', 'update:show'],
computed: {
...mapGetters({
uiFlags: 'contacts/getUIFlags',
}),
localShow: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
},
},
},
const props = defineProps({
show: { type: Boolean, default: false },
contact: { type: Object, default: () => ({}) },
});
methods: {
onCancel() {
this.$emit('cancel');
},
onSuccess() {
this.$emit('cancel');
},
async onSubmit(contactItem) {
await this.$store.dispatch('contacts/update', contactItem);
await this.$store.dispatch(
'contacts/fetchContactableInbox',
this.contact.id
);
},
},
const emit = defineEmits(['cancel']);
const store = useStore();
const uiFlags = useMapGetter('contacts/getUIFlags');
const onCancel = () => emit('cancel');
const onSubmit = async contactItem => {
await store.dispatch('contacts/update', contactItem);
await store.dispatch('contacts/fetchContactableInbox', props.contact.id);
};
// Restore Escape-to-close behavior that was provided by woot-modal before
// this drawer was reimplemented as a plain fixed panel.
useKeyboardEvents({
Escape: {
action: () => {
if (props.show) onCancel();
},
allowOnFocusedInput: true,
},
});
</script>
<template>
<woot-modal
v-model:show="localShow"
:on-close="onCancel"
modal-type="right-aligned"
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="ltr:translate-x-full rtl:-translate-x-full opacity-0"
leave-active-class="transition duration-150 ease-in"
leave-to-class="ltr:translate-x-[30%] rtl:-translate-x-[30%] opacity-0"
>
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="`${$t('EDIT_CONTACT.TITLE')} - ${
contact.name || contact.email
}`"
:header-content="$t('EDIT_CONTACT.DESC')"
/>
<div
v-if="show"
class="fixed inset-y-0 ltr:right-0 rtl:left-0 z-50 flex flex-col w-[30rem] max-w-full h-full bg-n-surface-2 ltr:border-l rtl:border-r border-n-weak shadow-lg overflow-auto"
>
<div class="flex items-center justify-between px-8 pt-8 pb-2">
<div>
<h2 class="text-lg font-medium text-n-slate-12 mb-1">
{{
`${$t('EDIT_CONTACT.TITLE')} - ${contact.name || contact.email}`
}}
</h2>
<p class="text-sm text-n-slate-11 mb-0">
{{ $t('EDIT_CONTACT.DESC') }}
</p>
</div>
<Button icon="i-lucide-x" slate ghost sm @click="onCancel" />
</div>
<ContactForm
:contact="contact"
:in-progress="uiFlags.isUpdating"
:on-submit="onSubmit"
@success="onSuccess"
@success="onCancel"
@cancel="onCancel"
/>
</div>
</woot-modal>
</transition>
</template>