feat: ask a teammate to review a ticket

«Pedir revisión» on a ticket asks a member (with an optional note): they
are notified and see it under «Te piden revisión» in my work until they
answer «Todo bien» or «Pedir cambios» (with what to change) from the
ticket's Revisiones box; whoever asked hears the answer and can withdraw
an open request. Events of the ticket (requested, answered, cancelled),
so they show in its history and the activity feed. Viewers can be asked
and answer; not oneself, nor twice the same person with one pending.

API: requestReview / answerReview / cancelReview, Issue.reviews,
myIssues(reviewing); POST /api/issues/{issue}/reviews,
POST …/reviews/{id}/answer, DELETE …/reviews/{id}. MCP: request_review,
answer_review, my_issues reviewing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:38:30 +02:00
parent 172c53d21d
commit 93d028e870
53 changed files with 2836 additions and 54 deletions

View File

@@ -70,7 +70,15 @@ export enum NotificationKind {
WATCHED = 'WATCHED',
ADDED_TO_PROJECT = 'ADDED_TO_PROJECT',
REMINDER = 'REMINDER',
REVIEW_REQUESTED = 'REVIEW_REQUESTED',
REVIEW_ANSWERED = 'REVIEW_ANSWERED',
}
/** The reactions a comment takes, in the order they are offered (GitHub's). */
export const REACTIONS = ['👍', '👎', '😄', '🎉', '😕', '❤️', '🚀', '👀'];
/** What a reviewer said. */
export enum ReviewVerdict {
APPROVED = 'APPROVED',
CHANGES = 'CHANGES',
}

View File

@@ -121,6 +121,8 @@ export interface MyIssuesFilter {
assignedToMe: boolean;
/** Only the tickets I follow. */
watching?: boolean;
/** Only the tickets I was asked to review (unanswered). */
reviewing?: boolean;
categories?: StatusCategory[];
/** Matches key, title or description. */
search?: string;

View File

@@ -2,6 +2,7 @@ import {
NotificationKind,
IssueLinkType,
IssuePriority,
ReviewVerdict,
ProjectRole,
ReleaseStatus,
SprintStatus,
@@ -192,6 +193,8 @@ export interface Issue extends IssueSummary {
attachments: Attachment[];
/** People following the ticket. */
watchers: UserRef[];
/** Reviews asked, open and answered, oldest first. */
reviews: IssueReview[];
/** Whether I keep it pinned (home page and search). */
pinned: boolean;
/** Whether I follow it with everything under it (an epic). */
@@ -208,6 +211,19 @@ export interface Issue extends IssueSummary {
}
/** Someone else on a ticket's page right now. */
/** A review someone asked on a ticket. */
export interface IssueReview {
id: string;
requester: UserRef;
reviewer: UserRef;
note: string | null;
requestedAt: string;
/** Null while waiting for an answer. */
verdict: ReviewVerdict | null;
answer: string | null;
answeredAt: string | null;
}
/** My own reminder on a ticket. */
export interface IssueReminder {
/** ISO date-time. */

View File

@@ -1,4 +1,4 @@
import { IssuePriority } from '../models/enums';
import { IssuePriority, ReviewVerdict } from '../models/enums';
import { MyDay } from '../models/my-day';
import { Home } from '../models/home';
import { PortfolioProject, ProjectInsights } from '../models/insights';
@@ -143,6 +143,23 @@ export abstract class IssuesGateway {
issueId: string,
releaseId: string | null,
): Observable<CommandResult>;
/** Asks a member to review the ticket. */
abstract requestReview(
issueId: string,
reviewerId: string,
note: string | null,
): Observable<CommandResult>;
/** Answers a review I was asked for. */
abstract answerReview(
issueId: string,
reviewId: string,
verdict: ReviewVerdict,
answer: string | null,
): Observable<CommandResult>;
abstract cancelReview(
issueId: string,
reviewId: string,
): Observable<CommandResult>;
/** The signed-in person follows (or stops following) the ticket. */
abstract watch(issueId: string, watching: boolean): Observable<CommandResult>;
/** My pinned tickets, most recently pinned first. */