feat: Add the ability to mention team in private message (#11758)
This PR allows agents to mention entire teams in private messages using `@team_name` syntax. When a team is mentioned, all team members with inbox access are automatically notified. The scheme changes can be found [here](https://github.com/chatwoot/prosemirror-schema/pull/34). --------- Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
@@ -301,11 +301,18 @@ export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
|
||||
const createNode = (editorView, nodeType, content) => {
|
||||
const { state } = editorView;
|
||||
switch (nodeType) {
|
||||
case 'mention':
|
||||
return state.schema.nodes.mention.create({
|
||||
case 'mention': {
|
||||
const mentionType = content.type || 'user';
|
||||
const displayName = content.displayName || content.name;
|
||||
|
||||
const mentionNode = state.schema.nodes.mention.create({
|
||||
userId: content.id,
|
||||
userFullName: content.name,
|
||||
userFullName: displayName,
|
||||
mentionType,
|
||||
});
|
||||
|
||||
return mentionNode;
|
||||
}
|
||||
case 'cannedResponse':
|
||||
return new MessageMarkdownTransformer(messageSchema).parse(content);
|
||||
case 'variable':
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('getContentNode', () => {
|
||||
{
|
||||
userId: content.id,
|
||||
userFullName: content.name,
|
||||
mentionType: 'user',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
insertAtCursor,
|
||||
findNodeToInsertImage,
|
||||
setURLWithQueryAndSize,
|
||||
getContentNode,
|
||||
} from '../editorHelper';
|
||||
import { EditorState } from '@chatwoot/prosemirror-schema';
|
||||
import { EditorView } from '@chatwoot/prosemirror-schema';
|
||||
@@ -18,12 +19,28 @@ const schema = new Schema({
|
||||
nodes: {
|
||||
doc: { content: 'paragraph+' },
|
||||
paragraph: {
|
||||
content: 'text*',
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
toDOM: () => ['p', 0], // Represents a paragraph as a <p> tag in the DOM.
|
||||
},
|
||||
text: {
|
||||
group: 'inline',
|
||||
toDOM: node => node.text, // Represents text as its actual string value.
|
||||
},
|
||||
mention: {
|
||||
attrs: {
|
||||
userId: { default: '' },
|
||||
userFullName: { default: '' },
|
||||
mentionType: { default: 'user' },
|
||||
},
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
toDOM: node => [
|
||||
'span',
|
||||
{ class: 'mention' },
|
||||
`@${node.attrs.userFullName}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -439,3 +456,173 @@ describe('setURLWithQueryAndSize', () => {
|
||||
expect(editorView.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentNode', () => {
|
||||
let mockEditorView;
|
||||
|
||||
beforeEach(() => {
|
||||
mockEditorView = {
|
||||
state: {
|
||||
schema: {
|
||||
nodes: {
|
||||
mention: {
|
||||
create: vi.fn(attrs => ({
|
||||
type: { name: 'mention' },
|
||||
attrs,
|
||||
})),
|
||||
},
|
||||
},
|
||||
text: vi.fn(content => ({ type: { name: 'text' }, text: content })),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('mention node creation', () => {
|
||||
it('creates a user mention node with correct attributes', () => {
|
||||
const userContent = {
|
||||
id: '123',
|
||||
name: 'John Doe',
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
const result = getContentNode(mockEditorView, 'mention', userContent, {
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockEditorView.state.schema.nodes.mention.create
|
||||
).toHaveBeenCalledWith({
|
||||
userId: '123',
|
||||
userFullName: 'John Doe',
|
||||
mentionType: 'user',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {
|
||||
type: { name: 'mention' },
|
||||
attrs: {
|
||||
userId: '123',
|
||||
userFullName: 'John Doe',
|
||||
mentionType: 'user',
|
||||
},
|
||||
},
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a team mention node with correct attributes', () => {
|
||||
const teamContent = {
|
||||
id: '456',
|
||||
name: 'Support Team',
|
||||
type: 'team',
|
||||
};
|
||||
|
||||
const result = getContentNode(mockEditorView, 'mention', teamContent, {
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockEditorView.state.schema.nodes.mention.create
|
||||
).toHaveBeenCalledWith({
|
||||
userId: '456',
|
||||
userFullName: 'Support Team',
|
||||
mentionType: 'team',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {
|
||||
type: { name: 'mention' },
|
||||
attrs: {
|
||||
userId: '456',
|
||||
userFullName: 'Support Team',
|
||||
mentionType: 'team',
|
||||
},
|
||||
},
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to user mention type when type is not specified', () => {
|
||||
const contentWithoutType = {
|
||||
id: '789',
|
||||
name: 'Jane Smith',
|
||||
};
|
||||
|
||||
getContentNode(mockEditorView, 'mention', contentWithoutType, {
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockEditorView.state.schema.nodes.mention.create
|
||||
).toHaveBeenCalledWith({
|
||||
userId: '789',
|
||||
userFullName: 'Jane Smith',
|
||||
mentionType: 'user',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses displayName over name when both are provided', () => {
|
||||
const contentWithDisplayName = {
|
||||
id: '101',
|
||||
name: 'john_doe',
|
||||
displayName: 'John Doe (Admin)',
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
getContentNode(mockEditorView, 'mention', contentWithDisplayName, {
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockEditorView.state.schema.nodes.mention.create
|
||||
).toHaveBeenCalledWith({
|
||||
userId: '101',
|
||||
userFullName: 'John Doe (Admin)',
|
||||
mentionType: 'user',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing displayName by falling back to name', () => {
|
||||
const contentWithoutDisplayName = {
|
||||
id: '102',
|
||||
name: 'jane_smith',
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
getContentNode(mockEditorView, 'mention', contentWithoutDisplayName, {
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockEditorView.state.schema.nodes.mention.create
|
||||
).toHaveBeenCalledWith({
|
||||
userId: '102',
|
||||
userFullName: 'jane_smith',
|
||||
mentionType: 'user',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsupported node types', () => {
|
||||
it('returns null node for unsupported type', () => {
|
||||
const result = getContentNode(mockEditorView, 'unsupported', 'content', {
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
node: null,
|
||||
from: 0,
|
||||
to: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user