mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat: Adds support for selecting emojis using the keyboard (#10055)
This commit is contained in:
@@ -17,6 +17,8 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
|
|||||||
import TagAgents from '../conversation/TagAgents.vue';
|
import TagAgents from '../conversation/TagAgents.vue';
|
||||||
import CannedResponse from '../conversation/CannedResponse.vue';
|
import CannedResponse from '../conversation/CannedResponse.vue';
|
||||||
import VariableList from '../conversation/VariableList.vue';
|
import VariableList from '../conversation/VariableList.vue';
|
||||||
|
import KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
appendSignature,
|
appendSignature,
|
||||||
removeSignature,
|
removeSignature,
|
||||||
@@ -24,6 +26,7 @@ import {
|
|||||||
scrollCursorIntoView,
|
scrollCursorIntoView,
|
||||||
findNodeToInsertImage,
|
findNodeToInsertImage,
|
||||||
setURLWithQueryAndSize,
|
setURLWithQueryAndSize,
|
||||||
|
getContentNode,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
@@ -35,10 +38,8 @@ import {
|
|||||||
} from 'shared/helpers/KeyboardHelpers';
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import {
|
|
||||||
replaceVariablesInMessage,
|
import { createTypingIndicator } from '@chatwoot/utils';
|
||||||
createTypingIndicator,
|
|
||||||
} from '@chatwoot/utils';
|
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||||
@@ -71,7 +72,12 @@ const createState = (
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WootMessageEditor',
|
name: 'WootMessageEditor',
|
||||||
components: { TagAgents, CannedResponse, VariableList },
|
components: {
|
||||||
|
TagAgents,
|
||||||
|
CannedResponse,
|
||||||
|
VariableList,
|
||||||
|
KeyboardEmojiSelector,
|
||||||
|
},
|
||||||
mixins: [keyboardEventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
@@ -119,9 +125,11 @@ export default {
|
|||||||
showUserMentions: false,
|
showUserMentions: false,
|
||||||
showCannedMenu: false,
|
showCannedMenu: false,
|
||||||
showVariables: false,
|
showVariables: false,
|
||||||
|
showEmojiMenu: false,
|
||||||
mentionSearchKey: '',
|
mentionSearchKey: '',
|
||||||
cannedSearchTerm: '',
|
cannedSearchTerm: '',
|
||||||
variableSearchTerm: '',
|
variableSearchTerm: '',
|
||||||
|
emojiSearchTerm: '',
|
||||||
editorView: null,
|
editorView: null,
|
||||||
range: null,
|
range: null,
|
||||||
state: undefined,
|
state: undefined,
|
||||||
@@ -169,7 +177,7 @@ export default {
|
|||||||
this.editorView = args.view;
|
this.editorView = args.view;
|
||||||
this.range = args.range;
|
this.range = args.range;
|
||||||
|
|
||||||
this.mentionSearchKey = args.text.replace('@', '');
|
this.mentionSearchKey = args.text;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -198,7 +206,7 @@ export default {
|
|||||||
this.editorView = args.view;
|
this.editorView = args.view;
|
||||||
this.range = args.range;
|
this.range = args.range;
|
||||||
|
|
||||||
this.cannedSearchTerm = args.text.replace('/', '');
|
this.cannedSearchTerm = args.text;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
@@ -226,7 +234,7 @@ export default {
|
|||||||
this.editorView = args.view;
|
this.editorView = args.view;
|
||||||
this.range = args.range;
|
this.range = args.range;
|
||||||
|
|
||||||
this.variableSearchTerm = args.text.replace('{{', '');
|
this.variableSearchTerm = args.text;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
@@ -238,6 +246,31 @@ export default {
|
|||||||
return event.keyCode === 13 && this.showVariables;
|
return event.keyCode === 13 && this.showVariables;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
suggestionsPlugin({
|
||||||
|
matcher: triggerCharacters(':', 1), // Trigger after ':' and at least 1 characters
|
||||||
|
suggestionClass: '',
|
||||||
|
onEnter: args => {
|
||||||
|
this.showEmojiMenu = true;
|
||||||
|
this.emojiSearchTerm = args.text || '';
|
||||||
|
this.range = args.range;
|
||||||
|
this.editorView = args.view;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onChange: args => {
|
||||||
|
this.editorView = args.view;
|
||||||
|
this.range = args.range;
|
||||||
|
this.emojiSearchTerm = args.text;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onExit: () => {
|
||||||
|
this.emojiSearchTerm = '';
|
||||||
|
this.showEmojiMenu = false;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onKeyDown: ({ event }) => {
|
||||||
|
return event.keyCode === 13 && this.showEmojiMenu;
|
||||||
|
},
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
@@ -267,6 +300,8 @@ export default {
|
|||||||
},
|
},
|
||||||
editorId() {
|
editorId() {
|
||||||
this.showCannedMenu = false;
|
this.showCannedMenu = false;
|
||||||
|
this.showEmojiMenu = false;
|
||||||
|
this.showVariables = false;
|
||||||
this.cannedSearchTerm = '';
|
this.cannedSearchTerm = '';
|
||||||
this.reloadState(this.value);
|
this.reloadState(this.value);
|
||||||
},
|
},
|
||||||
@@ -517,57 +552,36 @@ export default {
|
|||||||
this.editorView.dispatch(tr.setSelection(selection));
|
this.editorView.dispatch(tr.setSelection(selection));
|
||||||
this.editorView.focus();
|
this.editorView.focus();
|
||||||
},
|
},
|
||||||
insertMentionNode(mentionItem) {
|
/**
|
||||||
|
* Inserts special content (mention, canned response, variable, emoji) into the editor.
|
||||||
|
* @param {string} type - The type of special content to insert. Possible values: 'mention', 'canned_response', 'variable', 'emoji'.
|
||||||
|
* @param {Object|string} content - The content to insert, depending on the type.
|
||||||
|
*/
|
||||||
|
insertSpecialContent(type, content) {
|
||||||
if (!this.editorView) {
|
if (!this.editorView) {
|
||||||
return null;
|
return;
|
||||||
}
|
|
||||||
const node = this.editorView.state.schema.nodes.mention.create({
|
|
||||||
userId: mentionItem.id,
|
|
||||||
userFullName: mentionItem.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.insertNodeIntoEditor(node, this.range.from, this.range.to);
|
|
||||||
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
insertCannedResponse(cannedItem) {
|
|
||||||
const updatedMessage = replaceVariablesInMessage({
|
|
||||||
message: cannedItem,
|
|
||||||
variables: this.variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.editorView) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
let { node, from, to } = getContentNode(
|
||||||
updatedMessage
|
this.editorView,
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
this.range,
|
||||||
|
this.variables
|
||||||
);
|
);
|
||||||
|
|
||||||
const from =
|
if (!node) return;
|
||||||
node.textContent === updatedMessage
|
|
||||||
? this.range.from
|
|
||||||
: this.range.from - 1;
|
|
||||||
|
|
||||||
this.insertNodeIntoEditor(node, from, this.range.to);
|
|
||||||
|
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
insertVariable(variable) {
|
|
||||||
if (!this.editorView) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = `{{${variable}}}`;
|
|
||||||
let node = this.editorView.state.schema.text(content);
|
|
||||||
const { from, to } = this.range;
|
|
||||||
|
|
||||||
this.insertNodeIntoEditor(node, from, to);
|
this.insertNodeIntoEditor(node, from, to);
|
||||||
this.showVariables = false;
|
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
|
const event_map = {
|
||||||
return false;
|
mention: CONVERSATION_EVENTS.USED_MENTIONS,
|
||||||
|
cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
|
||||||
|
variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
|
||||||
|
emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$track(event_map[type]);
|
||||||
},
|
},
|
||||||
openFileBrowser() {
|
openFileBrowser() {
|
||||||
this.$refs.imageUpload.click();
|
this.$refs.imageUpload.click();
|
||||||
@@ -687,17 +701,22 @@ export default {
|
|||||||
<TagAgents
|
<TagAgents
|
||||||
v-if="showUserMentions && isPrivate"
|
v-if="showUserMentions && isPrivate"
|
||||||
:search-key="mentionSearchKey"
|
:search-key="mentionSearchKey"
|
||||||
@click="insertMentionNode"
|
@click="content => insertSpecialContent('mention', content)"
|
||||||
/>
|
/>
|
||||||
<CannedResponse
|
<CannedResponse
|
||||||
v-if="shouldShowCannedResponses"
|
v-if="shouldShowCannedResponses"
|
||||||
:search-key="cannedSearchTerm"
|
:search-key="cannedSearchTerm"
|
||||||
@click="insertCannedResponse"
|
@click="content => insertSpecialContent('cannedResponse', content)"
|
||||||
/>
|
/>
|
||||||
<VariableList
|
<VariableList
|
||||||
v-if="shouldShowVariables"
|
v-if="shouldShowVariables"
|
||||||
:search-key="variableSearchTerm"
|
:search-key="variableSearchTerm"
|
||||||
@click="insertVariable"
|
@click="content => insertSpecialContent('variable', content)"
|
||||||
|
/>
|
||||||
|
<KeyboardEmojiSelector
|
||||||
|
v-if="showEmojiMenu"
|
||||||
|
:search-key="emojiSearchTerm"
|
||||||
|
@click="emoji => insertSpecialContent('emoji', emoji)"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
ref="imageUpload"
|
ref="imageUpload"
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup>
|
||||||
|
import { shallowRef, computed, onMounted } from 'vue';
|
||||||
|
import emojis from 'shared/components/emoji/emojisGroup.json';
|
||||||
|
import MentionBox from '../mentions/MentionBox.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
searchKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['click']);
|
||||||
|
|
||||||
|
const allEmojis = shallowRef([]);
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
if (!props.searchKey) return [];
|
||||||
|
const searchTerm = props.searchKey.toLowerCase();
|
||||||
|
return allEmojis.value.filter(emoji =>
|
||||||
|
emoji.searchString.includes(searchTerm)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadEmojis() {
|
||||||
|
allEmojis.value = emojis.flatMap(group =>
|
||||||
|
group.emojis.map(emoji => ({
|
||||||
|
...emoji,
|
||||||
|
searchString: `${emoji.slug} ${emoji.name}`.toLowerCase(),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMentionClick(item = {}) {
|
||||||
|
emit('click', item.emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadEmojis();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
|
<template>
|
||||||
|
<MentionBox
|
||||||
|
v-if="items.length"
|
||||||
|
type="emoji"
|
||||||
|
:items="items"
|
||||||
|
@mentionSelect="handleMentionClick"
|
||||||
|
>
|
||||||
|
<template #default="{ item, selected }">
|
||||||
|
<span
|
||||||
|
class="max-w-full inline-flex items-center gap-0.5 min-w-0 mb-0 text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 truncate"
|
||||||
|
>
|
||||||
|
{{ item.emoji }}
|
||||||
|
<p
|
||||||
|
class="relative mb-0 truncate bottom-px"
|
||||||
|
:class="{
|
||||||
|
'text-woot-500 dark:text-woot-500': selected,
|
||||||
|
'font-normal': !selected,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
:{{ item.slug }}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</MentionBox>
|
||||||
|
</template>
|
||||||
@@ -41,14 +41,11 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<MentionBox
|
<MentionBox
|
||||||
v-if="items.length"
|
v-if="items.length"
|
||||||
:items="items"
|
:items="items"
|
||||||
@mentionSelect="handleMentionClick"
|
@mentionSelect="handleMentionClick"
|
||||||
>
|
/>
|
||||||
<template slot-scope="{ item }">
|
|
||||||
<strong>{{ item.label }}</strong> - {{ item.description }}
|
|
||||||
</template>
|
|
||||||
</MentionBox>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -56,20 +56,14 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<MentionBox
|
<MentionBox
|
||||||
v-if="items.length"
|
v-if="items.length"
|
||||||
type="variable"
|
type="variable"
|
||||||
:items="items"
|
:items="items"
|
||||||
@mentionSelect="handleVariableClick"
|
@mentionSelect="handleVariableClick"
|
||||||
>
|
/>
|
||||||
<template slot-scope="{ item }">
|
|
||||||
<span class="text-capitalize variable--list-label">
|
|
||||||
{{ item.description }}
|
|
||||||
</span>
|
|
||||||
({{ item.label }})
|
|
||||||
</template>
|
|
||||||
</MentionBox>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -90,22 +90,24 @@ const variableKey = (item = {}) => {
|
|||||||
}"
|
}"
|
||||||
@click="onListItemSelection(index)"
|
@click="onListItemSelection(index)"
|
||||||
>
|
>
|
||||||
<p
|
<slot :item="item" :index="index" :selected="index === selectedIndex">
|
||||||
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
<p
|
||||||
:class="{
|
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
||||||
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
:class="{
|
||||||
}"
|
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
||||||
>
|
}"
|
||||||
{{ item.description }}
|
>
|
||||||
</p>
|
{{ item.description }}
|
||||||
<p
|
</p>
|
||||||
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
<p
|
||||||
:class="{
|
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
||||||
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
:class="{
|
||||||
}"
|
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
||||||
>
|
}"
|
||||||
{{ variableKey(item) }}
|
>
|
||||||
</p>
|
{{ variableKey(item) }}
|
||||||
|
</p>
|
||||||
|
</slot>
|
||||||
</button>
|
</button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
|
|||||||
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
|
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
|
||||||
TRANSLATE_A_MESSAGE: 'Translated a message',
|
TRANSLATE_A_MESSAGE: 'Translated a message',
|
||||||
INSERTED_A_VARIABLE: 'Inserted a variable',
|
INSERTED_A_VARIABLE: 'Inserted a variable',
|
||||||
|
INSERTED_AN_EMOJI: 'Inserted an emoji',
|
||||||
USED_MENTIONS: 'Used mentions',
|
USED_MENTIONS: 'Used mentions',
|
||||||
SEARCH_CONVERSATION: 'Searched conversations',
|
SEARCH_CONVERSATION: 'Searched conversations',
|
||||||
APPLY_FILTER: 'Applied filters in the conversation list',
|
APPLY_FILTER: 'Applied filters in the conversation list',
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
MessageMarkdownTransformer,
|
MessageMarkdownTransformer,
|
||||||
MessageMarkdownSerializer,
|
MessageMarkdownSerializer,
|
||||||
} from '@chatwoot/prosemirror-schema';
|
} from '@chatwoot/prosemirror-schema';
|
||||||
|
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||||
|
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,3 +283,92 @@ export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content Node Creation Helper Functions for
|
||||||
|
* - mention
|
||||||
|
* - canned response
|
||||||
|
* - variable
|
||||||
|
* - emoji
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized node creation function that handles the creation of different types of nodes based on the specified type.
|
||||||
|
* @param {Object} editorView - The editor view instance.
|
||||||
|
* @param {string} nodeType - The type of node to create ('mention', 'cannedResponse', 'variable', 'emoji').
|
||||||
|
* @param {Object|string} content - The content needed to create the node, which varies based on node type.
|
||||||
|
* @returns {Object|null} - The created ProseMirror node or null if the type is not supported.
|
||||||
|
*/
|
||||||
|
const createNode = (editorView, nodeType, content) => {
|
||||||
|
const { state } = editorView;
|
||||||
|
switch (nodeType) {
|
||||||
|
case 'mention':
|
||||||
|
return state.schema.nodes.mention.create({
|
||||||
|
userId: content.id,
|
||||||
|
userFullName: content.name,
|
||||||
|
});
|
||||||
|
case 'cannedResponse':
|
||||||
|
return new MessageMarkdownTransformer(messageSchema).parse(content);
|
||||||
|
case 'variable':
|
||||||
|
return state.schema.text(`{{${content}}}`);
|
||||||
|
case 'emoji':
|
||||||
|
return state.schema.text(content);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object mapping types to their respective node creation functions.
|
||||||
|
*/
|
||||||
|
const nodeCreators = {
|
||||||
|
mention: (editorView, content, from, to) => ({
|
||||||
|
node: createNode(editorView, 'mention', content),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}),
|
||||||
|
cannedResponse: (editorView, content, from, to, variables) => {
|
||||||
|
const updatedMessage = replaceVariablesInMessage({
|
||||||
|
message: content,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
const node = createNode(editorView, 'cannedResponse', updatedMessage);
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
from: node.textContent === updatedMessage ? from : from - 1,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
variable: (editorView, content, from, to) => ({
|
||||||
|
node: createNode(editorView, 'variable', content),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}),
|
||||||
|
emoji: (editorView, content, from, to) => ({
|
||||||
|
node: createNode(editorView, 'emoji', content),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a content node based on the specified type and content, using a functional approach to select the appropriate node creation function.
|
||||||
|
* @param {Object} editorView - The editor view instance.
|
||||||
|
* @param {string} type - The type of content node to create ('mention', 'cannedResponse', 'variable', 'emoji').
|
||||||
|
* @param {string|Object} content - The content to be transformed into a node.
|
||||||
|
* @param {Object} range - An object containing 'from' and 'to' properties indicating the range in the document where the node should be placed.
|
||||||
|
* @param {Object} variables - Optional. Variables to replace in the content, used for 'cannedResponse' type.
|
||||||
|
* @returns {Object} - An object containing the created node and the updated 'from' and 'to' positions.
|
||||||
|
*/
|
||||||
|
export const getContentNode = (
|
||||||
|
editorView,
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
{ from, to },
|
||||||
|
variables
|
||||||
|
) => {
|
||||||
|
const creator = nodeCreators[type];
|
||||||
|
return creator
|
||||||
|
? creator(editorView, content, from, to, variables)
|
||||||
|
: { node: null, from, to };
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Moved from editorHelper.spec.js to editorContentHelper.spec.js
|
||||||
|
// the mock of chatwoot/prosemirror-schema is getting conflicted with other specs
|
||||||
|
import { getContentNode } from '../editorHelper';
|
||||||
|
import {
|
||||||
|
MessageMarkdownTransformer,
|
||||||
|
messageSchema,
|
||||||
|
} from '@chatwoot/prosemirror-schema';
|
||||||
|
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||||
|
|
||||||
|
vi.mock('@chatwoot/prosemirror-schema', () => ({
|
||||||
|
MessageMarkdownTransformer: vi.fn(),
|
||||||
|
messageSchema: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@chatwoot/utils', () => ({
|
||||||
|
replaceVariablesInMessage: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('getContentNode', () => {
|
||||||
|
let editorView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
editorView = {
|
||||||
|
state: {
|
||||||
|
schema: {
|
||||||
|
nodes: {
|
||||||
|
mention: {
|
||||||
|
create: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
text: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMentionNode', () => {
|
||||||
|
it('should create a mention node', () => {
|
||||||
|
const content = { id: 1, name: 'John Doe' };
|
||||||
|
const from = 0;
|
||||||
|
const to = 10;
|
||||||
|
getContentNode(editorView, 'mention', content, {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editorView.state.schema.nodes.mention.create).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
userId: content.id,
|
||||||
|
userFullName: content.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCannedResponseNode', () => {
|
||||||
|
it('should create a canned response node', () => {
|
||||||
|
const content = 'Hello {{name}}';
|
||||||
|
const variables = { name: 'John' };
|
||||||
|
const from = 0;
|
||||||
|
const to = 10;
|
||||||
|
const updatedMessage = 'Hello John';
|
||||||
|
|
||||||
|
replaceVariablesInMessage.mockReturnValue(updatedMessage);
|
||||||
|
MessageMarkdownTransformer.mockImplementation(() => ({
|
||||||
|
parse: vi.fn().mockReturnValue({ textContent: updatedMessage }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { node } = getContentNode(
|
||||||
|
editorView,
|
||||||
|
'cannedResponse',
|
||||||
|
content,
|
||||||
|
{ from, to },
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(replaceVariablesInMessage).toHaveBeenCalledWith({
|
||||||
|
message: content,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
expect(MessageMarkdownTransformer).toHaveBeenCalledWith(messageSchema);
|
||||||
|
expect(node.textContent).toBe(updatedMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getVariableNode', () => {
|
||||||
|
it('should create a variable node', () => {
|
||||||
|
const content = 'name';
|
||||||
|
const from = 0;
|
||||||
|
const to = 10;
|
||||||
|
getContentNode(editorView, 'variable', content, {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editorView.state.schema.text).toHaveBeenCalledWith('{{name}}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEmojiNode', () => {
|
||||||
|
it('should create an emoji node', () => {
|
||||||
|
const content = '😊';
|
||||||
|
const from = 0;
|
||||||
|
const to = 2;
|
||||||
|
getContentNode(editorView, 'emoji', content, {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editorView.state.schema.text).toHaveBeenCalledWith('😊');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getContentNode', () => {
|
||||||
|
it('should return null for invalid type', () => {
|
||||||
|
const content = 'invalid';
|
||||||
|
const from = 0;
|
||||||
|
const to = 10;
|
||||||
|
const { node } = getContentNode(editorView, 'invalid', content, {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(node).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate": "^2.5.2",
|
"@braid/vue-formulate": "^2.5.2",
|
||||||
"@chatwoot/ninja-keys": "1.2.3",
|
"@chatwoot/ninja-keys": "1.2.3",
|
||||||
"@chatwoot/prosemirror-schema": "1.0.11",
|
"@chatwoot/prosemirror-schema": "1.0.13",
|
||||||
"@chatwoot/utils": "^0.0.25",
|
"@chatwoot/utils": "^0.0.25",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@june-so/analytics-next": "^2.0.0",
|
"@june-so/analytics-next": "^2.0.0",
|
||||||
|
|||||||
@@ -2914,10 +2914,10 @@
|
|||||||
hotkeys-js "3.8.7"
|
hotkeys-js "3.8.7"
|
||||||
lit "2.2.6"
|
lit "2.2.6"
|
||||||
|
|
||||||
"@chatwoot/prosemirror-schema@1.0.11":
|
"@chatwoot/prosemirror-schema@1.0.13":
|
||||||
version "1.0.11"
|
version "1.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.11.tgz#b66201be8b09cd4c6370a60607cc5d4f9d924bdb"
|
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.13.tgz#1b7cd82e16bd66d8fcd96bc3415b56e7e40841a0"
|
||||||
integrity sha512-OAUa1CuHtetEPh/ZhRkMLVQHY2/eesCRb2IRdVzjh0z+4spp3fG826y8FbOEsa7m4hisNirnGF/mDH+NMmYetw==
|
integrity sha512-Ki7H2kUxkbDup+A8useTRQb2F45BxdTe1C9VsX4oSRh3WHqWriedEMmpnZy1SCzRm0zEFCw57RA4XppgZVkfrg==
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it-sup "^1.0.0"
|
markdown-it-sup "^1.0.0"
|
||||||
prosemirror-commands "^1.1.4"
|
prosemirror-commands "^1.1.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user