feat: an ordered backlog

A Backlog tab lists the tickets still to start in the order the team will
take them; drag them, or use the arrows, to reorder. Moving a ticket only
rewrites that ticket's rank, read from the event store, and board columns
follow the same order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:01:44 +02:00
parent d4da33d4e8
commit 0606f53026
53 changed files with 1599 additions and 32 deletions

View File

@@ -2188,6 +2188,15 @@
"type": "string"
}
},
{
"name": "sort",
"required": false,
"in": "query",
"description": "RECENT (default): last updated first. RANK: backlog order.",
"schema": {
"$ref": "#/components/schemas/IssueSort"
}
},
{
"name": "search",
"required": false,
@@ -3098,6 +3107,118 @@
]
}
},
"/api/issues/{issue}/rank": {
"put": {
"description": "Between the ticket that ends up above it and the one below (either may be left out for the top or the bottom). Board columns follow the same order.",
"operationId": "IssuesController_rank",
"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/RankIssueDto"
}
}
}
},
"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": "Move the ticket in the backlog",
"tags": [
"Issues"
]
}
},
"/api/issues/{issue}/checklist": {
"post": {
"operationId": "IssuesController_addChecklistItem",
@@ -6940,6 +7061,10 @@
}
]
},
"rank": {
"type": "string",
"description": "Place in the backlog and its board column (ascending, as text)"
},
"checklist": {
"type": "array",
"items": {
@@ -6986,6 +7111,7 @@
"attachments",
"watchers",
"parent",
"rank",
"checklist",
"commentCount",
"createdAt",
@@ -7421,6 +7547,13 @@
"total"
]
},
"IssueSort": {
"type": "string",
"enum": [
"RECENT",
"RANK"
]
},
"CreateIssueDto": {
"type": "object",
"properties": {
@@ -7656,6 +7789,23 @@
"parent"
]
},
"RankIssueDto": {
"type": "object",
"properties": {
"above": {
"type": "string",
"nullable": true,
"example": "WEB-4",
"description": "The ticket that ends up right above (id or key; null: the top)"
},
"below": {
"type": "string",
"nullable": true,
"example": "WEB-9",
"description": "The ticket that ends up right below (id or key; null: the bottom)"
}
}
},
"ChecklistItemBodyDto": {
"type": "object",
"properties": {

View File

@@ -242,6 +242,11 @@ type Issue {
priority: IssuePriority!
projectId: ID!
projectKey: String!
"""
Place in the backlog and in its board column (ascending, compare as text)
"""
rank: String!
release: ReleaseRef
reporter: UserRef!
@@ -281,6 +286,7 @@ input IssueFilterInput {
"""Matches key, title or description"""
search: String
sort: IssueSort = RECENT
statuses: [String!]
types: [String!]
unassigned: Boolean
@@ -322,6 +328,12 @@ enum IssuePriority {
MEDIUM
}
"""RECENT: last updated first. RANK: backlog order."""
enum IssueSort {
RANK
RECENT
}
type IssueTypeOption {
archived: Boolean!
color: String!
@@ -400,6 +412,9 @@ type Mutation {
"""
publishRelease(force: Boolean! = false, releaseId: ID!): CommandResult!
"""Move a ticket in the backlog, between two others of the same project"""
rankIssue(input: RankIssueInput!): CommandResult!
"""
Remove a file from a ticket (whoever uploaded it, or an owner/admin). Upload with the REST API.
"""
@@ -641,6 +656,15 @@ type Query {
users(limit: Int! = 20, search: String! = ""): [User!]!
}
input RankIssueInput {
"""The ticket that ends up right above (null: the top)"""
aboveIssueId: ID
"""The ticket that ends up right below (null: the bottom)"""
belowIssueId: ID
issueId: ID!
}
type Release {
"""Scope vs finished tickets, day by day"""
burnup: ReleaseBurnup!