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:
@@ -17,6 +17,7 @@ export const homeApiEvents = eventGroup({
|
||||
mine: IssuePage;
|
||||
awaitingQa: IssuePage;
|
||||
following: IssuePage;
|
||||
reviewing: IssuePage;
|
||||
releases: Release[];
|
||||
}>(),
|
||||
loadFailed: type<string>(),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IssueSummary,
|
||||
LinkIssuesInput,
|
||||
UpdateIssueInput,
|
||||
ReviewVerdict,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
|
||||
export const issuePageEvents = eventGroup({
|
||||
@@ -50,6 +51,18 @@ export const issuePageEvents = eventGroup({
|
||||
watchRequested: type<{ issueId: string; watching: boolean }>(),
|
||||
pinRequested: type<{ issueId: string; pinned: boolean }>(),
|
||||
followTreeRequested: type<{ issueId: string; on: boolean }>(),
|
||||
reviewRequested: type<{
|
||||
issueId: string;
|
||||
reviewerId: string;
|
||||
note: string | null;
|
||||
}>(),
|
||||
reviewAnswerRequested: type<{
|
||||
issueId: string;
|
||||
reviewId: string;
|
||||
verdict: ReviewVerdict;
|
||||
answer: string | null;
|
||||
}>(),
|
||||
reviewCancelRequested: type<{ issueId: string; reviewId: string }>(),
|
||||
reminderRequested: type<{
|
||||
issueId: string;
|
||||
/** null: drop it. */
|
||||
|
||||
@@ -21,6 +21,8 @@ export class HomeFacade {
|
||||
/** Open tickets I follow, whoever works on them. */
|
||||
readonly following = this.store.following;
|
||||
readonly followingTotal = this.store.followingTotal;
|
||||
/** Tickets I was asked to review and have not answered. */
|
||||
readonly reviewing = this.store.reviewing;
|
||||
/** Planned releases of my projects, soonest first. */
|
||||
readonly releases = this.store.releases;
|
||||
/** False until the first load: show skeletons, not empty states. */
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IssueLinkType,
|
||||
IssueSummary,
|
||||
UpdateIssueInput,
|
||||
ReviewVerdict,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { issuePageEvents } from '../events/issue.events';
|
||||
@@ -120,6 +121,39 @@ export class IssueFacade {
|
||||
);
|
||||
}
|
||||
|
||||
/** Asks a member to review the open ticket. */
|
||||
requestReview(reviewerId: string, note: string | null = null): void {
|
||||
this.withIssue((issueId) =>
|
||||
this.dispatch.reviewRequested({
|
||||
issueId,
|
||||
reviewerId,
|
||||
note: note?.trim() || null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Answers a review I was asked for. */
|
||||
answerReview(
|
||||
reviewId: string,
|
||||
verdict: ReviewVerdict,
|
||||
answer: string | null = null,
|
||||
): void {
|
||||
this.withIssue((issueId) =>
|
||||
this.dispatch.reviewAnswerRequested({
|
||||
issueId,
|
||||
reviewId,
|
||||
verdict,
|
||||
answer: answer?.trim() || null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
cancelReview(reviewId: string): void {
|
||||
this.withIssue((issueId) =>
|
||||
this.dispatch.reviewCancelRequested({ issueId, reviewId }),
|
||||
);
|
||||
}
|
||||
|
||||
/** Remind me of the ticket then (null: drop it); one per ticket. */
|
||||
setReminder(at: string | null, note: string | null = null): void {
|
||||
this.withIssue((issueId) =>
|
||||
|
||||
@@ -34,6 +34,8 @@ interface HomeState {
|
||||
awaitingQaTotal: number;
|
||||
following: IssueSummary[];
|
||||
followingTotal: number;
|
||||
/** Tickets I was asked to review and have not answered. */
|
||||
reviewing: IssueSummary[];
|
||||
releases: Release[];
|
||||
loaded: boolean;
|
||||
loading: boolean;
|
||||
@@ -54,6 +56,7 @@ export const HomeStore = signalStore(
|
||||
awaitingQaTotal: 0,
|
||||
following: [],
|
||||
followingTotal: 0,
|
||||
reviewing: [],
|
||||
releases: [],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
@@ -88,6 +91,7 @@ export const HomeStore = signalStore(
|
||||
awaitingQaTotal: payload.awaitingQa.total,
|
||||
following: payload.following.items,
|
||||
followingTotal: payload.following.total,
|
||||
reviewing: payload.reviewing.items,
|
||||
releases: payload.releases,
|
||||
loaded: true,
|
||||
loading: false,
|
||||
@@ -123,6 +127,12 @@ export const HomeStore = signalStore(
|
||||
categories: [...OPEN_CATEGORIES],
|
||||
limit: 20,
|
||||
}),
|
||||
reviewing: issues.mine({
|
||||
assignedToMe: false,
|
||||
reviewing: true,
|
||||
categories: [...OPEN_CATEGORIES, StatusCategory.DONE],
|
||||
limit: 20,
|
||||
}),
|
||||
releases: releases.upcoming(5),
|
||||
}).pipe(
|
||||
mapResponse({
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
IssuesGateway,
|
||||
IssueSummary,
|
||||
SprintsGateway,
|
||||
ReviewVerdict,
|
||||
TrackerRealtimeGateway,
|
||||
whenLabel,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
@@ -380,6 +381,53 @@ export const IssueStore = signalStore(
|
||||
),
|
||||
),
|
||||
),
|
||||
events
|
||||
.on(issuePageEvents.reviewRequested)
|
||||
.pipe(
|
||||
mergeMap(({ payload }) =>
|
||||
runCommand(
|
||||
issues.requestReview(
|
||||
payload.issueId,
|
||||
payload.reviewerId,
|
||||
payload.note,
|
||||
),
|
||||
'Revisión pedida',
|
||||
done,
|
||||
failed,
|
||||
),
|
||||
),
|
||||
),
|
||||
events
|
||||
.on(issuePageEvents.reviewAnswerRequested)
|
||||
.pipe(
|
||||
mergeMap(({ payload }) =>
|
||||
runCommand(
|
||||
issues.answerReview(
|
||||
payload.issueId,
|
||||
payload.reviewId,
|
||||
payload.verdict,
|
||||
payload.answer,
|
||||
),
|
||||
payload.verdict === ReviewVerdict.APPROVED
|
||||
? 'Revisado: todo bien'
|
||||
: 'Revisado: pides cambios',
|
||||
done,
|
||||
failed,
|
||||
),
|
||||
),
|
||||
),
|
||||
events
|
||||
.on(issuePageEvents.reviewCancelRequested)
|
||||
.pipe(
|
||||
mergeMap(({ payload }) =>
|
||||
runCommand(
|
||||
issues.cancelReview(payload.issueId, payload.reviewId),
|
||||
'Petición de revisión retirada',
|
||||
done,
|
||||
failed,
|
||||
),
|
||||
),
|
||||
),
|
||||
events
|
||||
.on(issuePageEvents.reminderRequested)
|
||||
.pipe(
|
||||
|
||||
Reference in New Issue
Block a user