Files
task-stream/libs/web/tracker/infrastructure/src/lib/adapters/graphql-collaboration.gateways.ts
Josep Piguillem 3a46a236e4 feat: quick polls in comments that end in a decision
«📊 Añadir una encuesta» adds 2 to 6 options to a new comment. Members,
viewers too, vote by clicking an option (one vote each, which they can
change or take back) and see each option's votes as a bar. Whoever
started it, or an admin, closes it with «Cerrar y decidir»: the most
voted wins (none on a tie) and a «Decidido: …» reply marked as a
decision shows above the ticket. Events of the comment (voted, closed).

API: addComment(…, poll), votePoll, closePoll, Comment.poll;
POST /api/comments/{id}/poll/vote and …/poll/close. MCP: add_comment
poll, vote_poll, close_poll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:48:12 +02:00

209 lines
5.9 KiB
TypeScript

import { inject, Injectable } from '@angular/core';
import {
Activity,
ActivityGateway,
CommandResult,
Comment,
CommentsGateway,
CreateReleaseInput,
Release,
ReleaseBurnup,
ReleasesGateway,
UserDirectoryEntry,
UsersGateway,
} from '@task-stream/web/tracker/domain';
import { map, Observable } from 'rxjs';
import {
ActivityDocument,
ActivityOfDayDocument,
AddCommentDocument,
ArchiveReleaseDocument,
CreateReleaseDocument,
DeleteCommentDocument,
EditCommentDocument,
IssueCommentsDocument,
ReactToCommentDocument,
MarkDecisionDocument,
ResolveThreadDocument,
VotePollDocument,
ClosePollDocument,
PublishReleaseDocument,
ReleaseBurnupDocument,
ReleaseNotesDraftDocument,
WriteReleaseNotesDocument,
ReleasesDocument,
UpcomingReleasesDocument,
SearchUsersDocument,
} from '../graphql/generated';
import { GraphqlOperations } from './graphql-operations';
import { toActivity, toCommandResult, toComment, toRelease } from './mappers';
@Injectable()
export class GraphqlCommentsGateway extends CommentsGateway {
private readonly gql = inject(GraphqlOperations);
list(issueId: string): Observable<Comment[]> {
return this.gql
.query(IssueCommentsDocument, { idOrKey: issueId })
.pipe(map((d) => d.issue.comments.items.map(toComment)));
}
add(
issueId: string,
body: string,
replyTo: string | null = null,
poll: string[] | null = null,
): Observable<CommandResult> {
return this.gql
.mutate(AddCommentDocument, {
issueId,
body,
replyTo,
poll: poll?.length ? poll : null,
})
.pipe(map((d) => toCommandResult(d.addComment)));
}
votePoll(
commentId: string,
option: number | null,
): Observable<CommandResult> {
return this.gql
.mutate(VotePollDocument, { commentId, option })
.pipe(map((d) => toCommandResult(d.votePoll)));
}
closePoll(commentId: string, decide: boolean): Observable<CommandResult> {
return this.gql
.mutate(ClosePollDocument, { commentId, decide })
.pipe(map((d) => toCommandResult(d.closePoll)));
}
react(
commentId: string,
emoji: string,
on: boolean,
): Observable<CommandResult> {
return this.gql
.mutate(ReactToCommentDocument, { commentId, emoji, on })
.pipe(map((d) => toCommandResult(d.reactToComment)));
}
edit(commentId: string, body: string): Observable<CommandResult> {
return this.gql
.mutate(EditCommentDocument, { commentId, body })
.pipe(map((d) => toCommandResult(d.editComment)));
}
remove(commentId: string): Observable<CommandResult> {
return this.gql
.mutate(DeleteCommentDocument, { commentId })
.pipe(map((d) => toCommandResult(d.deleteComment)));
}
resolveThread(
commentId: string,
resolved: boolean,
conclusion: string | null,
): Observable<CommandResult> {
return this.gql
.mutate(ResolveThreadDocument, { commentId, resolved, conclusion })
.pipe(map((d) => toCommandResult(d.resolveThread)));
}
markDecision(commentId: string, on: boolean): Observable<CommandResult> {
return this.gql
.mutate(MarkDecisionDocument, { commentId, on })
.pipe(map((d) => toCommandResult(d.markDecision)));
}
}
@Injectable()
export class GraphqlReleasesGateway extends ReleasesGateway {
private readonly gql = inject(GraphqlOperations);
list(projectId: string): Observable<Release[]> {
return this.gql
.query(ReleasesDocument, { idOrKey: projectId })
.pipe(map((d) => d.project.releases.map(toRelease)));
}
upcoming(limit: number): Observable<Release[]> {
return this.gql
.query(UpcomingReleasesDocument, { limit })
.pipe(map((d) => d.upcomingReleases.map(toRelease)));
}
create(input: CreateReleaseInput): Observable<CommandResult> {
return this.gql
.mutate(CreateReleaseDocument, { input })
.pipe(map((d) => toCommandResult(d.createRelease)));
}
publish(releaseId: string, force: boolean): Observable<CommandResult> {
return this.gql
.mutate(PublishReleaseDocument, { releaseId, force })
.pipe(map((d) => toCommandResult(d.publishRelease)));
}
archive(
releaseId: string,
moveTo: string | null = null,
): Observable<CommandResult> {
return this.gql
.mutate(ArchiveReleaseDocument, { releaseId, moveTo })
.pipe(map((d) => toCommandResult(d.archiveRelease)));
}
burnup(releaseId: string): Observable<ReleaseBurnup> {
return this.gql
.query(ReleaseBurnupDocument, { id: releaseId })
.pipe(map((d) => d.release.burnup));
}
notesDraft(releaseId: string): Observable<string> {
return this.gql
.query(ReleaseNotesDraftDocument, { id: releaseId })
.pipe(map((d) => d.release.notesDraft));
}
writeNotes(
releaseId: string,
notes: string | null,
): Observable<CommandResult> {
return this.gql
.mutate(WriteReleaseNotesDocument, { releaseId, notes })
.pipe(map((d) => toCommandResult(d.writeReleaseNotes)));
}
}
@Injectable()
export class GraphqlActivityGateway extends ActivityGateway {
private readonly gql = inject(GraphqlOperations);
list(projectId: string, issueId?: string): Observable<Activity[]> {
return this.gql
.query(ActivityDocument, { projectId, issueId: issueId ?? null })
.pipe(map((d) => d.activity.map(toActivity)));
}
onDay(projectId: string, day: string): Observable<Activity[]> {
const after = `${day}T00:00:00.000Z`;
const before = new Date(Date.parse(after) + 86_400_000).toISOString();
return this.gql
.query(ActivityOfDayDocument, { projectId, after, before })
.pipe(map((d) => d.activity.map(toActivity)));
}
}
@Injectable()
export class GraphqlUsersGateway extends UsersGateway {
private readonly gql = inject(GraphqlOperations);
search(term: string): Observable<UserDirectoryEntry[]> {
return this.gql
.query(SearchUsersDocument, { search: term })
.pipe(map((d) => d.users));
}
}