feat: saved board views and richer board filters

The board now filters by text, type, priority and unassigned tickets as
well. Any combination can be saved as a named view, private or shared with
the project, and opened again with one click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:14:38 +02:00
parent 0606f53026
commit 298383f19f
46 changed files with 2922 additions and 26 deletions

View File

@@ -1007,6 +1007,58 @@
"type": "string"
}
},
{
"name": "type",
"required": false,
"in": "query",
"description": "Ticket type ids",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "priority",
"required": false,
"in": "query",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/IssuePriority"
}
}
},
{
"name": "label",
"required": false,
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "search",
"required": false,
"in": "query",
"description": "Matches key, title or description",
"schema": {
"type": "string"
}
},
{
"name": "unassigned",
"required": false,
"in": "query",
"description": "Only tickets without assignee",
"schema": {
"type": "boolean"
}
},
{
"name": "perColumn",
"required": false,
@@ -5933,6 +5985,293 @@
"Users"
]
}
},
"/api/projects/{project}/filters": {
"get": {
"operationId": "SavedFiltersController_list",
"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/SavedFilterDto"
}
}
}
}
},
"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": "My views of the project's board, then the shared ones",
"tags": [
"Saved filters"
]
},
"post": {
"description": "Project members only. `shared` makes it visible to the whole project (not for viewers).",
"operationId": "SavedFiltersController_save",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SaveFilterDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SavedFilterIdDto"
}
}
}
},
"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": "Save board filters under a name",
"tags": [
"Saved filters"
]
}
},
"/api/filters/{id}": {
"delete": {
"operationId": "SavedFiltersController_delete",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
},
"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": "Delete a saved view (whoever saved it, or an admin if shared)",
"tags": [
"Saved filters"
]
}
}
},
"info": {
@@ -8267,6 +8606,174 @@
"required": [
"email"
]
},
"SavedFilterCriteriaResponseDto": {
"type": "object",
"properties": {
"types": {
"type": "array",
"items": {
"type": "string"
}
},
"areas": {
"type": "array",
"items": {
"type": "string"
}
},
"priorities": {
"type": "array",
"items": {
"$ref": "#/components/schemas/IssuePriority"
}
},
"assigneeId": {
"type": "string",
"nullable": true
},
"unassigned": {
"type": "boolean"
},
"releaseId": {
"type": "string",
"nullable": true,
"format": "uuid"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"search": {
"type": "string",
"nullable": true
}
}
},
"SavedFilterDto": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"projectId": {
"type": "string",
"format": "uuid"
},
"owner": {
"$ref": "#/components/schemas/UserRefDto"
},
"name": {
"type": "string",
"example": "Bugs críticos de frontend"
},
"shared": {
"type": "boolean",
"description": "Visible to the whole project"
},
"criteria": {
"$ref": "#/components/schemas/SavedFilterCriteriaResponseDto"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"owner",
"name",
"shared",
"criteria",
"createdAt"
]
},
"SavedFilterCriteriaDto": {
"type": "object",
"properties": {
"types": {
"description": "Ticket type ids",
"type": "array",
"items": {
"type": "string"
}
},
"areas": {
"description": "Area ids",
"type": "array",
"items": {
"type": "string"
}
},
"priorities": {
"type": "array",
"items": {
"$ref": "#/components/schemas/IssuePriority"
}
},
"assigneeId": {
"type": "string",
"nullable": true,
"description": "Pireon user id"
},
"unassigned": {
"type": "boolean"
},
"releaseId": {
"type": "string",
"nullable": true,
"format": "uuid"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"search": {
"type": "string",
"nullable": true,
"maxLength": 200
}
}
},
"SaveFilterDto": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Bugs críticos de frontend",
"maxLength": 60
},
"shared": {
"type": "object",
"default": false,
"description": "Visible to the whole project (members, not viewers, share)"
},
"criteria": {
"$ref": "#/components/schemas/SavedFilterCriteriaDto"
}
},
"required": [
"name",
"criteria"
]
},
"SavedFilterIdDto": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
}
},
"required": [
"id"
]
}
}
}

View File

@@ -399,6 +399,9 @@ type Mutation {
Delete the project (owner only), with its tickets, comments and releases. `confirmKey` must be the project key.
"""
deleteProject(confirmKey: String!, projectId: ID!): CommandResult!
"""Delete a view: whoever saved it, or an admin if it is shared"""
deleteSavedFilter(id: ID!): Boolean!
editComment(body: String!, commentId: ID!): CommandResult!
"""Relate tickets, e.g. FE DEPENDS_ON BE, QA TESTS FE"""
@@ -422,6 +425,9 @@ type Mutation {
removeChecklistItem(issueId: ID!, itemId: ID!): CommandResult!
removeProjectMember(projectId: ID!, userId: ID!): CommandResult!
"""Save the current board filters under a name (members of the project)"""
saveFilter(input: SaveFilterInput!): ID!
"""
Place a ticket under an epic or another ticket of the same project (null: back to the top). Three levels at most: epic → ticket → subtask.
"""
@@ -501,7 +507,19 @@ type Project {
analytics(days: Int! = 90): ProjectAnalytics!
"""Kanban board, one column per status"""
board(area: String, assigneeId: ID, perColumn: Int! = 50, releaseId: ID): [BoardColumn!]!
board(
area: String
assigneeId: ID
labels: [String!]
perColumn: Int! = 50
priorities: [IssuePriority!]
releaseId: ID
"""Matches key, title or description"""
search: String
types: [String!]
unassigned: Boolean
): [BoardColumn!]!
createdAt: String!
description: String
id: ID!
@@ -648,6 +666,9 @@ type Query {
publicProjects: [Project!]!
release(id: ID!): Release!
"""My views of the project's board, then the shared ones"""
savedFilters(projectId: ID!): [SavedFilter!]!
"""Planned releases of all my projects, soonest target date first"""
upcomingReleases(limit: Int! = 10): [Release!]!
user(id: String!): User
@@ -718,6 +739,51 @@ enum ReleaseStatus {
RELEASED
}
input SaveFilterInput {
criteria: SavedFilterCriteriaInput!
name: String!
projectId: ID!
"""Visible to the whole project (members, not viewers, share)"""
shared: Boolean! = false
}
"""
A named view of the board: private to whoever saved it, or shared with the project
"""
type SavedFilter {
createdAt: String!
criteria: SavedFilterCriteria!
id: ID!
name: String!
owner: UserRef!
projectId: ID!
shared: Boolean!
}
"""What a saved view narrows the board to; empty: everything"""
type SavedFilterCriteria {
areas: [String!]
assigneeId: ID
labels: [String!]
priorities: [IssuePriority!]
releaseId: ID
search: String
types: [String!]
unassigned: Boolean
}
input SavedFilterCriteriaInput {
areas: [String!]
assigneeId: ID
labels: [String!]
priorities: [IssuePriority!]
releaseId: ID
search: String
types: [String!]
unassigned: Boolean
}
"""
What a status means, whatever each project calls it. DONE and CANCELLED are finished.
"""