mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 12:08:01 +00:00
feat: Setup context menu for message (#6750)
This commit is contained in:
@@ -86,6 +86,12 @@
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.justify-content-end {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -355,6 +355,7 @@ export default {
|
||||
},
|
||||
|
||||
handleScroll(e) {
|
||||
bus.$emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
|
||||
this.setScrollParams();
|
||||
|
||||
const dataFetchCheck =
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user