Skip to content

Commit

Permalink
feat: Option to return anchor text for comments (#8196)
Browse files Browse the repository at this point in the history
* feat: Option to return anchor text for comments

* cleanup anchorText presentation

* consolidated anchor text

* cleanup unused method
  • Loading branch information
hmacr authored Jan 7, 2025
1 parent 25f264a commit fafaddf
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 10 deletions.
9 changes: 5 additions & 4 deletions app/scenes/Document/components/CommentThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import { ProsemirrorData } from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import Comment from "~/models/Comment";
import Document from "~/models/Document";
import { Avatar, AvatarSize } from "~/components/Avatar";
Expand Down Expand Up @@ -74,10 +75,10 @@ function CommentThread({

const canReply = can.comment && !thread.isResolved;

const highlightedCommentMarks = editor
?.getComments()
.filter((comment) => comment.id === thread.id);
const highlightedText = highlightedCommentMarks?.map((c) => c.text).join("");
const highlightedText = ProsemirrorHelper.getAnchorTextForComment(
editor?.getComments() ?? [],
thread.id
);

const commentsInThread = comments
.inThread(thread.id)
Expand Down
25 changes: 24 additions & 1 deletion server/presenters/comment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { Comment } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import presentUser from "./user";

export default function present(comment: Comment) {
type Options = {
/** Whether to include anchor text, if it exists */
includeAnchorText?: boolean;
};

export default function present(
comment: Comment,
{ includeAnchorText }: Options = {}
) {
let anchorText: string | undefined;

if (includeAnchorText && comment.document) {
const commentMarks = ProsemirrorHelper.getComments(
DocumentHelper.toProsemirror(comment.document)
);
anchorText = ProsemirrorHelper.getAnchorTextForComment(
commentMarks,
comment.id
);
}

return {
id: comment.id,
data: comment.data,
Expand All @@ -15,5 +37,6 @@ export default function present(comment: Comment) {
createdAt: comment.createdAt,
updatedAt: comment.updatedAt,
reactions: comment.reactions ?? [],
anchorText,
};
}
145 changes: 144 additions & 1 deletion server/routes/api/comments/comments.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { CommentStatusFilter, ReactionSummary } from "@shared/types";
import {
CommentStatusFilter,
ProsemirrorData,
ReactionSummary,
} from "@shared/types";
import { Comment, Reaction } from "@server/models";
import {
buildAdmin,
buildCollection,
buildComment,
buildCommentMark,
buildDocument,
buildResolvedComment,
buildTeam,
Expand Down Expand Up @@ -78,6 +83,92 @@ describe("#comments.info", () => {
expect(body.policies[0].abilities.update).toBeTruthy();
expect(body.policies[0].abilities.delete).toBeTruthy();
});

it("should return anchor text for an anchored comment", async () => {
const anchorText = "anchor text";
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
const comment = await buildComment({
userId: user.id,
documentId: document.id,
});
const content = {
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: anchorText,
marks: [buildCommentMark({ id: comment.id, userId: user.id })],
},
],
},
],
} as ProsemirrorData;
await document.update({ content });

const res = await server.post("/api/comments.info", {
body: {
token: user.getJwtToken(),
id: comment.id,
includeAnchorText: true,
},
});
const body = await res.json();

expect(res.status).toEqual(200);
expect(body.data.id).toEqual(comment.id);
expect(body.data.anchorText).toEqual(anchorText);
});

it("should not return anchor text for a non-anchored comment", async () => {
const anchorText = "anchor text";
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
const comment = await buildComment({
userId: user.id,
documentId: document.id,
});
const content = {
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: anchorText,
marks: [buildCommentMark({ userId: user.id })],
},
],
},
],
} as ProsemirrorData;
await document.update({ content });

const res = await server.post("/api/comments.info", {
body: {
token: user.getJwtToken(),
id: comment.id,
includeAnchorText: true,
},
});
const body = await res.json();

expect(res.status).toEqual(200);
expect(body.data.id).toEqual(comment.id);
expect(body.data.anchorText).toBeUndefined();
});
});

