mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: End conversation from widget (#3660)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
		| @@ -34,6 +34,8 @@ class Api::V1::Widget::BaseController < ApplicationController | |||||||
|     ) |     ) | ||||||
|     @contact = @contact_inbox&.contact |     @contact = @contact_inbox&.contact | ||||||
|     raise ActiveRecord::RecordNotFound unless @contact |     raise ActiveRecord::RecordNotFound unless @contact | ||||||
|  |  | ||||||
|  |     Current.contact = @contact | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create_conversation |   def create_conversation | ||||||
|   | |||||||
| @@ -44,6 +44,15 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController | |||||||
|     head :ok |     head :ok | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def toggle_status | ||||||
|  |     head :not_found && return if conversation.nil? | ||||||
|  |     unless conversation.resolved? | ||||||
|  |       conversation.status = 'resolved' | ||||||
|  |       conversation.save | ||||||
|  |     end | ||||||
|  |     head :ok | ||||||
|  |   end | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def trigger_typing_event(event) |   def trigger_typing_event(event) | ||||||
|   | |||||||
| @@ -2,12 +2,15 @@ | |||||||
|   "arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z", |   "arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z", | ||||||
|   "arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z", |   "arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z", | ||||||
|   "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", |   "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", | ||||||
|  |   "checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z", | ||||||
|   "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", |   "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", | ||||||
|   "chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z", |   "chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z", | ||||||
|   "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", |   "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", | ||||||
|   "document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z", |   "document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z", | ||||||
|   "emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z", |   "emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z", | ||||||
|   "link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z", |   "link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z", | ||||||
|  |   "more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z", | ||||||
|   "open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z", |   "open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z", | ||||||
|   "send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z" |   "send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z", | ||||||
|  |   "sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z","M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z","M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -48,6 +48,11 @@ const sendEmailTranscript = async ({ email }) => { | |||||||
|     { email } |     { email } | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | const toggleStatus = async () => { | ||||||
|  |   return API.get( | ||||||
|  |     `/api/v1/widget/conversations/toggle_status${window.location.search}` | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
| export { | export { | ||||||
|   createConversationAPI, |   createConversationAPI, | ||||||
| @@ -58,4 +63,5 @@ export { | |||||||
|   toggleTyping, |   toggleTyping, | ||||||
|   setUserLastSeenAt, |   setUserLastSeenAt, | ||||||
|   sendEmailTranscript, |   sendEmailTranscript, | ||||||
|  |   toggleStatus, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,5 +1,13 @@ | |||||||
| <template> | <template> | ||||||
|   <div v-if="showHeaderActions" class="actions flex items-center"> |   <div v-if="showHeaderActions" class="actions flex items-center"> | ||||||
|  |     <button | ||||||
|  |       v-if="conversationStatus === 'open'" | ||||||
|  |       class="button transparent compact" | ||||||
|  |       :title="$t('END_CONVERSATION')" | ||||||
|  |       @click="resolveConversation" | ||||||
|  |     > | ||||||
|  |       <fluent-icon icon="sign-out" size="22" class="text-black-900" /> | ||||||
|  |     </button> | ||||||
|     <button |     <button | ||||||
|       v-if="showPopoutButton" |       v-if="showPopoutButton" | ||||||
|       class="button transparent compact new-window--button " |       class="button transparent compact new-window--button " | ||||||
| @@ -19,6 +27,7 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| <script> | <script> | ||||||
|  | import { mapGetters } from 'vuex'; | ||||||
| import { IFrameHelper, RNHelper } from 'widget/helpers/utils'; | import { IFrameHelper, RNHelper } from 'widget/helpers/utils'; | ||||||
| import { buildPopoutURL } from '../helpers/urlParamsHelper'; | import { buildPopoutURL } from '../helpers/urlParamsHelper'; | ||||||
| import FluentIcon from 'shared/components/FluentIcon/Index.vue'; | import FluentIcon from 'shared/components/FluentIcon/Index.vue'; | ||||||
| @@ -33,6 +42,9 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  |     ...mapGetters({ | ||||||
|  |       conversationAttributes: 'conversationAttributes/getConversationParams', | ||||||
|  |     }), | ||||||
|     isIframe() { |     isIframe() { | ||||||
|       return IFrameHelper.isIFrame(); |       return IFrameHelper.isIFrame(); | ||||||
|     }, |     }, | ||||||
| @@ -40,7 +52,13 @@ export default { | |||||||
|       return RNHelper.isRNWebView(); |       return RNHelper.isRNWebView(); | ||||||
|     }, |     }, | ||||||
|     showHeaderActions() { |     showHeaderActions() { | ||||||
|       return this.isIframe || this.isRNWebView; |       return this.isIframe || this.isRNWebView || this.hasWidgetOptions; | ||||||
|  |     }, | ||||||
|  |     conversationStatus() { | ||||||
|  |       return this.conversationAttributes.status; | ||||||
|  |     }, | ||||||
|  |     hasWidgetOptions() { | ||||||
|  |       return this.showPopoutButton || this.conversationStatus === 'open'; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
| @@ -72,6 +90,9 @@ export default { | |||||||
|         RNHelper.sendMessage({ type: 'close-widget' }); |         RNHelper.sendMessage({ type: 'close-widget' }); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     resolveConversation() { | ||||||
|  |       this.$store.dispatch('conversation/resolveConversation'); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								app/javascript/widget/components/dropdown/DropdownMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								app/javascript/widget/components/dropdown/DropdownMenu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="relative"> | ||||||
|  |     <button class="z-10 focus:outline-none select-none" @click="toggleMenu"> | ||||||
|  |       <slot name="button"></slot> | ||||||
|  |     </button> | ||||||
|  |  | ||||||
|  |     <!-- to close when clicked on space around it--> | ||||||
|  |     <button | ||||||
|  |       v-if="isOpen" | ||||||
|  |       tabindex="-1" | ||||||
|  |       class="fixed inset-0 h-full w-full cursor-default focus:outline-none" | ||||||
|  |       @click="toggleMenu" | ||||||
|  |     ></button> | ||||||
|  |  | ||||||
|  |     <!--dropdown menu--> | ||||||
|  |     <transition | ||||||
|  |       enter-active-class="transition-all duration-200 ease-out" | ||||||
|  |       leave-active-class="transition-all duration-750 ease-in" | ||||||
|  |       enter-class="opacity-0 scale-75" | ||||||
|  |       enter-to-class="opacity-100 scale-100" | ||||||
|  |       leave-class="opacity-100 scale-100" | ||||||
|  |       leave-to-class="opacity-0 scale-75" | ||||||
|  |     > | ||||||
|  |       <div | ||||||
|  |         v-if="isOpen" | ||||||
|  |         class="menu-content absolute shadow-xl rounded-md border-solid border border-slate-100 mt-1 py-1 px-2 bg-white z-10" | ||||||
|  |         :class="menuPlacement === 'right' ? 'right-0' : 'left-0'" | ||||||
|  |       > | ||||||
|  |         <slot name="content"></slot> | ||||||
|  |       </div> | ||||||
|  |     </transition> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     menuPlacement: { | ||||||
|  |       type: String, | ||||||
|  |       default: 'right', | ||||||
|  |       validator: value => ['right', 'left'].indexOf(value) !== -1, | ||||||
|  |     }, | ||||||
|  |     open: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     toggleMenu: { | ||||||
|  |       type: Function, | ||||||
|  |       default: () => {}, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       isOpen: false, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     open() { | ||||||
|  |       this.isOpen = !this.isOpen; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     document.addEventListener('keydown', this.onEscape); | ||||||
|  |   }, | ||||||
|  |   beforeDestroy() { | ||||||
|  |     document.removeEventListener('keydown', this.onEscape); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     onEscape(e) { | ||||||
|  |       if (e.key === 'Esc' || e.key === 'Escape') { | ||||||
|  |         this.isOpen = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | @import '~widget/assets/scss/variables.scss'; | ||||||
|  |  | ||||||
|  | .menu-content { | ||||||
|  |   width: max-content; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -0,0 +1,67 @@ | |||||||
|  | <template> | ||||||
|  |   <button :class="['menu-item', itemClass]" @click="action"> | ||||||
|  |     <fluent-icon | ||||||
|  |       v-if="icon" | ||||||
|  |       :icon="iconName" | ||||||
|  |       :size="iconSize" | ||||||
|  |       :class="iconClass" | ||||||
|  |     /> | ||||||
|  |     <span :class="[{ 'pl-3': icon }, textClass]">{{ text }}</span> | ||||||
|  |   </button> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  | import FluentIcon from 'shared/components/FluentIcon/Index.vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { FluentIcon }, | ||||||
|  |   props: { | ||||||
|  |     text: { | ||||||
|  |       type: String, | ||||||
|  |       default: 'Default', | ||||||
|  |     }, | ||||||
|  |     textClass: { | ||||||
|  |       type: String, | ||||||
|  |       default: 'text-sm', | ||||||
|  |     }, | ||||||
|  |     icon: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: true, | ||||||
|  |     }, | ||||||
|  |     iconName: { | ||||||
|  |       type: String, | ||||||
|  |       default: '', | ||||||
|  |     }, | ||||||
|  |     iconSize: { | ||||||
|  |       type: String, | ||||||
|  |       default: '15', | ||||||
|  |     }, | ||||||
|  |     iconClass: { | ||||||
|  |       type: String, | ||||||
|  |       default: 'text-black-900', | ||||||
|  |     }, | ||||||
|  |     itemClass: { | ||||||
|  |       type: String, | ||||||
|  |       default: | ||||||
|  |         'flex items-center p-3 cursor-pointer ml-0 border-b border-slate-100', | ||||||
|  |     }, | ||||||
|  |     action: { | ||||||
|  |       type: Function, | ||||||
|  |       default: () => {}, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | @import '~widget/assets/scss/variables.scss'; | ||||||
|  |  | ||||||
|  | .menu-item { | ||||||
|  |   margin-left: $zero !important; | ||||||
|  |   outline: none; | ||||||
|  |   &:last-child { | ||||||
|  |     border-bottom: none; | ||||||
|  |   } | ||||||
|  |   &:disabled { | ||||||
|  |     cursor: not-allowed; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -11,12 +11,16 @@ class ActionCableConnector extends BaseActionCableConnector { | |||||||
|       'conversation.typing_on': this.onTypingOn, |       'conversation.typing_on': this.onTypingOn, | ||||||
|       'conversation.typing_off': this.onTypingOff, |       'conversation.typing_off': this.onTypingOff, | ||||||
|       'conversation.status_changed': this.onStatusChange, |       'conversation.status_changed': this.onStatusChange, | ||||||
|  |       'conversation.created': this.onConversationCreated, | ||||||
|       'presence.update': this.onPresenceUpdate, |       'presence.update': this.onPresenceUpdate, | ||||||
|       'contact.merged': this.onContactMerge, |       'contact.merged': this.onContactMerge, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onStatusChange = data => { |   onStatusChange = data => { | ||||||
|  |     if (data.status === 'resolved') { | ||||||
|  |       this.app.$store.dispatch('campaign/resetCampaign'); | ||||||
|  |     } | ||||||
|     this.app.$store.dispatch('conversationAttributes/update', data); |     this.app.$store.dispatch('conversationAttributes/update', data); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -33,6 +37,10 @@ class ActionCableConnector extends BaseActionCableConnector { | |||||||
|     this.app.$store.dispatch('conversation/addOrUpdateMessage', data); |     this.app.$store.dispatch('conversation/addOrUpdateMessage', data); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   onConversationCreated = () => { | ||||||
|  |     this.app.$store.dispatch('conversationAttributes/getAttributes'); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   onPresenceUpdate = data => { |   onPresenceUpdate = data => { | ||||||
|     this.app.$store.dispatch('agent/updatePresence', data.users); |     this.app.$store.dispatch('agent/updatePresence', data.users); | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ | |||||||
|     "IN_A_DAY": "Typically replies in a day" |     "IN_A_DAY": "Typically replies in a day" | ||||||
|   }, |   }, | ||||||
|   "START_CONVERSATION": "Start Conversation", |   "START_CONVERSATION": "Start Conversation", | ||||||
|  |   "END_CONVERSATION": "End Conversation", | ||||||
|   "CONTINUE_CONVERSATION": "Continue conversation", |   "CONTINUE_CONVERSATION": "Continue conversation", | ||||||
|   "START_NEW_CONVERSATION": "Start a new conversation", |   "START_NEW_CONVERSATION": "Start a new conversation", | ||||||
|   "UNREAD_VIEW": { |   "UNREAD_VIEW": { | ||||||
|   | |||||||
| @@ -100,6 +100,7 @@ export const actions = { | |||||||
|         { root: true } |         { root: true } | ||||||
|       ); |       ); | ||||||
|       await triggerCampaign({ campaignId, websiteToken }); |       await triggerCampaign({ campaignId, websiteToken }); | ||||||
|  |       commit('setCampaignExecuted', true); | ||||||
|       commit('setActiveCampaign', {}); |       commit('setActiveCampaign', {}); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       commit('setError', true); |       commit('setError', true); | ||||||
| @@ -113,6 +114,7 @@ export const actions = { | |||||||
|   }, |   }, | ||||||
|   resetCampaign: async ({ commit }) => { |   resetCampaign: async ({ commit }) => { | ||||||
|     try { |     try { | ||||||
|  |       commit('setCampaignExecuted', false); | ||||||
|       commit('setActiveCampaign', {}); |       commit('setActiveCampaign', {}); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       commit('setError', true); |       commit('setError', true); | ||||||
| @@ -130,6 +132,12 @@ export const mutations = { | |||||||
|   setError($state, value) { |   setError($state, value) { | ||||||
|     Vue.set($state.uiFlags, 'isError', value); |     Vue.set($state.uiFlags, 'isError', value); | ||||||
|   }, |   }, | ||||||
|  |   setHasFetched($state, value) { | ||||||
|  |     Vue.set($state.uiFlags, 'hasFetched', value); | ||||||
|  |   }, | ||||||
|  |   setCampaignExecuted($state, data) { | ||||||
|  |     Vue.set($state, 'campaignHasExecuted', data); | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import { | |||||||
|   sendAttachmentAPI, |   sendAttachmentAPI, | ||||||
|   toggleTyping, |   toggleTyping, | ||||||
|   setUserLastSeenAt, |   setUserLastSeenAt, | ||||||
|  |   toggleStatus, | ||||||
| } from 'widget/api/conversation'; | } from 'widget/api/conversation'; | ||||||
|  |  | ||||||
| import { createTemporaryMessage, getNonDeletedMessages } from './helpers'; | import { createTemporaryMessage, getNonDeletedMessages } from './helpers'; | ||||||
| @@ -130,4 +131,8 @@ export const actions = { | |||||||
|       // IgnoreError |       // IgnoreError | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   resolveConversation: async () => { | ||||||
|  |     await toggleStatus(); | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -132,6 +132,7 @@ describe('#actions', () => { | |||||||
|             root: true, |             root: true, | ||||||
|           }, |           }, | ||||||
|         ], |         ], | ||||||
|  |         ['setCampaignExecuted', true], | ||||||
|         ['setActiveCampaign', {}], |         ['setActiveCampaign', {}], | ||||||
|         [ |         [ | ||||||
|           'conversation/setConversationUIFlag', |           'conversation/setConversationUIFlag', | ||||||
| @@ -176,7 +177,10 @@ describe('#actions', () => { | |||||||
|     it('sends correct actions if  execute campaign API is success', async () => { |     it('sends correct actions if  execute campaign API is success', async () => { | ||||||
|       API.post.mockResolvedValue({}); |       API.post.mockResolvedValue({}); | ||||||
|       await actions.resetCampaign({ commit }); |       await actions.resetCampaign({ commit }); | ||||||
|       expect(commit.mock.calls).toEqual([['setActiveCampaign', {}]]); |       expect(commit.mock.calls).toEqual([ | ||||||
|  |         ['setCampaignExecuted', false], | ||||||
|  |         ['setActiveCampaign', {}], | ||||||
|  |       ]); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -25,4 +25,12 @@ describe('#mutations', () => { | |||||||
|       expect(state.activeCampaign).toEqual(campaigns[0]); |       expect(state.activeCampaign).toEqual(campaigns[0]); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   describe('#setCampaignExecuted', () => { | ||||||
|  |     it('set campaign executed flag', () => { | ||||||
|  |       const state = { records: [], uiFlags: {}, campaignHasExecuted: false }; | ||||||
|  |       mutations.setCampaignExecuted(state, true); | ||||||
|  |       expect(state.campaignHasExecuted).toEqual(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,20 +1,6 @@ | |||||||
| class ActionCableListener < BaseListener | class ActionCableListener < BaseListener | ||||||
|   include Events::Types |   include Events::Types | ||||||
|  |  | ||||||
|   def conversation_created(event) |  | ||||||
|     conversation, account = extract_conversation_and_account(event) |  | ||||||
|     tokens = user_tokens(account, conversation.inbox.members) |  | ||||||
|  |  | ||||||
|     broadcast(account, tokens, CONVERSATION_CREATED, conversation.push_event_data) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def conversation_read(event) |  | ||||||
|     conversation, account = extract_conversation_and_account(event) |  | ||||||
|     tokens = user_tokens(account, conversation.inbox.members) |  | ||||||
|  |  | ||||||
|     broadcast(account, tokens, CONVERSATION_READ, conversation.push_event_data) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def message_created(event) |   def message_created(event) | ||||||
|     message, account = extract_message_and_account(event) |     message, account = extract_message_and_account(event) | ||||||
|     conversation = message.conversation |     conversation = message.conversation | ||||||
| @@ -26,12 +12,25 @@ class ActionCableListener < BaseListener | |||||||
|   def message_updated(event) |   def message_updated(event) | ||||||
|     message, account = extract_message_and_account(event) |     message, account = extract_message_and_account(event) | ||||||
|     conversation = message.conversation |     conversation = message.conversation | ||||||
|     contact = conversation.contact |  | ||||||
|     tokens = user_tokens(account, conversation.inbox.members) + contact_tokens(conversation.contact_inbox, message) |     tokens = user_tokens(account, conversation.inbox.members) + contact_tokens(conversation.contact_inbox, message) | ||||||
|  |  | ||||||
|     broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data) |     broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def conversation_created(event) | ||||||
|  |     conversation, account = extract_conversation_and_account(event) | ||||||
|  |     tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox) | ||||||
|  |  | ||||||
|  |     broadcast(account, tokens, CONVERSATION_CREATED, conversation.push_event_data) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def conversation_read(event) | ||||||
|  |     conversation, account = extract_conversation_and_account(event) | ||||||
|  |     tokens = user_tokens(account, conversation.inbox.members) | ||||||
|  |  | ||||||
|  |     broadcast(account, tokens, CONVERSATION_READ, conversation.push_event_data) | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def conversation_status_changed(event) |   def conversation_status_changed(event) | ||||||
|     conversation, account = extract_conversation_and_account(event) |     conversation, account = extract_conversation_and_account(event) | ||||||
|     tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox) |     tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox) | ||||||
|   | |||||||
| @@ -20,6 +20,8 @@ module ActivityMessageHandler | |||||||
|   def create_status_change_message(user_name) |   def create_status_change_message(user_name) | ||||||
|     content = if user_name |     content = if user_name | ||||||
|                 I18n.t("conversations.activity.status.#{status}", user_name: user_name) |                 I18n.t("conversations.activity.status.#{status}", user_name: user_name) | ||||||
|  |               elsif Current.contact.present? | ||||||
|  |                 I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) | ||||||
|               elsif resolved? |               elsif resolved? | ||||||
|                 I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) |                 I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) | ||||||
|               end |               end | ||||||
|   | |||||||
| @@ -72,6 +72,7 @@ en: | |||||||
|     activity: |     activity: | ||||||
|       status: |       status: | ||||||
|         resolved: "Conversation was marked resolved by %{user_name}" |         resolved: "Conversation was marked resolved by %{user_name}" | ||||||
|  |         contact_resolved: "Conversation was resolved by %{contact_name}" | ||||||
|         open: "Conversation was reopened by %{user_name}" |         open: "Conversation was reopened by %{user_name}" | ||||||
|         pending: "Conversation was marked as pending by %{user_name}" |         pending: "Conversation was marked as pending by %{user_name}" | ||||||
|         snoozed: "Conversation was snoozed by %{user_name}" |         snoozed: "Conversation was snoozed by %{user_name}" | ||||||
|   | |||||||
| @@ -188,6 +188,7 @@ Rails.application.routes.draw do | |||||||
|             post :update_last_seen |             post :update_last_seen | ||||||
|             post :toggle_typing |             post :toggle_typing | ||||||
|             post :transcript |             post :transcript | ||||||
|  |             get  :toggle_status | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|         resource :contact, only: [:show, :update] do |         resource :contact, only: [:show, :update] do | ||||||
|   | |||||||
| @@ -2,10 +2,12 @@ module Current | |||||||
|   thread_mattr_accessor :user |   thread_mattr_accessor :user | ||||||
|   thread_mattr_accessor :account |   thread_mattr_accessor :account | ||||||
|   thread_mattr_accessor :account_user |   thread_mattr_accessor :account_user | ||||||
|  |   thread_mattr_accessor :contact | ||||||
|  |  | ||||||
|   def self.reset |   def self.reset | ||||||
|     Current.user = nil |     Current.user = nil | ||||||
|     Current.account = nil |     Current.account = nil | ||||||
|     Current.account_user = nil |     Current.account_user = nil | ||||||
|  |     Current.contact = nil | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -118,4 +118,19 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   describe 'GET /api/v1/widget/conversations/toggle_status' do | ||||||
|  |     context 'when user end conversation from widget' do | ||||||
|  |       it 'resolves the conversation' do | ||||||
|  |         expect(conversation.open?).to be true | ||||||
|  |         get '/api/v1/widget/conversations/toggle_status', | ||||||
|  |             headers: { 'X-Auth-Token' => token }, | ||||||
|  |             params: { website_token: web_widget.website_token }, | ||||||
|  |             as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         expect(conversation.reload.resolved?).to be true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Aswin Dev P.S
					Aswin Dev P.S