mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	 7fe94dc1a2
			
		
	
	7fe94dc1a2
	
	
	
		
			
			## Description This introduces a reusable Call button used in the Contacts details header (between Block contact and Send message) and in the conversation contact panel. The button shows only when the account has at least one Voice inbox and the contact has a phone number. If multiple Voice inboxes are present, clicking opens a picker modal; otherwise, a “Calling is under development” toast is shown > Actual wiring to functionality will available in follow up PRs references: #11602 , #11481 ## Screens <img width="250" alt="Screenshot 2025-08-18 at 8 33 02 PM" src="https://github.com/user-attachments/assets/d7a09a9d-8eff-4461-a75f-27854540c2a0" /> <img width="250" alt="Screenshot 2025-08-18 at 8 32 31 PM" src="https://github.com/user-attachments/assets/682ae66e-dd5f-4750-9c30-0a210e120250" /> <img width="250" alt="Screenshot 2025-08-18 at 8 32 40 PM" src="https://github.com/user-attachments/assets/7de0d753-eefc-4b7f-942b-ecca1964fcd7" /> ## Test Cases - Enable voice feature and create inboxes and test the cases for both contact details view and contact card view in conversation - Details: 0 Voice inboxes → no Call button. - Details: 1+ Voice inboxes, no phone number for contact → no Call button. - Details: 1 Voice inbox + phone number for contact → button visible; click → toast. - Details: >1 Voice inboxes + phone number for contact → click → modal → choose → toast. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
		
			
				
	
	
		
			190 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <script setup>
 | |
| import { computed, useSlots, ref } from 'vue';
 | |
| import { useI18n } from 'vue-i18n';
 | |
| import { useRoute } from 'vue-router';
 | |
| import { vOnClickOutside } from '@vueuse/components';
 | |
| 
 | |
| import Button from 'dashboard/components-next/button/Button.vue';
 | |
| import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
 | |
| import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
 | |
| import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.vue';
 | |
| 
 | |
| const props = defineProps({
 | |
|   selectedContact: {
 | |
|     type: Object,
 | |
|     default: () => ({}),
 | |
|   },
 | |
|   isUpdating: {
 | |
|     type: Boolean,
 | |
|     default: false,
 | |
|   },
 | |
| });
 | |
| 
 | |
| const emit = defineEmits(['goToContactsList', 'toggleBlock']);
 | |
| 
 | |
| const { t } = useI18n();
 | |
| const slots = useSlots();
 | |
| const route = useRoute();
 | |
| 
 | |
| const isContactSidebarOpen = ref(false);
 | |
| 
 | |
| const contactId = computed(() => route.params.contactId);
 | |
| 
 | |
| const selectedContactName = computed(() => {
 | |
|   return props.selectedContact?.name;
 | |
| });
 | |
| 
 | |
| const breadcrumbItems = computed(() => {
 | |
|   const items = [
 | |
|     {
 | |
|       label: t('CONTACTS_LAYOUT.HEADER.BREADCRUMB.CONTACTS'),
 | |
|       link: '#',
 | |
|     },
 | |
|   ];
 | |
|   if (props.selectedContact) {
 | |
|     items.push({
 | |
|       label: selectedContactName.value,
 | |
|     });
 | |
|   }
 | |
|   return items;
 | |
| });
 | |
| 
 | |
| const isContactBlocked = computed(() => {
 | |
|   return props.selectedContact?.blocked;
 | |
| });
 | |
| 
 | |
| const handleBreadcrumbClick = () => {
 | |
|   emit('goToContactsList');
 | |
| };
 | |
| 
 | |
| const toggleBlock = () => {
 | |
|   emit('toggleBlock', isContactBlocked.value);
 | |
| };
 | |
| 
 | |
| const handleConversationSidebarToggle = () => {
 | |
|   isContactSidebarOpen.value = !isContactSidebarOpen.value;
 | |
| };
 | |
| 
 | |
| const closeMobileSidebar = () => {
 | |
|   if (!isContactSidebarOpen.value) return;
 | |
|   isContactSidebarOpen.value = false;
 | |
| };
 | |
| </script>
 | |
| 
 | |
| <template>
 | |
|   <section
 | |
|     class="flex w-full h-full overflow-hidden justify-evenly bg-n-background"
 | |
|   >
 | |
|     <div
 | |
|       class="flex flex-col w-full h-full transition-all duration-300 ltr:2xl:ml-56 rtl:2xl:mr-56"
 | |
|     >
 | |
|       <header class="sticky top-0 z-10 px-6 3xl:px-0">
 | |
|         <div class="w-full mx-auto max-w-[40.625rem]">
 | |
|           <div
 | |
|             class="flex flex-col xs:flex-row items-start xs:items-center justify-between w-full py-7 gap-2"
 | |
|           >
 | |
|             <Breadcrumb
 | |
|               :items="breadcrumbItems"
 | |
|               @click="handleBreadcrumbClick"
 | |
