feat: Setup context menu for message (#6750)

This commit is contained in:
Pranav Raj S
2023-03-24 16:20:19 -07:00
committed by GitHub
parent d666afd757
commit 70e7530cb4
8 changed files with 223 additions and 152 deletions

View File

@@ -86,6 +86,12 @@
margin-left: var(--space-small); margin-left: var(--space-small);
} }
&:first-child {
.button {
margin-left: 0;
}
}
&.justify-content-end { &.justify-content-end {
justify-content: end; justify-content: end;
} }

View File

@@ -1,7 +1,5 @@
<template> <template>
<div <div
v-show="show"
ref="context"
class="context-menu-container" class="context-menu-container"
:style="style" :style="style"
tabindex="0" tabindex="0"
@@ -39,7 +37,6 @@ export default {
}, },
mounted() { mounted() {
this.$nextTick(() => this.$el.focus()); this.$nextTick(() => this.$el.focus());
this.show = true;
}, },
}; };
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<li v-if="shouldRenderMessage" :class="alignBubble"> <li v-if="shouldRenderMessage" :class="alignBubble">
<div :class="wrapClass"> <div :class="wrapClass" @contextmenu="openContextMenu($event)">
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass"> <div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
<bubble-mail-head <bubble-mail-head
:email-attributes="contentAttributes.email" :email-attributes="contentAttributes.email"
@@ -73,12 +73,6 @@
:created-at="createdAt" :created-at="createdAt"
/> />
</div> </div>
<translate-modal
v-if="showTranslateModal"
:content="data.content"
:content-attributes="contentAttributes"
@close="onCloseTranslateModal"
/>
<spinner v-if="isPending" size="tiny" /> <spinner v-if="isPending" size="tiny" />
<div <div
v-if="showAvatar" v-if="showAvatar"
@@ -114,15 +108,12 @@
<div v-if="shouldShowContextMenu" class="context-menu-wrap"> <div v-if="shouldShowContextMenu" class="context-menu-wrap">
<context-menu <context-menu
v-if="isBubble && !isMessageDeleted" v-if="isBubble && !isMessageDeleted"
:context-menu-position="contextMenuPosition"
:is-open="showContextMenu" :is-open="showContextMenu"
:show-copy="hasText" :enabled-options="contextMenuEnabledOptions"
:show-delete="hasText || hasAttachments" :message="data"
:show-canned-response-option="isOutgoing && hasText" @open="openContextMenu"
:menu-position="contextMenuPosition" @close="closeContextMenu"
:message-content="data.content"
@toggle="handleContextMenuClick"
@delete="handleDelete"
@translate="handleTranslate"
/> />
</div> </div>
</li> </li>
@@ -145,8 +136,8 @@ import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin'; import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper'; import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { mapGetters } from 'vuex'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import TranslateModal from './bubble/TranslateModal.vue'; import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default { export default {
components: { components: {
@@ -162,7 +153,6 @@ export default {
ContextMenu, ContextMenu,
Spinner, Spinner,
instagramImageErrorPlaceholder, instagramImageErrorPlaceholder,
TranslateModal,
}, },
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin], mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
props: { props: {
@@ -191,14 +181,10 @@ export default {
return { return {
showContextMenu: false, showContextMenu: false,
hasImageError: false, hasImageError: false,
showTranslateModal: false, contextMenuPosition: {},
}; };
}, },
computed: { computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
currentAccountId: 'getCurrentAccountId',
}),
shouldRenderMessage() { shouldRenderMessage() {
return ( return (
this.hasAttachments || this.hasAttachments ||
@@ -256,6 +242,13 @@ export default {
) + botMessageContent ) + botMessageContent
); );
}, },
contextMenuEnabledOptions() {
return {
copy: this.hasText,
delete: this.hasText || this.hasAttachments,
cannedResponse: this.isOutgoing && this.hasText,
};
},
contentAttributes() { contentAttributes() {
return this.data.content_attributes || {}; return this.data.content_attributes || {};
}, },
@@ -384,10 +377,6 @@ export default {
if (this.isPending || this.isFailed) return false; if (this.isPending || this.isFailed) return false;
return !this.sender.type || this.sender.type === 'agent_bot'; return !this.sender.type || this.sender.type === 'agent_bot';
}, },
contextMenuPosition() {
const { message_type: messageType } = this.data;
return messageType ? 'right' : 'left';
},
shouldShowContextMenu() { shouldShowContextMenu() {
return !(this.isFailed || this.isPending); return !(this.isFailed || this.isPending);
}, },
@@ -416,6 +405,10 @@ export default {
}, },
mounted() { mounted() {
this.hasImageError = false; this.hasImageError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
},
beforeDestroy() {
bus.$off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
}, },
methods: { methods: {
hasMediaAttachment(type) { hasMediaAttachment(type) {
@@ -429,37 +422,29 @@ export default {
handleContextMenuClick() { handleContextMenuClick() {
this.showContextMenu = !this.showContextMenu; this.showContextMenu = !this.showContextMenu;
}, },
async handleDelete() {
const { conversation_id: conversationId, id: messageId } = this.data;
try {
await this.$store.dispatch('deleteMessage', {
conversationId,
messageId,
});
this.showAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
this.showContextMenu = false;
} catch (error) {
this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
}
},
async retrySendMessage() { async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data); await this.$store.dispatch('sendMessageWithData', this.data);
}, },
onImageLoadError() { onImageLoadError() {
this.hasImageError = true; this.hasImageError = true;
}, },
handleTranslate() { openContextMenu(e) {
const { locale } = this.getAccount(this.currentAccountId); if (getSelection().toString()) {
const { conversation_id: conversationId, id: messageId } = this.data; return;
this.$store.dispatch('translateMessage', { }
conversationId, e.preventDefault();
messageId, if (e.type === 'contextmenu') {
targetLanguage: locale || 'en', this.$track(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
}); }
this.showTranslateModal = true; this.contextMenuPosition = {
x: e.pageX || e.clientX,
y: e.pageY || e.clientY,
};
this.showContextMenu = true;
}, },
onCloseTranslateModal() { closeContextMenu() {
this.showTranslateModal = false; this.showContextMenu = false;
this.contextMenuPosition = { x: null, y: null };
}, },
}, },
}; };

