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

@@ -39,6 +39,8 @@ export const issuePageEvents = eventGroup({
body: string;
/** Uploaded under the new comment once it exists. */
files?: File[];
/** Comment it answers (one level of replies). */
replyTo?: string | null;
}>(),
watchRequested: type<{ issueId: string; watching: boolean }>(),
trashRequested: type<{ issueId: string; trashed: boolean }>(),

View File

@@ -92,10 +92,14 @@ export class IssueFacade {
);
}
/** `files` are attached under the new comment. */
addComment(body: string, files: File[] = []): void {
/** `files` are attached under the new comment; `replyTo` answers one. */
addComment(
body: string,
files: File[] = [],
replyTo: string | null = null,
): void {
this.withIssue((issueId) =>
this.dispatch.commentAddRequested({ issueId, body, files }),
this.dispatch.commentAddRequested({ issueId, body, files, replyTo }),
);
}

View File

@@ -460,7 +460,7 @@ export const IssueStore = signalStore(
mergeMap(({ payload }) =>
runCommand(
comments
.add(payload.issueId, payload.body)
.add(payload.issueId, payload.body, payload.replyTo ?? null)
.pipe(
concatMap((comment) =>
payload.files?.length
@@ -474,7 +474,9 @@ export const IssueStore = signalStore(
),
payload.files?.length
? 'Comentario y adjuntos añadidos'
: 'Comentario añadido',
: payload.replyTo
? 'Respuesta añadida'
: 'Comentario añadido',
done,
failed,
),