«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>
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
|
import {
|
|
DEFAULT_PROJECT_SETTINGS,
|
|
IssueReadModel,
|
|
StatusCategory,
|
|
} from '@task-stream/api/tracker/domain';
|
|
import { FindIssuesRepository, Page } from '../ports/find-issues.repository';
|
|
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
|
import { SearchMyIssuesQuery } from './search-my-issues.query';
|
|
import { guestOnly } from './read-access';
|
|
|
|
@QueryHandler(SearchMyIssuesQuery)
|
|
export class SearchMyIssuesQueryHandler
|
|
implements IQueryHandler<SearchMyIssuesQuery, Page<IssueReadModel>>
|
|
{
|
|
constructor(
|
|
private readonly findProjects: FindProjectsRepository,
|
|
private readonly findIssues: FindIssuesRepository,
|
|
) {}
|
|
|
|
/** Membership is the access rule: only projects the viewer belongs to. */
|
|
async execute(query: SearchMyIssuesQuery): Promise<Page<IssueReadModel>> {
|
|
const { filter } = query;
|
|
const projects = (
|
|
await this.findProjects.findByMember(query.viewerId)
|
|
).filter(
|
|
(p) => !filter.projectIds?.length || filter.projectIds.includes(p.id),
|
|
);
|
|
// Bugs are each project's types drawn with the bug icon.
|
|
const bugTypes = filter.bugsOnly
|
|
? Object.fromEntries(
|
|
projects.map((p) => [
|
|
p.id,
|
|
(p.settings ?? DEFAULT_PROJECT_SETTINGS).issueTypes
|
|
.filter((t) => t.icon === 'bug')
|
|
.map((t) => t.id),
|
|
]),
|
|
)
|
|
: undefined;
|
|
const open = [
|
|
StatusCategory.TODO,
|
|
StatusCategory.IN_PROGRESS,
|
|
StatusCategory.QA,
|
|
];
|
|
const guestProjectIds = projects
|
|
.filter((p) => guestOnly(p, query.viewerId))
|
|
.map((p) => p.id);
|
|
return this.findIssues.searchAcross({
|
|
projectIds: projects.map((project) => project.id),
|
|
guestProjectIds,
|
|
participantId: query.viewerId,
|
|
priorities: filter.priorities?.length ? filter.priorities : undefined,
|
|
typesByProject: bugTypes,
|
|
dueBefore: filter.overdue
|
|
? new Date().toISOString().slice(0, 10)
|
|
: undefined,
|
|
assigneeId: query.filter.assignedToMe ? query.viewerId : undefined,
|
|
watcherId: query.filter.watching ? query.viewerId : undefined,
|
|
reviewerId: query.filter.reviewing ? query.viewerId : undefined,
|
|
statuses: query.filter.statuses,
|
|
categories: filter.overdue
|
|
? (filter.categories?.length ? filter.categories : open).filter((c) =>
|
|
open.includes(c),
|
|
)
|
|
: query.filter.categories,
|
|
search: query.filter.search?.trim().slice(0, 200) || undefined,
|
|
limit: Math.min(query.filter.limit, 200),
|
|
offset: query.filter.offset,
|
|
});
|
|
}
|
|
}
|