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

@@ -475,6 +475,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/issues/{issue}/rank": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/**
* Move the ticket in the backlog
* @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.
*/
put: operations["IssuesController_rank"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/issues/{issue}/checklist": {
parameters: {
query?: never;
@@ -1208,6 +1228,8 @@ export interface components {
watchers: components["schemas"]["UserRefDto"][];
/** @description The epic or ticket this one breaks down */
parent: components["schemas"]["IssueParentDto"] | null;
/** @description Place in the backlog and its board column (ascending, as text) */
rank: string;
checklist: components["schemas"]["ChecklistItemDto"][];
commentCount: number;
/** Format: date-time */
@@ -1334,6 +1356,8 @@ export interface components {
/** @description Total matches (for pagination) */
total: number;
};
/** @enum {string} */
IssueSort: "RECENT" | "RANK";
CreateIssueDto: {
description?: string | null;
/**
@@ -1425,6 +1449,18 @@ export interface components {
*/
parent: string | null;
};
RankIssueDto: {
/**
* @description The ticket that ends up right above (id or key; null: the top)
* @example WEB-4
*/
above?: string | null;
/**
* @description The ticket that ends up right below (id or key; null: the bottom)
* @example WEB-9
*/
below?: string | null;
};
ChecklistItemBodyDto: {
/** @example Actualizar el changelog */
text: string;
@@ -3017,6 +3053,8 @@ export interface operations {
label?: string[];
/** @description Only the tickets hanging from this one (id or key) */
parent?: string;
/** @description RECENT (default): last updated first. RANK: backlog order. */
sort?: components["schemas"]["IssueSort"];
/** @description Matches key, title or description */
search?: string;
};
@@ -3671,6 +3709,86 @@ export interface operations {
};
};
};
IssuesController_rank: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Ticket id or key */
issue: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["RankIssueDto"];
};
};
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;