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);
}
&:first-child {
.button {
margin-left: 0;
}
}
&.justify-content-end {
justify-content: end;
}

View File

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

View File

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

View File

@@ -355,6 +355,7 @@ export default {
},
handleScroll(e) {
bus.$emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
this.setScrollParams();
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_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute',
ADDED_AN_INBOX: 'Added an inbox',
OPEN_MESSAGE_CONTEXT_MENU: 'Opened message context menu',
});
export const LABEL_EVENTS = Object.freeze({

View File

@@ -166,7 +166,13 @@
"COPY": "Copy",
"DELETE": "Delete",
"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": {

View File

@@ -1,7 +1,8 @@
<template>
<div class="context-menu">
<!-- Add To Canned Responses -->
<woot-modal
v-if="isCannedResponseModalOpen && showCannedResponseOption"
v-if="isCannedResponseModalOpen && enabledOptions['cannedResponse']"
:show.sync="isCannedResponseModalOpen"
:on-close="hideCannedResponseModal"
>
@@ -10,156 +11,229 @@
:on-close="hideCannedResponseModal"
/>
</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
icon="more-vertical"
color-scheme="secondary"
variant="clear"
size="small"
@click="handleContextMenuClick"
@click="handleOpen"
/>
<div
<woot-context-menu
v-if="isOpen && !isCannedResponseModalOpen"
v-on-clickaway="handleContextMenuClick"
class="dropdown-pane dropdown-pane--open"
:class="`dropdown-pane--${menuPosition}`"
:x="contextMenuPosition.x"
:y="contextMenuPosition.y"
@close="handleClose"
>
<woot-dropdown-menu>
<woot-dropdown-item v-if="showDelete">
<woot-button
variant="clear"
color-scheme="alert"
size="small"
icon="delete"
@click="handleDelete"
>
{{ $t('CONVERSATION.CONTEXT_MENU.DELETE') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="showCopy">
<woot-button
variant="clear"
size="small"
icon="clipboard"
color-scheme="secondary"
@click="handleCopy"
>
{{ $t('CONVERSATION.CONTEXT_MENU.COPY') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="showCannedResponseOption">
<woot-button
variant="clear"
size="small"
icon="comment-add"
color-scheme="secondary"
@click="showCannedResponseModal"
>
{{ $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
size="small"
icon="translate"
color-scheme="secondary"
@click="handleTranslate"
>
{{ $t('CONVERSATION.CONTEXT_MENU.TRANSLATE') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
<div class="menu-container">
<menu-item
v-if="enabledOptions['copy']"
:option="{
icon: 'clipboard',
label: this.$t('CONVERSATION.CONTEXT_MENU.COPY'),
}"
variant="icon"
@click="handleCopy"
/>
<menu-item
:option="{
icon: 'translate',
label: this.$t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
}"
variant="icon"
@click="handleTranslate"
/>
<hr v-if="enabledOptions['cannedResponse']" />
<menu-item
v-if="enabledOptions['cannedResponse']"
:option="{
icon: 'comment-add',
label: this.$t(
'CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE'
),
}"
variant="icon"
@click="showCannedResponseModal"
/>
<hr v-if="enabledOptions['delete']" />
<menu-item
v-if="enabledOptions['delete']"
:option="{
icon: 'delete',
label: this.$t('CONVERSATION.CONTEXT_MENU.DELETE'),
}"
variant="icon"
@click="openDeleteModal"
/>
</div>
</woot-context-menu>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
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 { 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 {
components: {
AddCannedModal,
WootDropdownMenu,
WootDropdownItem,
TranslateModal,
MenuItem,
},
mixins: [alertMixin, clickaway, messageFormatterMixin],
props: {
messageContent: {
type: String,
default: '',
message: {
type: Object,
required: true,
},
isOpen: {
type: Boolean,
default: false,
},
showCopy: {
type: Boolean,
default: false,
enabledOptions: {
type: Object,
default: () => ({}),
},
showDelete: {
type: Boolean,
default: false,
},
menuPosition: {
type: String,
default: 'left',
},
showCannedResponseOption: {
type: Boolean,
default: true,
contextMenuPosition: {
type: Object,
default: () => ({}),
},
},
data() {
return { isCannedResponseModalOpen: false };
return {
isCannedResponseModalOpen: false,
showTranslateModal: false,
showDeleteModal: false,
};
},
computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
currentAccountId: 'getCurrentAccountId',
}),
plainTextContent() {
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: {
handleContextMenuClick() {
this.$emit('toggle', !this.isOpen);
},
async handleCopy() {
await copyTextToClipboard(this.plainTextContent);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
this.$emit('toggle', false);
},
handleDelete() {
this.$emit('delete');
},
hideCannedResponseModal() {
this.isCannedResponseModalOpen = false;
this.$emit('toggle', false);
this.handleClose();
},
showCannedResponseModal() {
this.$track(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE);
this.isCannedResponseModalOpen = true;
},
hideCannedResponseModal() {
this.isCannedResponseModalOpen = false;
this.handleClose();
},
handleOpen(e) {
this.$emit('open', e);
},
handleClose(e) {
this.$emit('close', e);
},
handleTranslate() {
this.$emit('translate');
this.handleContextMenuClick();
const { locale } = this.getAccount(this.currentAccountId);
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>
<style lang="scss" scoped>
.dropdown-pane {
bottom: var(--space-large);
.menu-container {
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);
}
.dropdown-pane--right {
left: var(--space-minus-small);
.context-menu--delete-modal {
::v-deep {
.modal-container {
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>

View File

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