|             />
 | |
|             <div class="flex items-center gap-2">
 | |
|               <Button
 | |
|                 :label="
 | |
|                   !isContactBlocked
 | |
|                     ? $t('CONTACTS_LAYOUT.HEADER.BLOCK_CONTACT')
 | |
|                     : $t('CONTACTS_LAYOUT.HEADER.UNBLOCK_CONTACT')
 | |
|                 "
 | |
|                 size="sm"
 | |
|                 slate
 | |
|                 :is-loading="isUpdating"
 | |
|                 :disabled="isUpdating"
 | |
|                 @click="toggleBlock"
 | |
|               />
 | |
|               <VoiceCallButton
 | |
|                 :phone="selectedContact?.phoneNumber"
 | |
|                 :label="$t('CONTACT_PANEL.CALL')"
 | |
|                 size="sm"
 | |
|               />
 | |
|               <ComposeConversation :contact-id="contactId">
 | |
|                 <template #trigger="{ toggle }">
 | |
|                   <Button
 | |
|                     :label="$t('CONTACTS_LAYOUT.HEADER.SEND_MESSAGE')"
 | |
|                     size="sm"
 | |
|                     @click="toggle"
 | |
|                   />
 | |
|                 </template>
 | |
|               </ComposeConversation>
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|       </header>
 | |
|       <main class="flex-1 px-6 overflow-y-auto 3xl:px-px">
 | |
|         <div class="w-full py-4 mx-auto max-w-[40.625rem]">
 | |
|           <slot name="default" />
 | |
|         </div>
 | |
|       </main>
 | |
|     </div>
 | |
| 
 | |
|     <!-- Desktop sidebar -->
 | |
|     <div
 | |
|       v-if="slots.sidebar"
 | |
|       class="hidden lg:block overflow-y-auto justify-end min-w-52 w-full py-6 max-w-md border-l border-n-weak bg-n-solid-2"
 | |
|     >
 | |
|       <slot name="sidebar" />
 | |
|     </div>
 | |
| 
 | |
|     <!-- Mobile sidebar container -->
 | |
|     <div
 | |
|       v-if="slots.sidebar"
 | |
|       class="lg:hidden fixed top-0 ltr:right-0 rtl:left-0 h-full z-50 flex justify-end transition-all duration-200 ease-in-out"
 | |
|       :class="isContactSidebarOpen ? 'w-full' : 'w-16'"
 | |
|     >
 | |
|       <!-- Toggle button -->
 | |
|       <div
 | |
|         v-on-click-outside="[
 | |
|           closeMobileSidebar,
 | |
|           { ignore: ['#contact-sidebar-content'] },
 | |
|         ]"
 | |
|         class="flex items-start p-1 w-fit h-fit relative order-1 xs:top-24 top-28 transition-all bg-n-solid-2 border border-n-weak duration-500 ease-in-out"
 | |
|         :class="[
 | |
|           isContactSidebarOpen
 | |
|             ? 'justify-end ltr:rounded-l-full rtl:rounded-r-full ltr:rounded-r-none rtl:rounded-l-none'
 | |
|             : 'justify-center rounded-full ltr:mr-6 rtl:ml-6',
 | |
|         ]"
 | |
|       >
 | |
|         <Button
 | |
|           ghost
 | |
|           slate
 | |
|           sm
 | |
|           class="!rounded-full rtl:rotate-180"
 | |
|           :class="{ 'bg-n-alpha-2': isContactSidebarOpen }"
 | |
|           :icon="
 | |
|             isContactSidebarOpen
 | |
|               ? 'i-lucide-panel-right-close'
 | |
|               : 'i-lucide-panel-right-open'
 | |
|           "
 | |
|           data-contact-sidebar-toggle
 | |
|           @click="handleConversationSidebarToggle"
 | |
|         />
 | |
|       </div>
 | |
| 
 | |
|       <Transition
 | |
|         enter-active-class="transition-transform duration-200 ease-in-out"
 | |
|         leave-active-class="transition-transform duration-200 ease-in-out"
 | |
|         enter-from-class="ltr:translate-x-full rtl:-translate-x-full"
 | |
|         enter-to-class="ltr:translate-x-0 rtl:-translate-x-0"
 | |
|         leave-from-class="ltr:translate-x-0 rtl:-translate-x-0"
 | |
|         leave-to-class="ltr:translate-x-full rtl:-translate-x-full"
 | |
|       >
 | |
|         <div
 | |
|           v-if="isContactSidebarOpen"
 | |
|           id="contact-sidebar-content"
 | |
|           class="order-2 w-[85%] sm:w-[50%] bg-n-solid-2 ltr:border-l rtl:border-r border-n-weak overflow-y-auto py-6 shadow-lg"
 | |
|         >
 | |
|           <slot name="sidebar" />
 | |
|         </div>
 | |
|       </Transition>
 | |
|     </div>
 | |
|   </section>
 | |
| </template>
 |