View File

@@ -355,6 +355,7 @@ export default {
}, },
handleScroll(e) { handleScroll(e) {
bus.$emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
this.setScrollParams(); this.setScrollParams();
const dataFetchCheck = const dataFetchCheck =

View File

@@ -13,6 +13,7 @@ export const ACCOUNT_EVENTS = Object.freeze({
ADDED_TO_CANNED_RESPONSE: 'Used added to canned response option', ADDED_TO_CANNED_RESPONSE: 'Used added to canned response option',
ADDED_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute', ADDED_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute',
ADDED_AN_INBOX: 'Added an inbox', ADDED_AN_INBOX: 'Added an inbox',
OPEN_MESSAGE_CONTEXT_MENU: 'Opened message context menu',
}); });
export const LABEL_EVENTS = Object.freeze({ export const LABEL_EVENTS = Object.freeze({

View File

@@ -166,7 +166,13 @@
"COPY": "Copy", "COPY": "Copy",
"DELETE": "Delete", "DELETE": "Delete",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses", "CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate" "TRANSLATE": "Translate",
"DELETE_CONFIRMATION": {
"TITLE": "Are you sure you want to delete this message?",
"MESSAGE": "You cannot undo this action",
"DELETE": "Delete",
"CANCEL": "Cancel"
}
} }
}, },
"EMAIL_TRANSCRIPT": { "EMAIL_TRANSCRIPT": {

View File

@@ -1,7 +1,8 @@
<template> <template>
<div class="context-menu"> <div class="context-menu">
<!-- Add To Canned Responses -->
<woot-modal <woot-modal
v-if="isCannedResponseModalOpen && showCannedResponseOption" v-if="isCannedResponseModalOpen && enabledOptions['cannedResponse']"
:show.sync="isCannedResponseModalOpen" :show.sync="isCannedResponseModalOpen"
:on-close="hideCannedResponseModal" :on-close="hideCannedResponseModal"
> >
@@ -10,156 +11,229 @@
:on-close="hideCannedResponseModal" :on-close="hideCannedResponseModal"
/> />
</woot-modal> </woot-modal>
<!-- Translate Content -->
<translate-modal
v-if="showTranslateModal"
:content="messageContent"
:content-attributes="contentAttributes"
@close="onCloseTranslateModal"
/>
<!-- Confirm Deletion -->
<woot-delete-modal
v-if="showDeleteModal"
class="context-menu--delete-modal"
:show.sync="showDeleteModal"
:on-close="closeDeleteModal"
:on-confirm="confirmDeletion"
:title="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.TITLE')"
:message="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.MESSAGE')"
:confirm-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.DELETE')"
:reject-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.CANCEL')"
/>
<woot-button <woot-button
icon="more-vertical" icon="more-vertical"
color-scheme="secondary" color-scheme="secondary"
variant="clear" variant="clear"
size="small" size="small"
@click="handleContextMenuClick" @click="handleOpen"
/> />
<div <woot-context-menu
v-if="isOpen && !isCannedResponseModalOpen" v-if="isOpen && !isCannedResponseModalOpen"
v-on-clickaway="handleContextMenuClick" :x="contextMenuPosition.x"
class="dropdown-pane dropdown-pane--open" :y="contextMenuPosition.y"
:class="`dropdown-pane--${menuPosition}`" @close="handleClose"
> >
<woot-dropdown-menu> <div class="menu-container">
<woot-dropdown-item v-if="showDelete"> <menu-item
<woot-button v-if="enabledOptions['copy']"
variant="clear" :option="{
color-scheme="alert" icon: 'clipboard',
size="small" label: this.$t('CONVERSATION.CONTEXT_MENU.COPY'),
icon="delete" }"
@click="handleDelete" variant="icon"
> @click="handleCopy"
{{ $t('CONVERSATION.CONTEXT_MENU.DELETE') }} />
</woot-button> <menu-item
</woot-dropdown-item> :option="{
<woot-dropdown-item v-if="showCopy"> icon: 'translate',
<woot-button label: this.$t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
variant="clear" }"
size="small" variant="icon"
icon="clipboard" @click="handleTranslate"
color-scheme="secondary" />
@click="handleCopy" <hr v-if="enabledOptions['cannedResponse']" />
> <menu-item
{{ $t('CONVERSATION.CONTEXT_MENU.COPY') }} v-if="enabledOptions['cannedResponse']"
</woot-button> :option="{
</woot-dropdown-item> icon: 'comment-add',
<woot-dropdown-item v-if="showCannedResponseOption"> label: this.$t(
<woot-button 'CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE'
variant="clear" ),
size="small" }"
icon="comment-add" variant="icon"
color-scheme="secondary" @click="showCannedResponseModal"
@click="showCannedResponseModal" />
> <hr v-if="enabledOptions['delete']" />
{{ $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE') }} <menu-item
</woot-button> v-if="enabledOptions['delete']"
</woot-dropdown-item> :option="{
<woot-dropdown-item> icon: 'delete',
<woot-button label: this.$t('CONVERSATION.CONTEXT_MENU.DELETE'),
variant="clear" }"
size="small" variant="icon"
icon="translate" @click="openDeleteModal"
color-scheme="secondary" />
@click="handleTranslate" </div>
> </woot-context-menu>
{{ $t('CONVERSATION.CONTEXT_MENU.TRANSLATE') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
</div> </div>
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned'; import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
import TranslateModal from 'dashboard/components/widgets/conversation/bubble/TranslateModal';
import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue';
export default { export default {
components: { components: {
AddCannedModal, AddCannedModal,
WootDropdownMenu, TranslateModal,
WootDropdownItem, MenuItem,
}, },
mixins: [alertMixin, clickaway, messageFormatterMixin], mixins: [alertMixin, clickaway, messageFormatterMixin],
props: { props: {
messageContent: { message: {
type: String, type: Object,
default: '', required: true,
}, },
isOpen: { isOpen: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showCopy: { enabledOptions: {
type: Boolean, type: Object,
default: false, default: () => ({}),
}, },
showDelete: { contextMenuPosition: {
type: Boolean, type: Object,
default: false, default: () => ({}),
},
menuPosition: {
type: String,
default: 'left',
},
showCannedResponseOption: {
type: Boolean,
default: true,
}, },
}, },
data() { data() {
return { isCannedResponseModalOpen: false }; return {
isCannedResponseModalOpen: false,
showTranslateModal: false,
showDeleteModal: false,
};
}, },
computed: { computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
currentAccountId: 'getCurrentAccountId',
}),
plainTextContent() { plainTextContent() {
return this.getPlainText(this.messageContent); return this.getPlainText(this.messageContent);
}, },
conversationId() {
return this.message.conversation_id;
},
messageId() {
return this.message.id;
},
messageContent() {
return this.message.content;
},
contentAttributes() {
return this.message.content_attributes;
},
}, },
methods: { methods: {
handleContextMenuClick() {
this.$emit('toggle', !this.isOpen);
},
async handleCopy() { async handleCopy() {
await copyTextToClipboard(this.plainTextContent); await copyTextToClipboard(this.plainTextContent);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL')); this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
this.$emit('toggle', false); this.handleClose();
},
handleDelete() {
this.$emit('delete');
},
hideCannedResponseModal() {
this.isCannedResponseModalOpen = false;
this.$emit('toggle', false);
}, },
showCannedResponseModal() { showCannedResponseModal() {
this.$track(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE); this.$track(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE);
this.isCannedResponseModalOpen = true; this.isCannedResponseModalOpen = true;
}, },
hideCannedResponseModal() {
this.isCannedResponseModalOpen = false;
this.handleClose();
},
handleOpen(e) {
this.$emit('open', e);
},
handleClose(e) {
this.$emit('close', e);
},
handleTranslate() { handleTranslate() {
this.$emit('translate'); const { locale } = this.getAccount(this.currentAccountId);
this.handleContextMenuClick(); this.$store.dispatch('translateMessage', {
conversationId: this.conversationId,
messageId: this.messageId,
targetLanguage: locale || 'en',
});
this.handleClose();
this.showTranslateModal = true;
},
onCloseTranslateModal() {
this.showTranslateModal = false;
},
openDeleteModal() {
this.handleClose();
this.showDeleteModal = true;
},
async confirmDeletion() {
try {
await this.$store.dispatch('deleteMessage', {
conversationId: this.conversationId,
messageId: this.messageId,
});
this.showAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
this.handleClose();
} catch (error) {
this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
}
},
closeDeleteModal() {
this.showDeleteModal = false;
}, },
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-pane { .menu-container {
bottom: var(--space-large); padding: var(--space-smaller);
background-color: var(--white);
box-shadow: var(--shadow-context-menu);
border-radius: var(--border-radius-normal);
hr {
border-bottom: 1px solid var(--color-border-light);
margin: var(--space-smaller);
}
} }
.dropdown-pane--left {
right: var(--space-minus-small); .context-menu--delete-modal {
} ::v-deep {
.dropdown-pane--right { .modal-container {
left: var(--space-minus-small); max-width: 48rem;
h2 {
font-weight: var(--font-weight-medium);
font-size: var(--font-size-default);
}
}
.modal-footer {
padding: var(--space-normal) var(--space-large) var(--space-large);
}
}
} }
</style> </style>

View File

@@ -6,4 +6,5 @@ export const BUS_EVENTS = {
SCROLL_TO_MESSAGE: 'SCROLL_TO_MESSAGE', SCROLL_TO_MESSAGE: 'SCROLL_TO_MESSAGE',
TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU', TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU',
WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT', WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT',
ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL',
}; };