feat: pinned tickets, at hand on the home page and in search

Each person pins up to 20 tickets from the ticket's page («Fijar»). They
show as a strip under the projects on the home page and in the search
palette before typing. Private and without history, like the views of
«my work» (Mongo `pinned_issues`); tickets in the trash or out of reach
are left out, and when the list is full the unreadable ones are dropped
first.

API: GET /api/me/pins, PUT/DELETE /api/issues/{issue}/pin; GraphQL
myPins, pinIssue, unpinIssue and Issue.pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 10:15:28 +02:00
parent 423f82c1a3
commit dfa42a8a1a
32 changed files with 1612 additions and 46 deletions

View File

@@ -32,6 +32,7 @@ import {
import {
CommentsService,
IssuesService,
PinsService,
PresenceService,
ProjectsService,
} from '@task-stream/api/tracker/application';
@@ -94,6 +95,7 @@ export class IssuesController {
private readonly refs: Refs,
private readonly presence: PresenceService,
private readonly projects: ProjectsService,
private readonly pins: PinsService,
) {}
@Get('me/home')
@@ -163,6 +165,41 @@ export class IssuesController {
await this.presence.leave(me.id, await this.refs.issueId(me.id, issue));
}
@Get('me/pins')
@ApiOperation({
summary: 'My pinned tickets, most recently pinned first',
description:
'Across my projects; tickets in the trash or out of my reach are left out.',
})
@ApiOkResponse({ type: [IssueDto] })
@ApiReadErrors()
async myPins(@CurrentUser() me: User) {
return this.refs.issueDtos(await this.pins.mine(me.id), viewerOf(me));
}
@Put('issues/:issue/pin')
@HttpCode(204)
@ISSUE
@ApiOperation({
summary: 'Pin a ticket: keep it at hand on my home page and in search',
description: 'Private to me; at most 20. Pinning it again changes nothing.',
})
@ApiNoContentResponse()
@ApiCommandErrors()
async pin(@CurrentUser() me: User, @Param('issue') issue: string) {
await this.pins.pin(me.id, await this.refs.issueId(me.id, issue), true);
}
@Delete('issues/:issue/pin')
@HttpCode(204)
@ISSUE
@ApiOperation({ summary: 'Unpin a ticket' })
@ApiNoContentResponse()
@ApiCommandErrors()
async unpin(@CurrentUser() me: User, @Param('issue') issue: string) {
await this.pins.pin(me.id, await this.refs.issueId(me.id, issue), false);
}
@Put('issues/:issue/watch')
@ISSUE
@ApiOperation({