feat: sprints with burndown and carry-over

Plan sprints with dates and a goal, run one at a time per project, and
put tickets in them from the ticket page. The Sprints tab follows the one
under way with its progress and a burndown in tickets or points against
the ideal line. Closing a sprint sends what was left to the next one or
back to the backlog; the board can be filtered by sprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:59:01 +02:00
parent d2e31789a7
commit 50d3ffd1dd
104 changed files with 7209 additions and 33 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -176,6 +176,17 @@ input CreateReleaseInput {
targetDate: String
}
input CreateSprintInput {
"""YYYY-MM-DD, included"""
endDate: String!
goal: String
name: String!
projectId: ID!
"""YYYY-MM-DD"""
startDate: String!
}
type CreatedResolved {
created: Int!
resolved: Int!
@@ -249,6 +260,7 @@ type Issue {
rank: String!
release: ReleaseRef
reporter: UserRef!
sprint: SprintRef
"""A status id of the project's settings"""
status: String!
@@ -278,6 +290,9 @@ input IssueFilterInput {
categories: [StatusCategory!]
labels: [String!]
"""Only tickets in no sprint"""
noSprint: Boolean
"""Only the tickets hanging from this one"""
parentId: ID
priorities: [IssuePriority!]
@@ -287,6 +302,7 @@ input IssueFilterInput {
"""Matches key, title or description"""
search: String
sort: IssueSort = RECENT
sprintId: ID
statuses: [String!]
types: [String!]
unassigned: Boolean
@@ -388,10 +404,18 @@ type Mutation {
"""Move a ticket through the workflow (incl. QA)"""
changeIssueStatus(input: ChangeIssueStatusInput!): CommandResult!
changeProjectMemberRole(input: ProjectMemberInput!): CommandResult!
"""
Close the sprint under way. Unfinished tickets move to carryOverToSprintId, or back to the backlog when it is null.
"""
closeSprint(carryOverToSprintId: ID, sprintId: ID!): CommandResult!
createIssue(input: CreateIssueInput!): CommandResult!
createProject(input: CreateProjectInput!): CommandResult!
createRelease(input: CreateReleaseInput!): CommandResult!
"""Plan a sprint (owners and admins)"""
createSprint(input: CreateSprintInput!): CommandResult!
"""Author or project admin"""
deleteComment(commentId: ID!): CommandResult!
@@ -402,6 +426,9 @@ type Mutation {
"""Delete a view: whoever saved it, or an admin if it is shared"""
deleteSavedFilter(id: ID!): Boolean!
"""Delete a sprint that never started; its tickets go back to the backlog"""
deleteSprint(sprintId: ID!): CommandResult!
editComment(body: String!, commentId: ID!): CommandResult!
"""Relate tickets, e.g. FE DEPENDS_ON BE, QA TESTS FE"""
@@ -435,6 +462,12 @@ type Mutation {
"""Put a ticket in a release (null removes it)"""
setIssueRelease(issueId: ID!, releaseId: ID): CommandResult!
"""Put a ticket in a sprint (null: back to the backlog)"""
setIssueSprint(issueId: ID!, sprintId: ID): CommandResult!
"""Start a planned sprint (only one under way per project)"""
startSprint(sprintId: ID!): CommandResult!
unlinkIssues(input: LinkIssuesInput!): CommandResult!
"""Stop following a ticket"""
@@ -451,6 +484,7 @@ type Mutation {
"""
updateProjectSettings(projectId: ID!, settings: ProjectSettingsInput!): CommandResult!
updateRelease(input: UpdateReleaseInput!): CommandResult!
updateSprint(input: UpdateSprintInput!): CommandResult!
"""
Follow a ticket: be told when it moves, is reassigned or gets a comment
@@ -517,6 +551,7 @@ type Project {
"""Matches key, title or description"""
search: String
sprintId: ID
types: [String!]
unassigned: Boolean
): [BoardColumn!]!
@@ -668,6 +703,12 @@ type Query {
"""My views of the project's board, then the shared ones"""
savedFilters(projectId: ID!): [SavedFilter!]!
sprint(id: ID!): Sprint!
"""
A project's sprints: the one under way, the planned ones, then the closed ones
"""
sprints(projectId: ID!): [Sprint!]!
"""Planned releases of all my projects, soonest target date first"""
upcomingReleases(limit: Int! = 10): [Release!]!
@@ -784,6 +825,73 @@ input SavedFilterCriteriaInput {
unassigned: Boolean
}
"""A time-boxed iteration: the tickets the team commits to"""
type Sprint {
burndown: SprintBurndown!
closedAt: String
createdAt: String!
"""Last day (included), YYYY-MM-DD"""
endDate: String!
goal: String
id: ID!
"""Tickets in this sprint, in backlog order"""
issues(limit: Int! = 100, offset: Int! = 0): IssuePage!
name: String!
progress: SprintProgress!
projectId: ID!
"""First day, YYYY-MM-DD"""
startDate: String!
startedAt: String
status: SprintStatus!
version: Int!
}
"""
Per day of the sprint: tickets in it, still open, and their points (null: days to come). `ideal` falls from the first day to zero.
"""
type SprintBurndown {
dates: [String!]!
ideal: [Float!]!
remaining: [Int]!
remainingPoints: [Int]!
scope: [Int]!
sprintId: ID!
}
type SprintChange {
action: String!
actorId: ID!
"""Null: deleted"""
sprint: Sprint
sprintId: ID!
version: Int!
}
type SprintProgress {
"""Done or cancelled"""
finished: Int!
finishedStoryPoints: Int!
percent: Int!
storyPoints: Int!
total: Int!
}
type SprintRef {
id: ID!
name: String!
}
"""PLANNED, ACTIVE (one per project at a time) or CLOSED"""
enum SprintStatus {
ACTIVE
CLOSED
PLANNED
}
"""
What a status means, whatever each project calls it. DONE and CANCELLED are finished.
"""
@@ -820,6 +928,7 @@ type Subscription {
notificationAdded: Notification!
projectChanged(projectId: ID!): ProjectChange!
releaseChanged(projectId: ID!): ReleaseChange!
sprintChanged(projectId: ID!): SprintChange!
}
type Throughput {
@@ -865,6 +974,14 @@ input UpdateReleaseInput {
targetDate: String
}
input UpdateSprintInput {
endDate: String
goal: String
name: String
sprintId: ID!
startDate: String
}
"""User managed by the Pireon identity server"""
type User {
email: String!