feat(server): send comment notifications (#12925)
close CLOUD-228 #### PR Dependency Tree * **PR #12924** * **PR #12925** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
createReplyMutation,
|
createReplyMutation,
|
||||||
deleteCommentMutation,
|
deleteCommentMutation,
|
||||||
deleteReplyMutation,
|
deleteReplyMutation,
|
||||||
|
DocMode,
|
||||||
listCommentChangesQuery,
|
listCommentChangesQuery,
|
||||||
listCommentsQuery,
|
listCommentsQuery,
|
||||||
resolveCommentMutation,
|
resolveCommentMutation,
|
||||||
@@ -64,6 +65,8 @@ e2e('should create comment work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -82,6 +85,8 @@ e2e('should create comment work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -94,6 +99,52 @@ e2e('should create comment work', async t => {
|
|||||||
t.is(result2.createComment.replies.length, 0);
|
t.is(result2.createComment.replies.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
e2e('should create comment with mentions work', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
await app.create(Mockers.DocUser, {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
userId: member.id,
|
||||||
|
type: DocRole.Owner,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(member);
|
||||||
|
|
||||||
|
const count = app.queue.count('notification.sendComment');
|
||||||
|
const result = await app.gql({
|
||||||
|
query: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
mentions: [
|
||||||
|
// send
|
||||||
|
owner.id,
|
||||||
|
// ignore doc owner himself
|
||||||
|
member.id,
|
||||||
|
// ignore not workspace member
|
||||||
|
other.id,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(result.createComment.id);
|
||||||
|
t.false(result.createComment.resolved);
|
||||||
|
t.is(result.createComment.replies.length, 0);
|
||||||
|
// only send one notification to owner
|
||||||
|
t.is(app.queue.count('notification.sendComment'), count + 1);
|
||||||
|
const notification = app.queue.last('notification.sendComment');
|
||||||
|
t.is(notification.name, 'notification.sendComment');
|
||||||
|
t.is(notification.payload.userId, owner.id);
|
||||||
|
});
|
||||||
|
|
||||||
e2e('should create comment work when user is Commenter', async t => {
|
e2e('should create comment work when user is Commenter', async t => {
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
await app.create(Mockers.DocUser, {
|
await app.create(Mockers.DocUser, {
|
||||||
@@ -110,6 +161,8 @@ e2e('should create comment work when user is Commenter', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -134,6 +187,8 @@ e2e('should create comment failed when user is not member', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -166,6 +221,8 @@ e2e('should create comment failed when user is Reader', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -190,6 +247,8 @@ e2e('should update comment work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -225,6 +284,8 @@ e2e('should update comment failed by another user', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -286,6 +347,8 @@ e2e('should resolve comment work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -350,6 +413,8 @@ e2e('should resolve comment work by doc Commenter himself', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -387,6 +452,8 @@ e2e('should resolve comment failed by doc Reader user', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -441,6 +508,8 @@ e2e('should delete comment work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -473,6 +542,8 @@ e2e('should create reply work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -486,6 +557,8 @@ e2e('should create reply work', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -498,6 +571,122 @@ e2e('should create reply work', async t => {
|
|||||||
t.is(result.createReply.commentId, createResult.createComment.id);
|
t.is(result.createReply.commentId, createResult.createComment.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
e2e('should create reply with mentions work', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
await app.create(Mockers.DocUser, {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
userId: member.id,
|
||||||
|
type: DocRole.Owner,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(member);
|
||||||
|
const createResult = await app.gql({
|
||||||
|
query: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const count = app.queue.count('notification.sendComment');
|
||||||
|
const result = await app.gql({
|
||||||
|
query: createReplyMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
mentions: [
|
||||||
|
// send
|
||||||
|
owner.id,
|
||||||
|
// ignore doc owner himself
|
||||||
|
member.id,
|
||||||
|
// ignore not workspace member
|
||||||
|
other.id,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(result.createReply.id);
|
||||||
|
t.is(result.createReply.commentId, createResult.createComment.id);
|
||||||
|
// only send one notification to owner
|
||||||
|
t.is(app.queue.count('notification.sendComment'), count + 1);
|
||||||
|
const notification = app.queue.last('notification.sendComment');
|
||||||
|
t.is(notification.name, 'notification.sendComment');
|
||||||
|
t.is(notification.payload.userId, owner.id);
|
||||||
|
t.is(notification.payload.body.replyId, result.createReply.id);
|
||||||
|
t.is(notification.payload.isMention, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e(
|
||||||
|
'should create reply and send comment notification to doc owner',
|
||||||
|
async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
await app.create(Mockers.DocUser, {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
userId: member.id,
|
||||||
|
type: DocRole.Owner,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(owner);
|
||||||
|
const createResult = await app.gql({
|
||||||
|
query: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const count = app.queue.count('notification.sendComment');
|
||||||
|
const result = await app.gql({
|
||||||
|
query: createReplyMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(result.createReply.id);
|
||||||
|
t.is(result.createReply.commentId, createResult.createComment.id);
|
||||||
|
t.is(app.queue.count('notification.sendComment'), count + 1);
|
||||||
|
const notification = app.queue.last('notification.sendComment');
|
||||||
|
t.is(notification.name, 'notification.sendComment');
|
||||||
|
t.is(notification.payload.userId, member.id);
|
||||||
|
t.is(notification.payload.body.replyId, result.createReply.id);
|
||||||
|
t.is(notification.payload.isMention, undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
e2e('should create reply work when user is Commenter', async t => {
|
e2e('should create reply work when user is Commenter', async t => {
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
await app.create(Mockers.DocUser, {
|
await app.create(Mockers.DocUser, {
|
||||||
@@ -514,6 +703,8 @@ e2e('should create reply work when user is Commenter', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -527,6 +718,8 @@ e2e('should create reply work when user is Commenter', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -547,6 +740,8 @@ e2e('should create reply failed when comment not found', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: 'not-found',
|
commentId: 'not-found',
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -570,6 +765,8 @@ e2e('should create reply failed when user is not member', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -585,6 +782,8 @@ e2e('should create reply failed when user is not member', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -616,6 +815,8 @@ e2e('should create reply failed when user is Reader', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -631,6 +832,8 @@ e2e('should create reply failed when user is Reader', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -655,6 +858,8 @@ e2e('should update reply work when user is reply owner', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -668,6 +873,8 @@ e2e('should update reply work when user is reply owner', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -702,6 +909,8 @@ e2e('should update reply failed when user is not reply owner', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -715,6 +924,8 @@ e2e('should update reply failed when user is not reply owner', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -775,6 +986,8 @@ e2e('should delete reply work when user is reply owner', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -788,6 +1001,8 @@ e2e('should delete reply work when user is reply owner', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -822,6 +1037,8 @@ e2e('should delete reply work when user is doc Editor', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -835,6 +1052,8 @@ e2e('should delete reply work when user is doc Editor', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -870,6 +1089,8 @@ e2e('should delete reply work when user is doc Manager', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: teamWorkspace.id,
|
workspaceId: teamWorkspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -883,6 +1104,8 @@ e2e('should delete reply work when user is doc Manager', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test' }],
|
content: [{ type: 'text', text: 'test' }],
|
||||||
@@ -933,6 +1156,8 @@ e2e('should list comments and changes work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test 1' }],
|
content: [{ type: 'text', text: 'test 1' }],
|
||||||
@@ -949,6 +1174,8 @@ e2e('should list comments and changes work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test 2' }],
|
content: [{ type: 'text', text: 'test 2' }],
|
||||||
@@ -963,6 +1190,8 @@ e2e('should list comments and changes work', async t => {
|
|||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test 3' }],
|
content: [{ type: 'text', text: 'test 3' }],
|
||||||
@@ -976,6 +1205,8 @@ e2e('should list comments and changes work', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test 1 reply 1' }],
|
content: [{ type: 'text', text: 'test 1 reply 1' }],
|
||||||
@@ -991,6 +1222,8 @@ e2e('should list comments and changes work', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test 1 reply 2' }],
|
content: [{ type: 'text', text: 'test 1 reply 2' }],
|
||||||
@@ -1062,6 +1295,8 @@ e2e('should list comments and changes work', async t => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
commentId: createResult.createComment.id,
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
content: {
|
content: {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{ type: 'text', text: 'test 1 reply 3' }],
|
content: [{ type: 'text', text: 'test 1 reply 3' }],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
CommentAttachmentQuotaExceeded,
|
CommentAttachmentQuotaExceeded,
|
||||||
CommentNotFound,
|
CommentNotFound,
|
||||||
type FileUpload,
|
type FileUpload,
|
||||||
|
JobQueue,
|
||||||
readableToBuffer,
|
readableToBuffer,
|
||||||
ReplyNotFound,
|
ReplyNotFound,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
paginateWithCustomCursor,
|
paginateWithCustomCursor,
|
||||||
PaginationInput,
|
PaginationInput,
|
||||||
} from '../../base/graphql';
|
} from '../../base/graphql';
|
||||||
|
import { Comment, DocMode, Models, Reply } from '../../models';
|
||||||
import { CurrentUser } from '../auth/session';
|
import { CurrentUser } from '../auth/session';
|
||||||
import { AccessController, DocAction } from '../permission';
|
import { AccessController, DocAction } from '../permission';
|
||||||
import { CommentAttachmentStorage } from '../storage';
|
import { CommentAttachmentStorage } from '../storage';
|
||||||
@@ -50,7 +52,9 @@ export class CommentResolver {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly service: CommentService,
|
private readonly service: CommentService,
|
||||||
private readonly ac: AccessController,
|
private readonly ac: AccessController,
|
||||||
private readonly commentAttachmentStorage: CommentAttachmentStorage
|
private readonly commentAttachmentStorage: CommentAttachmentStorage,
|
||||||
|
private readonly queue: JobQueue,
|
||||||
|
private readonly models: Models
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Mutation(() => CommentObjectType)
|
@Mutation(() => CommentObjectType)
|
||||||
@@ -64,6 +68,15 @@ export class CommentResolver {
|
|||||||
...input,
|
...input,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.sendCommentNotification(
|
||||||
|
me,
|
||||||
|
comment,
|
||||||
|
input.docTitle,
|
||||||
|
input.docMode,
|
||||||
|
input.mentions
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...comment,
|
...comment,
|
||||||
user: {
|
user: {
|
||||||
@@ -142,6 +155,16 @@ export class CommentResolver {
|
|||||||
...input,
|
...input,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.sendCommentNotification(
|
||||||
|
me,
|
||||||
|
comment,
|
||||||
|
input.docTitle,
|
||||||
|
input.docMode,
|
||||||
|
input.mentions,
|
||||||
|
reply
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...reply,
|
...reply,
|
||||||
user: {
|
user: {
|
||||||
@@ -338,6 +361,74 @@ export class CommentResolver {
|
|||||||
return this.commentAttachmentStorage.getUrl(workspaceId, docId, key);
|
return this.commentAttachmentStorage.getUrl(workspaceId, docId, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendCommentNotification(
|
||||||
|
sender: UserType,
|
||||||
|
comment: Comment,
|
||||||
|
docTitle: string,
|
||||||
|
docMode: DocMode,
|
||||||
|
mentions?: string[],
|
||||||
|
reply?: Reply
|
||||||
|
) {
|
||||||
|
// send comment notification to doc owners
|
||||||
|
const owner = await this.models.docUser.getOwner(
|
||||||
|
comment.workspaceId,
|
||||||
|
comment.docId
|
||||||
|
);
|
||||||
|
if (owner && owner.userId !== sender.id) {
|
||||||
|
await this.queue.add('notification.sendComment', {
|
||||||
|
userId: owner.userId,
|
||||||
|
body: {
|
||||||
|
workspaceId: comment.workspaceId,
|
||||||
|
createdByUserId: sender.id,
|
||||||
|
commentId: comment.id,
|
||||||
|
replyId: reply?.id,
|
||||||
|
doc: {
|
||||||
|
id: comment.docId,
|
||||||
|
title: docTitle,
|
||||||
|
mode: docMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// send comment mention notification to mentioned users
|
||||||
|
if (mentions) {
|
||||||
|
for (const mentionUserId of mentions) {
|
||||||
|
// skip if the mention user is the doc owner
|
||||||
|
if (mentionUserId === owner?.userId || mentionUserId === sender.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the mention user has Doc.Comments.Read permission
|
||||||
|
const hasPermission = await this.ac
|
||||||
|
.user(mentionUserId)
|
||||||
|
.workspace(comment.workspaceId)
|
||||||
|
.doc(comment.docId)
|
||||||
|
.can('Doc.Comments.Read');
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.queue.add('notification.sendComment', {
|
||||||
|
isMention: true,
|
||||||
|
userId: mentionUserId,
|
||||||
|
body: {
|
||||||
|
workspaceId: comment.workspaceId,
|
||||||
|
createdByUserId: sender.id,
|
||||||
|
commentId: comment.id,
|
||||||
|
replyId: reply?.id,
|
||||||
|
doc: {
|
||||||
|
id: comment.docId,
|
||||||
|
title: docTitle,
|
||||||
|
mode: docMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async assertPermission(
|
private async assertPermission(
|
||||||
me: UserType,
|
me: UserType,
|
||||||
item: {
|
item: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
CommentResolve,
|
CommentResolve,
|
||||||
CommentUpdate,
|
CommentUpdate,
|
||||||
DeletedChangeItem,
|
DeletedChangeItem,
|
||||||
|
DocMode,
|
||||||
Reply,
|
Reply,
|
||||||
ReplyCreate,
|
ReplyCreate,
|
||||||
ReplyUpdate,
|
ReplyUpdate,
|
||||||
@@ -150,8 +151,21 @@ export class CommentCreateInput implements Partial<CommentCreate> {
|
|||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
docId!: string;
|
docId!: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
docTitle!: string;
|
||||||
|
|
||||||
|
@Field(() => DocMode)
|
||||||
|
docMode!: DocMode;
|
||||||
|
|
||||||
@Field(() => GraphQLJSONObject)
|
@Field(() => GraphQLJSONObject)
|
||||||
content!: object;
|
content!: object;
|
||||||
|
|
||||||
|
@Field(() => [String], {
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
'The mention user ids, if not provided, the comment will not be mentioned',
|
||||||
|
})
|
||||||
|
mentions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@@ -181,6 +195,19 @@ export class ReplyCreateInput implements Partial<ReplyCreate> {
|
|||||||
|
|
||||||
@Field(() => GraphQLJSONObject)
|
@Field(() => GraphQLJSONObject)
|
||||||
content!: object;
|
content!: object;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
docTitle!: string;
|
||||||
|
|
||||||
|
@Field(() => DocMode)
|
||||||
|
docMode!: DocMode;
|
||||||
|
|
||||||
|
@Field(() => [String], {
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
'The mention user ids, if not provided, the comment reply will not be mentioned',
|
||||||
|
})
|
||||||
|
mentions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ type CommentChangeObjectTypeEdge {
|
|||||||
input CommentCreateInput {
|
input CommentCreateInput {
|
||||||
content: JSONObject!
|
content: JSONObject!
|
||||||
docId: ID!
|
docId: ID!
|
||||||
|
docMode: DocMode!
|
||||||
|
docTitle: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The mention user ids, if not provided, the comment will not be mentioned
|
||||||
|
"""
|
||||||
|
mentions: [String!]
|
||||||
workspaceId: ID!
|
workspaceId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1584,6 +1591,13 @@ input RemoveContextFileInput {
|
|||||||
input ReplyCreateInput {
|
input ReplyCreateInput {
|
||||||
commentId: ID!
|
commentId: ID!
|
||||||
content: JSONObject!
|
content: JSONObject!
|
||||||
|
docMode: DocMode!
|
||||||
|
docTitle: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The mention user ids, if not provided, the comment reply will not be mentioned
|
||||||
|
"""
|
||||||
|
mentions: [String!]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplyObjectType {
|
type ReplyObjectType {
|
||||||
|
|||||||
@@ -165,6 +165,10 @@ export interface CommentChangeObjectTypeEdge {
|
|||||||
export interface CommentCreateInput {
|
export interface CommentCreateInput {
|
||||||
content: Scalars['JSONObject']['input'];
|
content: Scalars['JSONObject']['input'];
|
||||||
docId: Scalars['ID']['input'];
|
docId: Scalars['ID']['input'];
|
||||||
|
docMode: DocMode;
|
||||||
|
docTitle: Scalars['String']['input'];
|
||||||
|
/** The mention user ids, if not provided, the comment will not be mentioned */
|
||||||
|
mentions?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||||
workspaceId: Scalars['ID']['input'];
|
workspaceId: Scalars['ID']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2173,6 +2177,10 @@ export interface RemoveContextFileInput {
|
|||||||
export interface ReplyCreateInput {
|
export interface ReplyCreateInput {
|
||||||
commentId: Scalars['ID']['input'];
|
commentId: Scalars['ID']['input'];
|
||||||
content: Scalars['JSONObject']['input'];
|
content: Scalars['JSONObject']['input'];
|
||||||
|
docMode: DocMode;
|
||||||
|
docTitle: Scalars['String']['input'];
|
||||||
|
/** The mention user ids, if not provided, the comment reply will not be mentioned */
|
||||||
|
mentions?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReplyObjectType {
|
export interface ReplyObjectType {
|
||||||
|
|||||||
Reference in New Issue
Block a user