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

@@ -363,6 +363,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/projects/{project}/epics": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Epics: top-level tickets that group others
* @description With the progress of the tickets hanging from each one (done or cancelled, and story points).
*/
get: operations["IssuesController_epics"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/issues/{issue}": {
parameters: {
query?: never;
@@ -435,6 +455,61 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/issues/{issue}/parent": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/**
* Place the ticket under an epic or another ticket
* @description Same project, three levels at most (epic → ticket → subtask), no loops. null takes it back to the top.
*/
put: operations["IssuesController_setParent"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/issues/{issue}/checklist": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Add a step to the ticket's checklist */
post: operations["IssuesController_addChecklistItem"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/issues/{issue}/checklist/{item}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Remove a checklist step */
delete: operations["IssuesController_removeChecklistItem"];
options?: never;
head?: never;
/** Reword, tick or untick a checklist step */
patch: operations["IssuesController_updateChecklistItem"];
trace?: never;
};
"/api/issues/{issue}/links": {
parameters: {
query?: never;
@@ -1071,6 +1146,21 @@ export interface components {
*/
commentId: string | null;
};
IssueParentDto: {
/** Format: uuid */
id: string;
/** @example WEB-3 */
key: string;
/** @example Pago con tarjeta */
title: string;
};
ChecklistItemDto: {
/** Format: uuid */
id: string;
/** @example Actualizar el changelog */
text: string;
done: boolean;
};
IssueDto: {
/** Format: uuid */
id: string;
@@ -1116,6 +1206,9 @@ export interface components {
attachments: components["schemas"]["AttachmentDto"][];
/** @description People following it */
watchers: components["schemas"]["UserRefDto"][];
/** @description The epic or ticket this one breaks down */
parent: components["schemas"]["IssueParentDto"] | null;
checklist: components["schemas"]["ChecklistItemDto"][];
commentCount: number;
/** Format: date-time */
createdAt: string;
@@ -1266,6 +1359,23 @@ export interface components {
assigneeId?: string | null;
/** Format: uuid */
releaseId?: string | null;
/**
* @description Epic or ticket of the same project the new one breaks down (id or key)
* @example WEB-3
*/
parent?: string | null;
};
ChildProgressDto: {
total: number;
/** @description Done or cancelled */
finished: number;
percent: number;
storyPoints: number;
finishedStoryPoints: number;
};
EpicDto: {
issue: components["schemas"]["IssueDto"];
progress: components["schemas"]["ChildProgressDto"];
};
UpdateIssueDto: {
description?: string | null;
@@ -1308,6 +1418,21 @@ export interface components {
*/
releaseId: string | null;
};
SetIssueParentDto: {
/**
* @description Epic or ticket of the same project (id or key), or null to take it back to the top
* @example WEB-3
*/
parent: string | null;
};
ChecklistItemBodyDto: {
/** @example Actualizar el changelog */
text: string;
};
UpdateChecklistItemDto: {
text?: string;
done?: boolean;
};
LinkIssueDto: {
type: components["schemas"]["IssueLinkType"];
/**
@@ -2890,6 +3015,8 @@ export interface operations {
unassigned?: boolean;
releaseId?: string;
label?: string[];
/** @description Only the tickets hanging from this one (id or key) */
parent?: string;
/** @description Matches key, title or description */
search?: string;
};
@@ -3028,6 +3155,64 @@ export interface operations {
};
};
};
IssuesController_epics: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["EpicDto"][];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_findOne: {
parameters: {
query?: never;
@@ -3406,6 +3591,324 @@ export interface operations {
};
};
};
IssuesController_setParent: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Ticket id or key */
issue: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SetIssueParentDto"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommandResultDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_addChecklistItem: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Ticket id or key */
issue: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ChecklistItemBodyDto"];
};
};
responses: {
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommandResultDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_removeChecklistItem: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Ticket id or key */
issue: string;
item: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommandResultDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_updateChecklistItem: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Ticket id or key */
issue: string;
item: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UpdateChecklistItemDto"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommandResultDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_link: {
parameters: {
query?: never;