Files
task-stream/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts
Josep Piguillem 50d3ffd1dd feat: sprints with burndown and carry-over
Plan sprints with dates and a goal, run one at a time per project, and
put tickets in them from the ticket page. The Sprints tab follows the one
under way with its progress and a burndown in tickets or points against
the ideal line. Closing a sprint sends what was left to the next one or
back to the backlog; the board can be filtered by sprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:59:01 +02:00

240 lines
6.8 KiB
TypeScript

import { inject, Injectable } from '@angular/core';
import {
BoardColumn,
BoardFilter,
ChangeStatusInput,
CommandResult,
CreateIssueInput,
Epic,
Issue,
IssuePage,
IssuesGateway,
IssueSummary,
LinkIssuesInput,
MyIssuesFilter,
UpdateIssueInput,
} from '@task-stream/web/tracker/domain';
import { map, Observable } from 'rxjs';
import {
AddChecklistItemDocument,
AssignIssueDocument,
BacklogDocument,
BoardDocument,
ChangeIssueStatusDocument,
CreateIssueDocument,
EpicsDocument,
IssueChildrenDocument,
IssueDocument,
LinkIssuesDocument,
MyIssuesDocument,
RankIssueDocument,
RemoveChecklistItemDocument,
SearchIssuesDocument,
SetIssueParentDocument,
SetIssueReleaseDocument,
UnlinkIssuesDocument,
UnwatchIssueDocument,
WatchIssueDocument,
UpdateChecklistItemDocument,
UpdateIssueDocument,
} from '../graphql/generated';
import { GraphqlOperations } from './graphql-operations';
import { toCommandResult, toIssue, toIssueSummary } from './mappers';
@Injectable()
export class GraphqlIssuesGateway extends IssuesGateway {
private readonly gql = inject(GraphqlOperations);
board(filter: BoardFilter): Observable<BoardColumn[]> {
return this.gql
.query(BoardDocument, {
idOrKey: filter.projectId,
releaseId: filter.releaseId ?? null,
assigneeId: filter.assigneeId ?? null,
area: filter.area ?? null,
types: filter.types?.length ? filter.types : null,
priorities: filter.priorities?.length
? (filter.priorities as never)
: null,
labels: filter.labels?.length ? filter.labels : null,
search: filter.search?.trim() || null,
unassigned: filter.unassigned ?? null,
sprintId: filter.sprintId ?? null,
})
.pipe(
map((d) =>
d.project.board.map((column) => ({
status: column.status,
total: column.total,
issues: column.issues.map(toIssueSummary),
})),
),
);
}
issue(idOrKey: string): Observable<Issue> {
return this.gql
.query(IssueDocument, { idOrKey })
.pipe(map((d) => toIssue(d.issue)));
}
mine(filter: MyIssuesFilter): Observable<IssuePage> {
return this.gql
.query(MyIssuesDocument, {
assignedToMe: filter.assignedToMe,
categories: filter.categories as never,
limit: filter.limit,
})
.pipe(
map((d) => ({
items: d.myIssues.items.map(toIssueSummary),
total: d.myIssues.total,
})),
);
}
search(projectId: string, text: string): Observable<IssueSummary[]> {
return this.gql
.query(SearchIssuesDocument, { filter: { projectId, search: text } })
.pipe(map((d) => d.issues.items.map(toIssueSummary)));
}
create(input: CreateIssueInput): Observable<CommandResult> {
return this.gql
.mutate(CreateIssueDocument, { input: input as never })
.pipe(map((d) => toCommandResult(d.createIssue)));
}
update(input: UpdateIssueInput): Observable<CommandResult> {
return this.gql
.mutate(UpdateIssueDocument, { input: input as never })
.pipe(map((d) => toCommandResult(d.updateIssue)));
}
changeStatus(input: ChangeStatusInput): Observable<CommandResult> {
return this.gql
.mutate(ChangeIssueStatusDocument, { input: input as never })
.pipe(map((d) => toCommandResult(d.changeIssueStatus)));
}
assign(
issueId: string,
assigneeId: string | null,
): Observable<CommandResult> {
return this.gql
.mutate(AssignIssueDocument, { issueId, assigneeId })
.pipe(map((d) => toCommandResult(d.assignIssue)));
}
link(input: LinkIssuesInput): Observable<CommandResult> {
return this.gql
.mutate(LinkIssuesDocument, { input: input as never })
.pipe(map((d) => toCommandResult(d.linkIssues)));
}
unlink(input: LinkIssuesInput): Observable<CommandResult> {
return this.gql
.mutate(UnlinkIssuesDocument, { input: input as never })
.pipe(map((d) => toCommandResult(d.unlinkIssues)));
}
watch(issueId: string, watching: boolean): Observable<CommandResult> {
return watching
? this.gql
.mutate(WatchIssueDocument, { issueId })
.pipe(map((d) => toCommandResult(d.watchIssue)))
: this.gql
.mutate(UnwatchIssueDocument, { issueId })
.pipe(map((d) => toCommandResult(d.unwatchIssue)));
}
children(projectId: string, issueId: string): Observable<IssueSummary[]> {
return this.gql
.query(IssueChildrenDocument, {
filter: { projectId, parentId: issueId },
})
.pipe(map((d) => d.issues.items.map(toIssueSummary)));
}
setParent(
issueId: string,
parentId: string | null,
): Observable<CommandResult> {
return this.gql
.mutate(SetIssueParentDocument, { issueId, parentId })
.pipe(map((d) => toCommandResult(d.setIssueParent)));
}
epics(projectId: string): Observable<Epic[]> {
return this.gql.query(EpicsDocument, { projectId }).pipe(
map((d) =>
d.epics.map((epic) => ({
issue: toIssueSummary(epic.issue),
progress: { ...epic.progress },
})),
),
);
}
backlog(projectId: string): Observable<IssuePage> {
return this.gql
.query(BacklogDocument, {
filter: { projectId, categories: ['TODO'], sort: 'RANK' },
})
.pipe(
map((d) => ({
items: d.issues.items.map(toIssueSummary),
total: d.issues.total,
})),
);
}
rank(
issueId: string,
aboveIssueId: string | null,
belowIssueId: string | null,
): Observable<CommandResult> {
return this.gql
.mutate(RankIssueDocument, {
input: { issueId, aboveIssueId, belowIssueId },
})
.pipe(map((d) => toCommandResult(d.rankIssue)));
}
addChecklistItem(issueId: string, text: string): Observable<CommandResult> {
return this.gql
.mutate(AddChecklistItemDocument, { input: { issueId, text } })
.pipe(map((d) => toCommandResult(d.addChecklistItem)));
}
updateChecklistItem(
issueId: string,
itemId: string,
changes: { text?: string; done?: boolean },
): Observable<CommandResult> {
return this.gql
.mutate(UpdateChecklistItemDocument, {
input: { issueId, itemId, ...changes },
})
.pipe(map((d) => toCommandResult(d.updateChecklistItem)));
}
removeChecklistItem(
issueId: string,
itemId: string,
): Observable<CommandResult> {
return this.gql
.mutate(RemoveChecklistItemDocument, { issueId, itemId })
.pipe(map((d) => toCommandResult(d.removeChecklistItem)));
}
setRelease(
issueId: string,
releaseId: string | null,
): Observable<CommandResult> {
return this.gql
.mutate(SetIssueReleaseDocument, { issueId, releaseId })
.pipe(map((d) => toCommandResult(d.setIssueRelease)));
}
}