Ctrl/⌘ K (or /) opens a search over the projects and tickets of all my projects, driven by the keyboard. g plus a letter moves between project pages, c creates a ticket on the board, e edits the open ticket and ? lists every shortcut. My work can now be searched by text through the API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
7.5 KiB
TypeScript
261 lines
7.5 KiB
TypeScript
import { inject, Injectable } from '@angular/core';
|
|
import {
|
|
BoardColumn,
|
|
BoardFilter,
|
|
BulkChange,
|
|
BulkResult,
|
|
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,
|
|
BulkUpdateIssuesDocument,
|
|
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,
|
|
search: filter.search?.trim() || null,
|
|
})
|
|
.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 },
|
|
})),
|
|
),
|
|
);
|
|
}
|
|
|
|
bulkUpdate(issueIds: string[], change: BulkChange): Observable<BulkResult> {
|
|
return this.gql
|
|
.mutate(BulkUpdateIssuesDocument, {
|
|
input: {
|
|
issueIds,
|
|
status: change.status ?? null,
|
|
unassign: change.assigneeId === null ? true : null,
|
|
assigneeId: change.assigneeId ?? null,
|
|
noSprint: change.sprintId === null ? true : null,
|
|
sprintId: change.sprintId ?? null,
|
|
priority: (change.priority ?? null) as never,
|
|
addLabels: change.addLabels ?? null,
|
|
},
|
|
})
|
|
.pipe(map((d) => d.bulkUpdateIssues));
|
|
}
|
|
|
|
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)));
|
|
}
|
|
}
|