feat: replies to comments

A comment can answer another one (replyTo), one level deep: answering a
reply joins the same thread, and it must be on the same ticket. Whoever
wrote the comment gets a new REPLIED notification, and the activity says
"respondió a un comentario".

A comment deleted while it has replies stays, without its text
(deleted: true), so the thread still reads; it goes with its last reply
and does not count in commentCount.

The ticket page shows threads with their replies indented and a
"Responder" box; REST, GraphQL and the MCP (add_comment, get_issue) take
and show replyTo. Existing comments are unchanged. Docs, Word and PDF
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 11:14:37 +02:00
parent c2928d2385
commit ea4ef82239
44 changed files with 517 additions and 60 deletions

View File

@@ -42,9 +42,13 @@ export class GraphqlCommentsGateway extends CommentsGateway {
.pipe(map((d) => d.issue.comments.items.map(toComment)));
}
add(issueId: string, body: string): Observable<CommandResult> {
add(
issueId: string,
body: string,
replyTo: string | null = null,
): Observable<CommandResult> {
return this.gql
.mutate(AddCommentDocument, { issueId, body })
.mutate(AddCommentDocument, { issueId, body, replyTo })
.pipe(map((d) => toCommandResult(d.addComment)));
}

View File

@@ -141,6 +141,8 @@ fragment CommentFields on Comment {
id
issueId
body
replyTo
deleted
createdAt
editedAt
author {

View File

@@ -212,6 +212,7 @@ export type NotificationKind =
| 'MENTIONED'
| 'QA_FAILED'
| 'REMINDER'
| 'REPLIED'
| 'STATUS_CHANGED'
| 'WATCHED';
@@ -554,6 +555,8 @@ export type CommentFieldsFragment = {
id: string;
issueId: string;
body: string;
replyTo: string | null;
deleted: boolean;
createdAt: string;
editedAt: string | null;
author: { id: string; name: string };
@@ -875,6 +878,8 @@ export type IssueCommentsQuery = {
id: string;
issueId: string;
body: string;
replyTo: string | null;
deleted: boolean;
createdAt: string;
editedAt: string | null;
author: { id: string; name: string };
@@ -886,6 +891,7 @@ export type IssueCommentsQuery = {
export type AddCommentMutationVariables = Exact<{
issueId: string;
body: string;
replyTo?: string | null | undefined;
}>;
export type AddCommentMutation = {
@@ -1853,6 +1859,8 @@ export type CommentChangedSubscription = {
id: string;
issueId: string;
body: string;
replyTo: string | null;
deleted: boolean;
createdAt: string;
editedAt: string | null;
author: { id: string; name: string };
@@ -2698,6 +2706,8 @@ export const CommentFieldsFragmentDoc = {
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'body' } },
{ kind: 'Field', name: { kind: 'Name', value: 'replyTo' } },
{ kind: 'Field', name: { kind: 'Name', value: 'deleted' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{
@@ -5477,6 +5487,8 @@ export const IssueCommentsDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'body' } },
{ kind: 'Field', name: { kind: 'Name', value: 'replyTo' } },
{ kind: 'Field', name: { kind: 'Name', value: 'deleted' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{
@@ -5527,6 +5539,14 @@ export const AddCommentDocument = {
},
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'replyTo' },
},
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
],
selectionSet: {
kind: 'SelectionSet',
@@ -5551,6 +5571,14 @@ export const AddCommentDocument = {
name: { kind: 'Name', value: 'body' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'replyTo' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'replyTo' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
@@ -11232,6 +11260,8 @@ export const CommentChangedDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'body' } },
{ kind: 'Field', name: { kind: 'Name', value: 'replyTo' } },
{ kind: 'Field', name: { kind: 'Name', value: 'deleted' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{

View File

@@ -112,8 +112,8 @@ query IssueComments($idOrKey: String!) {
}
}
mutation AddComment($issueId: ID!, $body: String!) {
addComment(issueId: $issueId, body: $body) {
mutation AddComment($issueId: ID!, $body: String!, $replyTo: ID) {
addComment(issueId: $issueId, body: $body, replyTo: $replyTo) {
...CommandResultFields
}
}