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

@@ -2178,6 +2178,16 @@
}
}
},
{
"name": "parent",
"required": false,
"in": "query",
"description": "Only the tickets hanging from this one (id or key)",
"schema": {
"example": "WEB-3",
"type": "string"
}
},
{
"name": "search",
"required": false,
@@ -2365,6 +2375,92 @@
]
}
},
"/api/projects/{project}/epics": {
"get": {
"description": "With the progress of the tickets hanging from each one (done or cancelled, and story points).",
"operationId": "IssuesController_epics",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/EpicDto"
}
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
},
{}
],
"summary": "Epics: top-level tickets that group others",
"tags": [
"Issues"
]
}
},
"/api/issues/{issue}": {
"get": {
"operationId": "IssuesController_findOne",
@@ -2890,6 +2986,457 @@
]
}
},
"/api/issues/{issue}/parent": {
"put": {
"description": "Same project, three levels at most (epic → ticket → subtask), no loops. null takes it back to the top.",
"operationId": "IssuesController_setParent",
"parameters": [
{
"name": "issue",
"required": true,
"in": "path",
"description": "Ticket id or key",
"schema": {
"example": "WEB-12",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SetIssueParentDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResultDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Place the ticket under an epic or another ticket",
"tags": [
"Issues"
]
}
},
"/api/issues/{issue}/checklist": {
"post": {
"operationId": "IssuesController_addChecklistItem",
"parameters": [
{
"name": "issue",
"required": true,
"in": "path",
"description": "Ticket id or key",
"schema": {
"example": "WEB-12",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChecklistItemBodyDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResultDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Add a step to the ticket's checklist",
"tags": [
"Issues"
]
}
},
"/api/issues/{issue}/checklist/{item}": {
"patch": {
"operationId": "IssuesController_updateChecklistItem",
"parameters": [
{
"name": "issue",
"required": true,
"in": "path",
"description": "Ticket id or key",
"schema": {
"example": "WEB-12",
"type": "string"
}
},
{
"name": "item",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateChecklistItemDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResultDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Reword, tick or untick a checklist step",
"tags": [
"Issues"
]
},
"delete": {
"operationId": "IssuesController_removeChecklistItem",
"parameters": [
{
"name": "issue",
"required": true,
"in": "path",
"description": "Ticket id or key",
"schema": {
"example": "WEB-12",
"type": "string"
}
},
{
"name": "item",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResultDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Remove a checklist step",
"tags": [
"Issues"
]
}
},
"/api/issues/{issue}/links": {
"post": {
"description": "e.g. WEB-3 DEPENDS_ON API-7, WEB-9 TESTS WEB-3. May cross projects.",
@@ -6213,6 +6760,49 @@
"commentId"
]
},
"IssueParentDto": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"key": {
"type": "string",
"example": "WEB-3"
},
"title": {
"type": "string",
"example": "Pago con tarjeta"
}
},
"required": [
"id",
"key",
"title"
]
},
"ChecklistItemDto": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"text": {
"type": "string",
"example": "Actualizar el changelog"
},
"done": {
"type": "boolean"
}
},
"required": [
"id",
"text",
"done"
]
},
"IssueDto": {
"type": "object",
"properties": {
@@ -6340,6 +6930,22 @@
"$ref": "#/components/schemas/UserRefDto"
}
},
"parent": {
"nullable": true,
"description": "The epic or ticket this one breaks down",
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/IssueParentDto"
}
]
},
"checklist": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ChecklistItemDto"
}
},
"commentCount": {
"type": "number"
},
@@ -6379,6 +6985,8 @@
"links",
"attachments",
"watchers",
"parent",
"checklist",
"commentCount",
"createdAt",
"updatedAt",
@@ -6876,6 +7484,12 @@
"type": "string",
"nullable": true,
"format": "uuid"
},
"parent": {
"type": "string",
"nullable": true,
"example": "WEB-3",
"description": "Epic or ticket of the same project the new one breaks down (id or key)"
}
},
"required": [
@@ -6883,6 +7497,51 @@
"title"
]
},
"ChildProgressDto": {
"type": "object",
"properties": {
"total": {
"type": "number"
},
"finished": {
"type": "number",
"description": "Done or cancelled"
},
"percent": {
"type": "number",
"minimum": 0,
"maximum": 100
},
"storyPoints": {
"type": "number"
},
"finishedStoryPoints": {
"type": "number"
}
},
"required": [
"total",
"finished",
"percent",
"storyPoints",
"finishedStoryPoints"
]
},
"EpicDto": {
"type": "object",
"properties": {
"issue": {
"$ref": "#/components/schemas/IssueDto"
},
"progress": {
"$ref": "#/components/schemas/ChildProgressDto"
}
},
"required": [
"issue",
"progress"
]
},
"UpdateIssueDto": {
"type": "object",
"properties": {
@@ -6983,6 +7642,45 @@
"releaseId"
]
},
"SetIssueParentDto": {
"type": "object",
"properties": {
"parent": {
"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"
}
},
"required": [
"parent"
]
},
"ChecklistItemBodyDto": {
"type": "object",
"properties": {
"text": {
"type": "string",
"example": "Actualizar el changelog",
"maxLength": 300
}
},
"required": [
"text"
]
},
"UpdateChecklistItemDto": {
"type": "object",
"properties": {
"text": {
"type": "string",
"maxLength": 300
},
"done": {
"type": "boolean"
}
}
},
"LinkIssueDto": {
"type": "object",
"properties": {