import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Put, Query, } from '@nestjs/common'; import { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiParam, ApiTags, } from '@nestjs/swagger'; import { User } from '@task-stream/api/identity/domain'; import { ApiOptionalPireonAuth, ApiPireonAuth, CurrentUser, } from '@task-stream/api/identity/presenters'; import { ApiCommandErrors, ApiReadErrors, } from '@task-stream/api/shared/presenters'; import { IssuesService, IssueSort, SprintsService, } from '@task-stream/api/tracker/application'; import { viewerOf } from '../common/viewer'; import { CloseSprintDto, CreateSprintDto, PageQueryDto, SetIssueSprintDto, UpdateSprintDto, } from './dto/requests.dto'; import { CommandResultDto, IssuePageDto, SprintBurndownDto, SprintDto, SprintProgressDto, } from './dto/responses.dto'; import { Refs } from './refs'; const PROJECT = ApiParam({ name: 'project', description: 'Project id or key', example: 'WEB', }); const SPRINT = ApiParam({ name: 'sprint', format: 'uuid' }); /** REST adapter of the sprint use cases: plan, run and close iterations. */ @ApiTags('Sprints') @ApiPireonAuth() @Controller() export class SprintsController { constructor( private readonly sprints: SprintsService, private readonly issues: IssuesService, private readonly refs: Refs, ) {} @Get('projects/:project/sprints') @ApiOptionalPireonAuth() @PROJECT @ApiOperation({ summary: "A project's sprints", description: 'The one under way, the planned ones, then the closed ones.', }) @ApiOkResponse({ type: [SprintDto] }) @ApiReadErrors() async list( @CurrentUser() me: User | undefined, @Param('project') project: string, ) { return this.sprints.list( viewerOf(me), await this.refs.projectId(viewerOf(me), project), ); } @Post('projects/:project/sprints') @PROJECT @ApiOperation({ summary: 'Plan a sprint (owners and admins)' }) @ApiCreatedResponse({ type: CommandResultDto }) @ApiCommandErrors() async create( @CurrentUser() me: User, @Param('project') project: string, @Body() body: CreateSprintDto, ) { return this.sprints.create(me.id, { ...body, projectId: await this.refs.projectId(me.id, project), }); } @Get('sprints/:sprint') @ApiOptionalPireonAuth() @SPRINT @ApiOperation({ summary: 'A sprint' }) @ApiOkResponse({ type: SprintDto }) @ApiReadErrors() findOne( @CurrentUser() me: User | undefined, @Param('sprint', ParseUUIDPipe) sprint: string, ) { return this.sprints.findOne(viewerOf(me), sprint); } @Patch('sprints/:sprint') @SPRINT @ApiOperation({ summary: 'Rename, re-date or change the goal (not once closed)', }) @ApiOkResponse({ type: CommandResultDto }) @ApiCommandErrors() update( @CurrentUser() me: User, @Param('sprint', ParseUUIDPipe) sprint: string, @Body() body: UpdateSprintDto, ) { return this.sprints.update(me.id, sprint, body); } @Delete('sprints/:sprint') @SPRINT @ApiOperation({ summary: 'Delete a sprint that never started', description: 'Its tickets go back to the backlog.', }) @ApiOkResponse({ type: CommandResultDto }) @ApiCommandErrors() delete( @CurrentUser() me: User, @Param('sprint', ParseUUIDPipe) sprint: string, ) { return this.sprints.delete(me.id, sprint); } @Post('sprints/:sprint/start') @SPRINT @ApiOperation({ summary: 'Start a planned sprint (only one under way per project)', }) @ApiCreatedResponse({ type: CommandResultDto }) @ApiCommandErrors() start( @CurrentUser() me: User, @Param('sprint', ParseUUIDPipe) sprint: string, ) { return this.sprints.start(me.id, sprint); } @Post('sprints/:sprint/close') @SPRINT @ApiOperation({ summary: 'Close the sprint under way', description: 'Unfinished tickets move to `carryOverTo` (another open sprint) or back to the backlog; finished ones stay as its record.', }) @ApiCreatedResponse({ type: CommandResultDto }) @ApiCommandErrors() close( @CurrentUser() me: User, @Param('sprint', ParseUUIDPipe) sprint: string, @Body() body: CloseSprintDto, ) { return this.sprints.close(me.id, sprint, body.carryOverTo ?? null); } @Get('sprints/:sprint/progress') @ApiOptionalPireonAuth() @SPRINT @ApiOperation({ summary: 'Finished tickets and points' }) @ApiOkResponse({ type: SprintProgressDto }) @ApiReadErrors() progress( @CurrentUser() me: User | undefined, @Param('sprint', ParseUUIDPipe) sprint: string, ) { return this.sprints.progress(viewerOf(me), sprint); } @Get('sprints/:sprint/burndown') @ApiOptionalPireonAuth() @SPRINT @ApiOperation({ summary: 'Open tickets and points, day by day, against the ideal line', }) @ApiOkResponse({ type: SprintBurndownDto }) @ApiReadErrors() burndown( @CurrentUser() me: User | undefined, @Param('sprint', ParseUUIDPipe) sprint: string, ) { return this.sprints.burndown(viewerOf(me), sprint); } @Get('sprints/:sprint/issues') @ApiOptionalPireonAuth() @SPRINT @ApiOperation({ summary: 'Tickets in the sprint, in backlog order' }) @ApiOkResponse({ type: IssuePageDto }) @ApiReadErrors() async issuesOf( @CurrentUser() me: User | undefined, @Param('sprint', ParseUUIDPipe) sprint: string, @Query() query: PageQueryDto, ) { const found = await this.sprints.findOne(viewerOf(me), sprint); const page = await this.issues.search(viewerOf(me), { projectId: found.projectId, sprintId: found.id, sort: IssueSort.RANK, limit: query.limit, offset: query.offset, }); return { ...page, items: await this.refs.issueDtos(page.items, viewerOf(me)), }; } @Put('issues/:issue/sprint') @ApiParam({ name: 'issue', description: 'Ticket id or key', example: 'WEB-12', }) @ApiOperation({ summary: 'Put the ticket in a sprint (null: the backlog)' }) @ApiOkResponse({ type: CommandResultDto }) @ApiCommandErrors() async setIssueSprint( @CurrentUser() me: User, @Param('issue') issue: string, @Body() body: SetIssueSprintDto, ) { return this.sprints.setIssueSprint( me.id, await this.refs.issueId(me.id, issue), body.sprintId, ); } }