describe("#comments.list", () => {
Expand Down Expand Up @@ -120,6 +211,58 @@ describe("#comments.list", () => {
expect(body.pagination.total).toEqual(2);
});

it("should return anchor texts for comments in a document", async () => {
const anchorText = "anchor text";
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
const commentOne = await buildComment({
userId: user.id,
documentId: document.id,
});
const commentTwo = await buildResolvedComment(user, {
userId: user.id,
documentId: document.id,
});
const content = {
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: anchorText,
marks: [buildCommentMark({ id: commentOne.id, userId: user.id })],
},
],
},
],
} as ProsemirrorData;
await document.update({ content });

const res = await server.post("/api/comments.list", {
body: {
token: user.getJwtToken(),
documentId: document.id,
includeAnchorText: true,
sort: "createdAt",
direction: "ASC",
},
});
const body = await res.json();

expect(res.status).toEqual(200);
expect(body.data.length).toEqual(2);
expect(body.data[0].id).toEqual(commentOne.id);
expect(body.data[1].id).toEqual(commentTwo.id);
expect(body.data[0].anchorText).toEqual(anchorText);
expect(body.data[1].anchorText).toBeUndefined();
});

it("should return unresolved comments for a collection", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
Expand Down
12 changes: 9 additions & 3 deletions server/routes/api/comments/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ router.post(
feature(TeamPreference.Commenting),
validate(T.CommentsInfoSchema),
async (ctx: APIContext<T.CommentsInfoReq>) => {
const { id } = ctx.input.body;
const { id, includeAnchorText } = ctx.input.body;
const { user } = ctx.state.auth;

const comment = await Comment.findByPk(id, {
Expand All @@ -72,8 +72,10 @@ router.post(
authorize(user, "read", comment);
authorize(user, "read", document);

comment.document = document;

ctx.body = {
data: presentComment(comment),
data: presentComment(comment, { includeAnchorText }),
policies: presentPolicies(user, [comment]),
};
}
Expand All @@ -93,6 +95,7 @@ router.post(
parentCommentId,
statusFilter,
collectionId,
includeAnchorText,
} = ctx.input.body;
const { user } = ctx.state.auth;
const statusQuery = [];
Expand Down Expand Up @@ -135,6 +138,7 @@ router.post(
Comment.findAll(params),
Comment.count({ where }),
]);
comments.forEach((comment) => (comment.document = document));
} else if (collectionId) {
const collection = await Collection.findByPk(collectionId);
authorize(user, "read", collection);
Expand Down Expand Up @@ -184,7 +188,9 @@ router.post(

ctx.body = {
pagination: { ...ctx.state.pagination, total },
data: comments.map(presentComment),
data: comments.map((comment) =>
presentComment(comment, { includeAnchorText })
),
policies: presentPolicies(user, comments),
};
}
Expand Down
7 changes: 6 additions & 1 deletion server/routes/api/comments/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,18 @@ export const CommentsListSchema = BaseSchema.extend({
parentCommentId: z.string().uuid().optional(),
/** Comment statuses to include in results */
statusFilter: z.nativeEnum(CommentStatusFilter).array().optional(),
/** Whether to include anchor text, if it exists */
includeAnchorText: z.boolean().optional(),
}),
});

export type CommentsListReq = z.infer<typeof CommentsListSchema>;

export const CommentsInfoSchema = z.object({
body: BaseIdSchema,
body: BaseIdSchema.extend({
/** Whether to include anchor text, if it exists */
includeAnchorText: z.boolean().optional(),
}),
});

export type CommentsInfoReq = z.infer<typeof CommentsInfoSchema>;
Expand Down
20 changes: 20 additions & 0 deletions server/test/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,23 @@ export function buildProseMirrorDoc(content: DeepPartial<ProsemirrorData>[]) {
content,
});
}

export function buildCommentMark(overrides: {
id?: string;
userId?: string;
draft?: boolean;
resolved?: boolean;
}) {
if (!overrides.id) {
overrides.id = randomstring.generate(10);
}

if (!overrides.userId) {
overrides.userId = randomstring.generate(10);
}

return {
type: "comment",
attrs: overrides,
};
}
Loading

0 comments on commit fafaddf

Please sign in to comment.