feat: Tag agents in a private note (#1688)
Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
@@ -1,55 +1,113 @@
|
||||
<template>
|
||||
<div ref="editor" class="editor-root"></div>
|
||||
<div class="editor-root">
|
||||
<tag-agents
|
||||
v-if="showUserMentions && isPrivate"
|
||||
:search-key="mentionSearchKey"
|
||||
@click="insertMentionNode"
|
||||
/>
|
||||
<div ref="editor"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import {
|
||||
schema,
|
||||
defaultMarkdownParser,
|
||||
defaultMarkdownSerializer,
|
||||
} from 'prosemirror-markdown';
|
||||
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||
|
||||
const createState = (content, placeholder) =>
|
||||
EditorState.create({
|
||||
doc: defaultMarkdownParser.parse(content),
|
||||
plugins: wootWriterSetup({ schema, placeholder }),
|
||||
import {
|
||||
addMentionsToMarkdownSerializer,
|
||||
addMentionsToMarkdownParser,
|
||||
schemaWithMentions,
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/schema';
|
||||
import {
|
||||
suggestionsPlugin,
|
||||
triggerCharacters,
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||
import TagAgents from '../conversation/TagAgents.vue';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { defaultMarkdownParser } from 'prosemirror-markdown';
|
||||
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
|
||||
|
||||
const createState = (content, placeholder, plugins = []) => {
|
||||
return EditorState.create({
|
||||
doc: addMentionsToMarkdownParser(defaultMarkdownParser).parse(content),
|
||||
plugins: wootWriterSetup({
|
||||
schema: schemaWithMentions,
|
||||
placeholder,
|
||||
plugins,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'WootMessageEditor',
|
||||
components: { TagAgents },
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
isPrivate: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastValue: null,
|
||||
showUserMentions: false,
|
||||
mentionSearchKey: '',
|
||||
editorView: null,
|
||||
range: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
plugins() {
|
||||
return [
|
||||
suggestionsPlugin({
|
||||
matcher: triggerCharacters('@'),
|
||||
onEnter: args => {
|
||||
this.showUserMentions = true;
|
||||
this.range = args.range;
|
||||
this.editorView = args.view;
|
||||
return false;
|
||||
},
|
||||
onChange: args => {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
|
||||
this.mentionSearchKey = args.text.replace('@', '');
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
this.mentionSearchKey = '';
|
||||
this.showUserMentions = false;
|
||||
this.editorView = null;
|
||||
return false;
|
||||
},
|
||||
onKeyDown: ({ event }) => {
|
||||
return event.keyCode === 13 && this.showUserMentions;
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showUserMentions(updatedValue) {
|
||||
this.$emit('toggle-user-mention', this.isPrivate && updatedValue);
|
||||
},
|
||||
value(newValue) {
|
||||
if (newValue !== this.lastValue) {
|
||||
this.state = createState(newValue, this.placeholder);
|
||||
this.state = createState(newValue, this.placeholder, this.plugins);
|
||||
this.view.updateState(this.state);
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.state = createState(this.value, this.placeholder);
|
||||
this.state = createState(this.value, this.placeholder, this.plugins);
|
||||
},
|
||||
mounted() {
|
||||
this.view = new EditorView(this.$refs.editor, {
|
||||
state: this.state,
|
||||
dispatchTransaction: tx => {
|
||||
this.state = this.state.apply(tx);
|
||||
this.view.updateState(this.state);
|
||||
this.lastValue = defaultMarkdownSerializer.serialize(this.state.doc);
|
||||
this.$emit('input', this.lastValue);
|
||||
this.emitOnChange();
|
||||
},
|
||||
handleDOMEvents: {
|
||||
keyup: () => {
|
||||
@@ -65,6 +123,33 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
insertMentionNode(mentionItem) {
|
||||
if (!this.view) {
|
||||
return null;
|
||||
}
|
||||
const node = this.view.state.schema.nodes.mention.create({
|
||||
userId: mentionItem.key,
|
||||
userFullName: mentionItem.label,
|
||||
});
|
||||
|
||||
const tr = this.view.state.tr.replaceWith(
|
||||
this.range.from,
|
||||
this.range.to,
|
||||
node
|
||||
);
|
||||
this.state = this.view.state.apply(tr);
|
||||
return this.emitOnChange();
|
||||
},
|
||||
emitOnChange() {
|
||||
this.view.updateState(this.state);
|
||||
this.lastValue = addMentionsToMarkdownSerializer(
|
||||
defaultMarkdownSerializer
|
||||
).serialize(this.state.doc);
|
||||
this.$emit('input', this.lastValue);
|
||||
},
|
||||
hideMentions() {
|
||||
this.showUserMentions = false;
|
||||
},
|
||||
resetTyping() {
|
||||
this.$emit('typing-off');
|
||||
this.idleTimer = null;
|
||||
@@ -115,4 +200,14 @@ export default {
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.is-private {
|
||||
.prosemirror-mention-node {
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--s-300);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: 1px 4px;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</file-upload>
|
||||
</button>
|
||||
<button
|
||||
v-if="enableRichEditor"
|
||||
v-if="enableRichEditor && !isOnPrivateNote"
|
||||
class="button clear button--emoji"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
@click="toggleFormatMode"
|
||||
@@ -102,6 +102,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isOnPrivateNote: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enableRichEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
/>
|
||||
<div class="reply-box__top">
|
||||
<canned-response
|
||||
v-if="showCannedResponsesList"
|
||||
v-on-clickaway="hideCannedResponse"
|
||||
:search-key="cannedResponseSearchKey"
|
||||
v-if="showMentions && hasSlashCommand"
|
||||
v-on-clickaway="hideMentions"
|
||||
:search-key="mentionSearchKey"
|
||||
@click="replaceText"
|
||||
/>
|
||||
<emoji-input
|
||||
@@ -19,7 +19,7 @@
|
||||
:on-click="emojiOnClick"
|
||||
/>
|
||||
<resizable-text-area
|
||||
v-if="!isFormatMode"
|
||||
v-if="!showRichContentEditor"
|
||||
ref="messageInput"
|
||||
v-model="message"
|
||||
class="input"
|
||||
@@ -34,12 +34,14 @@
|
||||
v-else
|
||||
v-model="message"
|
||||
class="input"
|
||||
:is-private="isOnPrivateNote"
|
||||
:placeholder="messagePlaceHolder"
|
||||
:min-height="4"
|
||||
@typing-off="onTypingOff"
|
||||
@typing-on="onTypingOn"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@toggle-user-mention="toggleUserMention"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasAttachments" class="attachment-preview-box">
|
||||
@@ -58,7 +60,8 @@
|
||||
:on-send="sendMessage"
|
||||
:is-send-disabled="isReplyButtonDisabled"
|
||||
:set-format-mode="setFormatMode"
|
||||
:is-format-mode="isFormatMode"
|
||||
:is-on-private-note="isOnPrivateNote"
|
||||
:is-format-mode="showRichContentEditor"
|
||||
:enable-rich-editor="isRichEditorEnabled"
|
||||
:enter-to-send-enabled="enterToSendEnabled"
|
||||
@toggleEnterToSend="toggleEnterToSend"
|
||||
@@ -108,15 +111,23 @@ export default {
|
||||
message: '',
|
||||
isFocused: false,
|
||||
showEmojiPicker: false,
|
||||
showCannedResponsesList: false,
|
||||
showMentions: false,
|
||||
attachedFiles: [],
|
||||
isUploading: false,
|
||||
replyType: REPLY_EDITOR_MODES.REPLY,
|
||||
isFormatMode: false,
|
||||
cannedResponseSearchKey: '',
|
||||
mentionSearchKey: '',
|
||||
hasUserMention: false,
|
||||
hasSlashCommand: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showRichContentEditor() {
|
||||
if (this.isOnPrivateNote) {
|
||||
return true;
|
||||
}
|
||||
return this.isFormatMode;
|
||||
},
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
uiSettings: 'getUISettings',
|
||||
@@ -126,7 +137,7 @@ export default {
|
||||
},
|
||||
isPrivate() {
|
||||
if (this.currentChat.can_reply) {
|
||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
return this.isOnPrivateNote;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -208,18 +219,17 @@ export default {
|
||||
},
|
||||
isRichEditorEnabled() {
|
||||
return (
|
||||
this.isAWebWidgetInbox ||
|
||||
this.isAnEmailChannel ||
|
||||
this.replyType === REPLY_EDITOR_MODES.NOTE
|
||||
this.isAWebWidgetInbox || this.isAnEmailChannel || this.isOnPrivateNote
|
||||
);
|
||||
},
|
||||
isOnPrivateNote() {
|
||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
const { can_reply: canReply } = conversation;
|
||||
const isUserReplyingOnPrivate =
|
||||
this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
if (isUserReplyingOnPrivate) {
|
||||
if (this.isOnPrivateNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,18 +240,15 @@ export default {
|
||||
}
|
||||
},
|
||||
message(updatedMessage) {
|
||||
const isSlashCommand = updatedMessage[0] === '/';
|
||||
this.hasSlashCommand = updatedMessage[0] === '/';
|
||||
const hasNextWord = updatedMessage.includes(' ');
|
||||
const isShortCodeActive = isSlashCommand && !hasNextWord;
|
||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
this.cannedResponseSearchKey = updatedMessage.substr(
|
||||
1,
|
||||
updatedMessage.length
|
||||
);
|
||||
this.showCannedResponsesList = true;
|
||||
this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length);
|
||||
this.showMentions = true;
|
||||
} else {
|
||||
this.cannedResponseSearchKey = '';
|
||||
this.showCannedResponsesList = false;
|
||||
this.mentionSearchKey = '';
|
||||
this.showMentions = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -252,13 +259,19 @@ export default {
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
methods: {
|
||||
toggleUserMention(currentMentionState) {
|
||||
this.hasUserMention = currentMentionState;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (isEscape(e)) {
|
||||
this.hideEmojiPicker();
|
||||
this.hideCannedResponse();
|
||||
this.hideMentions();
|
||||
} else if (isEnter(e)) {
|
||||
const hasSendOnEnterEnabled =
|
||||
(this.isFormatMode && this.enterToSendEnabled) || !this.isFormatMode;
|
||||
(this.showRichContentEditor &&
|
||||
this.enterToSendEnabled &&
|
||||
!this.hasUserMention) ||
|
||||
!this.showRichContentEditor;
|
||||
const shouldSendMessage =
|
||||
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;
|
||||
if (shouldSendMessage) {
|
||||
@@ -279,7 +292,7 @@ export default {
|
||||
if (this.isReplyButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.showCannedResponsesList) {
|
||||
if (!this.showMentions) {
|
||||
const newMessage = this.message;
|
||||
const messagePayload = this.getMessagePayload(newMessage);
|
||||
this.clearMessage();
|
||||
@@ -318,8 +331,8 @@ export default {
|
||||
this.toggleEmojiPicker();
|
||||
}
|
||||
},
|
||||
hideCannedResponse() {
|
||||
this.showCannedResponsesList = false;
|
||||
hideMentions() {
|
||||
this.showMentions = false;
|
||||
},
|
||||
onTypingOn() {
|
||||
this.toggleTyping('on');
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<mention-box :items="items" @mention-select="handleMentionClick" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MentionBox from '../mentions/MentionBox.vue';
|
||||
|
||||
export default {
|
||||
components: { MentionBox },
|
||||
props: {
|
||||
searchKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agents: 'agents/getVerifiedAgents',
|
||||
}),
|
||||
items() {
|
||||
if (!this.searchKey) {
|
||||
return this.agents.map(agent => ({
|
||||
label: agent.name,
|
||||
key: agent.id,
|
||||
description: agent.email,
|
||||
}));
|
||||
}
|
||||
|
||||
return this.agents
|
||||
.filter(agent =>
|
||||
agent.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(this.searchKey.toLocaleLowerCase())
|
||||
)
|
||||
.map(agent => ({
|
||||
label: agent.name,
|
||||
key: agent.id,
|
||||
description: agent.email,
|
||||
}));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleMentionClick(item = {}) {
|
||||
this.$emit('click', item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user