Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9e32791a4 | ||
|
|
a7945384e0 | ||
|
|
20c4b70e8d | ||
|
|
c4e636fce0 | ||
|
|
8ac095dea5 | ||
|
|
ecb28383c1 | ||
|
|
6129cb5f80 | ||
|
|
24a65fc483 | ||
|
|
55b6e5fbf6 |
@@ -3,17 +3,17 @@
|
|||||||
<tag-agents
|
<tag-agents
|
||||||
v-if="showUserMentions && isPrivate"
|
v-if="showUserMentions && isPrivate"
|
||||||
:search-key="mentionSearchKey"
|
:search-key="mentionSearchKey"
|
||||||
@click="insertMentionNode"
|
@click="content => insertSpecialContent('mention', content)"
|
||||||
/>
|
/>
|
||||||
<canned-response
|
<canned-response
|
||||||
v-if="shouldShowCannedResponses"
|
v-if="shouldShowCannedResponses"
|
||||||
:search-key="cannedSearchTerm"
|
:search-key="cannedSearchTerm"
|
||||||
@click="insertCannedResponse"
|
@click="content => insertSpecialContent('cannedResponse', content)"
|
||||||
/>
|
/>
|
||||||
<variable-list
|
<variable-list
|
||||||
v-if="shouldShowVariables"
|
v-if="shouldShowVariables"
|
||||||
:search-key="variableSearchTerm"
|
:search-key="variableSearchTerm"
|
||||||
@click="insertVariable"
|
@click="content => insertSpecialContent('variable', content)"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
ref="imageUpload"
|
ref="imageUpload"
|
||||||
@@ -47,11 +47,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
messageSchema,
|
messageSchema,
|
||||||
buildEditor,
|
|
||||||
EditorView,
|
EditorView,
|
||||||
MessageMarkdownTransformer,
|
MessageMarkdownTransformer,
|
||||||
MessageMarkdownSerializer,
|
MessageMarkdownSerializer,
|
||||||
EditorState,
|
|
||||||
Selection,
|
Selection,
|
||||||
} from '@chatwoot/prosemirror-schema';
|
} from '@chatwoot/prosemirror-schema';
|
||||||
import {
|
import {
|
||||||
@@ -70,6 +68,7 @@ import {
|
|||||||
scrollCursorIntoView,
|
scrollCursorIntoView,
|
||||||
findNodeToInsertImage,
|
findNodeToInsertImage,
|
||||||
setURLWithQueryAndSize,
|
setURLWithQueryAndSize,
|
||||||
|
createState,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
@@ -82,10 +81,7 @@ import {
|
|||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
import {
|
import { createTypingIndicator } from '@chatwoot/utils';
|
||||||
replaceVariablesInMessage,
|
|
||||||
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';
|
||||||
@@ -94,27 +90,7 @@ import {
|
|||||||
MESSAGE_EDITOR_MENU_OPTIONS,
|
MESSAGE_EDITOR_MENU_OPTIONS,
|
||||||
MESSAGE_EDITOR_IMAGE_RESIZES,
|
MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||||
} from 'dashboard/constants/editor';
|
} from 'dashboard/constants/editor';
|
||||||
|
import useSpecialContent from 'dashboard/composables/editor/useSpecialContent';
|
||||||
const createState = (
|
|
||||||
content,
|
|
||||||
placeholder,
|
|
||||||
// eslint-disable-next-line default-param-last
|
|
||||||
plugins = [],
|
|
||||||
// eslint-disable-next-line default-param-last
|
|
||||||
methods = {},
|
|
||||||
enabledMenuOptions
|
|
||||||
) => {
|
|
||||||
return EditorState.create({
|
|
||||||
doc: new MessageMarkdownTransformer(messageSchema).parse(content),
|
|
||||||
plugins: buildEditor({
|
|
||||||
schema: messageSchema,
|
|
||||||
placeholder,
|
|
||||||
methods,
|
|
||||||
plugins,
|
|
||||||
enabledMenuOptions,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WootMessageEditor',
|
name: 'WootMessageEditor',
|
||||||
@@ -139,6 +115,11 @@ export default {
|
|||||||
channelType: { type: String, default: '' },
|
channelType: { type: String, default: '' },
|
||||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { getContentNode } = useSpecialContent();
|
||||||
|
|
||||||
|
return { getContentNode };
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
typingIndicator: createTypingIndicator(
|
typingIndicator: createTypingIndicator(
|
||||||
@@ -452,22 +433,12 @@ export default {
|
|||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
},
|
},
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keyup: () => {
|
keyup: this.onKeyup,
|
||||||
this.onKeyup();
|
keydown: (_view, event) => this.onKeydown(event),
|
||||||
},
|
focus: this.onFocus,
|
||||||
keydown: (view, event) => {
|
click: this.isEditorMouseFocusedOnAnImage,
|
||||||
this.onKeydown(event);
|
blur: this.onBlur,
|
||||||
},
|
paste: (_view, event) => {
|
||||||
focus: () => {
|
|
||||||
this.onFocus();
|
|
||||||
},
|
|
||||||
click: () => {
|
|
||||||
this.isEditorMouseFocusedOnAnImage();
|
|
||||||
},
|
|
||||||
blur: () => {
|
|
||||||
this.onBlur();
|
|
||||||
},
|
|
||||||
paste: (view, event) => {
|
|
||||||
const data = event.clipboardData.files;
|
const data = event.clipboardData.files;
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -551,57 +522,39 @@ 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, or variable) into the editor.
|
||||||
|
*
|
||||||
|
* @param {string} type - The type of special content to insert. Possible values: 'mention', 'canned_response', 'variable'.
|
||||||
|
* @param {Object|string} content - The content to insert, depending on the type.
|
||||||
|
* - For 'mention' type: An object with 'id' and 'name' properties representing the user mention.
|
||||||
|
* - For 'canned_response' type: A string representing the canned response.
|
||||||
|
* - For 'variable' type: A string representing the variable name.
|
||||||
|
*/
|
||||||
|
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 } = this.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,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$track(event_map[type]);
|
||||||
},
|
},
|
||||||
openFileBrowser() {
|
openFileBrowser() {
|
||||||
this.$refs.imageUpload.click();
|
this.$refs.imageUpload.click();
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||||
|
import {
|
||||||
|
MessageMarkdownTransformer,
|
||||||
|
messageSchema,
|
||||||
|
} from '@chatwoot/prosemirror-schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides utility functions for creating special content nodes (mentions, canned responses, variables) in the editor state.
|
||||||
|
*
|
||||||
|
* @module useSpecialContent
|
||||||
|
* @returns {Object} - An object containing the `getContentNode` function.
|
||||||
|
*/
|
||||||
|
export default function useSpecialContent() {
|
||||||
|
/**
|
||||||
|
* Creates a mention node for the editor state
|
||||||
|
* @param {Object} editorView - The editor view instance
|
||||||
|
* @param {Object} content - The content object containing user id and name
|
||||||
|
* @param {number} from - The start position of the mention in the document
|
||||||
|
* @param {number} to - The end position of the mention in the document
|
||||||
|
* @returns {Object} - The created mention node and the updated from and to positions
|
||||||
|
*/
|
||||||
|
const getMentionNode = (editorView, content, from, to) => {
|
||||||
|
const node = editorView.state.schema.nodes.mention.create({
|
||||||
|
userId: content.id,
|
||||||
|
userFullName: content.name,
|
||||||
|
});
|
||||||
|
return { node, from, to };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a canned response node for the editor state
|
||||||
|
* @param {Object} editorView - The editor view instance
|
||||||
|
* @param {string} content - The canned response content
|
||||||
|
* @param {number} from - The start position of the canned response in the document
|
||||||
|
* @param {number} to - The end position of the canned response in the document
|
||||||
|
* @param {Object} variables - The variables to replace in the canned response
|
||||||
|
* @returns {Object} - The created canned response node and the updated from and to positions
|
||||||
|
*/
|
||||||
|
const getCannedResponseNode = (editorView, content, from, to, variables) => {
|
||||||
|
const updatedMessage = replaceVariablesInMessage({
|
||||||
|
message: content,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const node = new MessageMarkdownTransformer(messageSchema).parse(
|
||||||
|
updatedMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
from = node.textContent === updatedMessage ? from : from - 1;
|
||||||
|
|
||||||
|
return { node, from, to };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a variable node for the editor state
|
||||||
|
* @param {Object} editorView - The editor view instance
|
||||||
|
* @param {string} content - The variable content
|
||||||
|
* @param {number} from - The start position of the variable in the document
|
||||||
|
* @param {number} to - The end position of the variable in the document
|
||||||
|
* @returns {Object} - The created variable node and the updated from and to positions
|
||||||
|
*/
|
||||||
|
const getVariableNode = (editorView, content, from, to) => {
|
||||||
|
const node = editorView.state.schema.text(`{{${content}}}`);
|
||||||
|
return { node, from, to };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a content node based on the type (mention, canned response, or variable)
|
||||||
|
* @param {Object} editorView - The editor view instance
|
||||||
|
* @param {string} type - The type of content (mention, cannedResponse, or variable)
|
||||||
|
* @param {string|Object} content - The content to create the node from
|
||||||
|
* @param {Object} range - The range object containing the from and to positions
|
||||||
|
* @param {Object} variables - The variables to replace in the canned response (optional)
|
||||||
|
* @returns {Object} - The created content node and the updated from and to positions
|
||||||
|
*/
|
||||||
|
const getContentNode = (editorView, type, content, range, variables) => {
|
||||||
|
const methodMap = {
|
||||||
|
mention: getMentionNode,
|
||||||
|
cannedResponse: getCannedResponseNode,
|
||||||
|
variable: getVariableNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!methodMap[type]) {
|
||||||
|
return { node: null, from: range.from, to: range.to };
|
||||||
|
}
|
||||||
|
|
||||||
|
let { node, from, to } = methodMap[type](
|
||||||
|
editorView,
|
||||||
|
content,
|
||||||
|
range.from,
|
||||||
|
range.to,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return { node, from, to };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getContentNode,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import useSpecialContent from 'dashboard/composables/editor/useSpecialContent';
|
||||||
|
import {
|
||||||
|
MessageMarkdownTransformer,
|
||||||
|
messageSchema,
|
||||||
|
} from '@chatwoot/prosemirror-schema';
|
||||||
|
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||||
|
|
||||||
|
jest.mock('@chatwoot/prosemirror-schema', () => ({
|
||||||
|
MessageMarkdownTransformer: jest.fn(),
|
||||||
|
messageSchema: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@chatwoot/utils', () => ({
|
||||||
|
replaceVariablesInMessage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useSpecialContent', () => {
|
||||||
|
let editorView;
|
||||||
|
let specialContent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
editorView = {
|
||||||
|
state: {
|
||||||
|
schema: {
|
||||||
|
nodes: {
|
||||||
|
mention: {
|
||||||
|
create: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
text: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
specialContent = useSpecialContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMentionNode', () => {
|
||||||
|
it('should create a mention node', () => {
|
||||||
|
const content = { id: 1, name: 'John Doe' };
|
||||||
|
const from = 0;
|
||||||
|
const to = 10;
|
||||||
|
specialContent.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: jest.fn().mockReturnValue({ textContent: updatedMessage }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { node } = specialContent.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;
|
||||||
|
specialContent.getContentNode(editorView, 'variable', content, {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editorView.state.schema.text).toHaveBeenCalledWith('{{name}}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getContentNode', () => {
|
||||||
|
it('should return null for invalid type', () => {
|
||||||
|
const content = 'invalid';
|
||||||
|
const from = 0;
|
||||||
|
const to = 10;
|
||||||
|
const { node } = specialContent.getContentNode(
|
||||||
|
editorView,
|
||||||
|
'invalid',
|
||||||
|
content,
|
||||||
|
{ from, to }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(node).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,10 @@ import {
|
|||||||
messageSchema,
|
messageSchema,
|
||||||
MessageMarkdownTransformer,
|
MessageMarkdownTransformer,
|
||||||
MessageMarkdownSerializer,
|
MessageMarkdownSerializer,
|
||||||
|
buildEditor,
|
||||||
|
EditorState,
|
||||||
} from '@chatwoot/prosemirror-schema';
|
} from '@chatwoot/prosemirror-schema';
|
||||||
|
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,3 +284,24 @@ export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createState = (
|
||||||
|
content,
|
||||||
|
placeholder,
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
|
plugins = [],
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
|
methods = {},
|
||||||
|
enabledMenuOptions
|
||||||
|
) => {
|
||||||
|
return EditorState.create({
|
||||||
|
doc: new MessageMarkdownTransformer(messageSchema).parse(content),
|
||||||
|
plugins: buildEditor({
|
||||||
|
schema: messageSchema,
|
||||||
|
placeholder,
|
||||||
|
methods,
|
||||||
|
plugins,
|
||||||
|
enabledMenuOptions,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user