feat: mentions and following tickets

Mention project members in comments with @, follow and unfollow tickets,
and get told when something changes on the tickets you follow. Commenting
on a ticket starts following it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 09:41:32 +02:00
parent efa06a0723
commit 88928b402f
49 changed files with 1318 additions and 41 deletions

View File

@@ -183,6 +183,25 @@ export class IssueResolver {
);
}
@Mutation(() => CommandResultGql, {
description:
'Follow a ticket: be told when it moves, is reassigned or gets a comment',
})
watchIssue(
@CurrentUser() me: User,
@Args('issueId', { type: () => ID }) issueId: string,
) {
return this.issuesService.watch(me.id, issueId, true);
}
@Mutation(() => CommandResultGql, { description: 'Stop following a ticket' })
unwatchIssue(
@CurrentUser() me: User,
@Args('issueId', { type: () => ID }) issueId: string,
) {
return this.issuesService.watch(me.id, issueId, false);
}
@Mutation(() => CommandResultGql, {
description:
'Remove a file from a ticket (whoever uploaded it, or an owner/admin). Upload with the REST API.',

View File

@@ -143,6 +143,8 @@ export class IssueGql {
@Field(() => String, { nullable: true }) dueDate!: string | null;
@Field(() => [IssueLinkGql]) links!: IssueLinkGql[];
@Field(() => [AttachmentGql]) attachments!: AttachmentGql[];
@Field(() => [UserRefGql], { description: 'People following the ticket' })
watchers!: UserRefGql[];
@Field(() => Int) commentCount!: number;
@Field() createdAt!: string;
@Field() updatedAt!: string;

View File

@@ -159,6 +159,8 @@ export class IssueDto {
dueDate!: string | null;
@ApiProperty({ type: [IssueLinkDto] }) links!: IssueLinkDto[];
@ApiProperty({ type: [AttachmentDto] }) attachments!: AttachmentDto[];
@ApiProperty({ type: [UserRefDto], description: 'People following it' })
watchers!: UserRefDto[];
@ApiProperty() commentCount!: number;
@ApiProperty({ format: 'date-time' }) createdAt!: string;
@ApiProperty({ format: 'date-time' }) updatedAt!: string;

View File

@@ -77,6 +77,36 @@ export class IssuesController {
private readonly refs: Refs,
) {}
@Put('issues/:issue/watch')
@ISSUE
@ApiOperation({
summary: 'Follow a ticket (members, viewers included)',
description:
'Followers are told when it moves, is reassigned or gets a comment. Commenting follows it too.',
})
@ApiOkResponse({ type: CommandResultDto })
@ApiCommandErrors()
async watch(@CurrentUser() me: User, @Param('issue') issue: string) {
return this.issues.watch(
me.id,
await this.refs.issueId(me.id, issue),
true,
);
}
@Delete('issues/:issue/watch')
@ISSUE
@ApiOperation({ summary: 'Stop following a ticket' })
@ApiOkResponse({ type: CommandResultDto })
@ApiCommandErrors()
async unwatch(@CurrentUser() me: User, @Param('issue') issue: string) {
return this.issues.watch(
me.id,
await this.refs.issueId(me.id, issue),
false,
);
}
@Get('me/issues')
@ApiOperation({
summary: 'My work: tickets across all my projects',