feat: epics, subtasks and checklists

Tickets can hang from another one of the same project, three levels deep
at most (epic → ticket → subtask). The ticket page lists its subtasks with
their progress, creates new ones and moves the ticket under an epic; a new
Épicas tab shows every epic and how far it has got. Each ticket also gets a
checklist for the small steps that don't deserve a ticket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 13:52:43 +02:00
parent 75323115fb
commit d4da33d4e8
84 changed files with 4719 additions and 79 deletions

View File

@@ -408,6 +408,14 @@ export class IssueSearchQueryDto extends PageQueryDto {
@IsString({ each: true })
label?: string[];
@ApiPropertyOptional({
description: 'Only the tickets hanging from this one (id or key)',
example: 'WEB-3',
})
@IsOptional()
@IsString()
parent?: string;
@ApiPropertyOptional({ description: 'Matches key, title or description' })
@IsOptional()
@IsString()
@@ -501,6 +509,17 @@ export class CreateIssueDto extends IssueFieldsDto {
@IsOptional()
@IsString()
releaseId?: string | null;
@ApiPropertyOptional({
type: String,
nullable: true,
example: 'WEB-3',
description:
'Epic or ticket of the same project the new one breaks down (id or key)',
})
@IsOptional()
@IsString()
parent?: string | null;
}
export class UpdateIssueDto extends IssueFieldsDto {
@@ -560,6 +579,41 @@ export class SetIssueReleaseDto {
releaseId!: string | null;
}
export class SetIssueParentDto {
@ApiProperty({
type: String,
nullable: true,
example: 'WEB-3',
description:
'Epic or ticket of the same project (id or key), or null to take it back to the top',
})
@ValidateIf((dto: SetIssueParentDto) => dto.parent !== null)
@IsString()
parent!: string | null;
}
export class ChecklistItemBodyDto {
@ApiProperty({ example: 'Actualizar el changelog', maxLength: 300 })
@IsString()
@MinLength(1)
@MaxLength(300)
text!: string;
}
export class UpdateChecklistItemDto {
@ApiPropertyOptional({ maxLength: 300 })
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(300)
text?: string;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
done?: boolean;
}
export class LinkIssueDto {
@ApiProperty({ enum: IssueLinkType, enumName: 'IssueLinkType' })
@IsEnum(IssueLinkType)