feat: notifications in the app and, optionally, by e-mail
Derived from events and kept per person: whoever a ticket is assigned to, whoever reports or works on it when it moves, fails QA or gets a comment, and whoever joins a project. Never about what you did yourself, once per event. They arrive live through a subscription to a bell in the header, and are copied by e-mail to whoever turns it on when SMTP_URL is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4707,6 +4707,363 @@
|
||||
],
|
||||
"security": []
|
||||
}
|
||||
},
|
||||
"/api/me/notifications": {
|
||||
"get": {
|
||||
"operationId": "NotificationsController_mine",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "unreadOnly",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Only unread ones",
|
||||
"schema": {
|
||||
"default": false,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 30,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MyNotificationsDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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 notifications, newest first",
|
||||
"tags": [
|
||||
"Users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/me/notifications/read": {
|
||||
"post": {
|
||||
"operationId": "NotificationsController_markRead",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MarkNotificationsReadDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Done"
|
||||
},
|
||||
"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": "Mark notifications as read (all of them without `ids`)",
|
||||
"tags": [
|
||||
"Users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/me/notification-settings": {
|
||||
"get": {
|
||||
"operationId": "NotificationsController_settings",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationSettingsResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "Whether I get copies by e-mail",
|
||||
"tags": [
|
||||
"Users"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "NotificationsController_updateSettings",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationSettingsDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationSettingsResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "Turn e-mail copies on or off",
|
||||
"tags": [
|
||||
"Users"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -6714,6 +7071,145 @@
|
||||
"description": "Publish even with unfinished tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Object": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"NotificationKind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ASSIGNED",
|
||||
"STATUS_CHANGED",
|
||||
"QA_FAILED",
|
||||
"COMMENTED",
|
||||
"MENTIONED",
|
||||
"WATCHED",
|
||||
"ADDED_TO_PROJECT",
|
||||
"REMINDER"
|
||||
]
|
||||
},
|
||||
"NotificationDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NotificationKind"
|
||||
}
|
||||
]
|
||||
},
|
||||
"projectId": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"projectKey": {
|
||||
"type": "string",
|
||||
"example": "WEB"
|
||||
},
|
||||
"issueId": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"format": "uuid"
|
||||
},
|
||||
"issueKey": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "WEB-12"
|
||||
},
|
||||
"actor": {
|
||||
"$ref": "#/components/schemas/UserRefDto"
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"example": "Ana te asignó WEB-12: Login roto en Safari"
|
||||
},
|
||||
"readAt": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"format": "date-time",
|
||||
"description": "Null: unread"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"kind",
|
||||
"projectId",
|
||||
"projectKey",
|
||||
"issueId",
|
||||
"issueKey",
|
||||
"actor",
|
||||
"summary",
|
||||
"readAt",
|
||||
"createdAt"
|
||||
]
|
||||
},
|
||||
"MyNotificationsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NotificationDto"
|
||||
}
|
||||
},
|
||||
"unread": {
|
||||
"type": "number",
|
||||
"description": "All my unread ones, not just these"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items",
|
||||
"unread"
|
||||
]
|
||||
},
|
||||
"MarkNotificationsReadDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "Omit to mark all of them",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NotificationSettingsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "boolean",
|
||||
"description": "Copies by e-mail"
|
||||
},
|
||||
"emailAvailable": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this installation can send e-mail"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email",
|
||||
"emailAvailable"
|
||||
]
|
||||
},
|
||||
"NotificationSettingsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "boolean",
|
||||
"description": "Copies by e-mail"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +342,9 @@ type Mutation {
|
||||
"""Relate tickets, e.g. FE DEPENDS_ON BE, QA TESTS FE"""
|
||||
linkIssues(input: LinkIssuesInput!): CommandResult!
|
||||
|
||||
"""Mark these as read, or all of them when `ids` is omitted"""
|
||||
markNotificationsRead(ids: [ID!]): Boolean!
|
||||
|
||||
"""
|
||||
Ship the release. Fails while it has unfinished tickets unless force = true.
|
||||
"""
|
||||
@@ -357,6 +360,7 @@ type Mutation {
|
||||
setIssueRelease(issueId: ID!, releaseId: ID): CommandResult!
|
||||
unlinkIssues(input: LinkIssuesInput!): CommandResult!
|
||||
updateIssue(input: UpdateIssueInput!): CommandResult!
|
||||
updateNotificationSettings(email: Boolean!): NotificationSettings!
|
||||
updateProject(input: UpdateProjectInput!): CommandResult!
|
||||
|
||||
"""
|
||||
@@ -366,6 +370,48 @@ type Mutation {
|
||||
updateRelease(input: UpdateReleaseInput!): CommandResult!
|
||||
}
|
||||
|
||||
type MyNotifications {
|
||||
items: [Notification!]!
|
||||
|
||||
"""All my unread ones, not just these"""
|
||||
unread: Int!
|
||||
}
|
||||
|
||||
type Notification {
|
||||
actor: UserRef!
|
||||
createdAt: String!
|
||||
id: ID!
|
||||
issueId: ID
|
||||
issueKey: String
|
||||
kind: NotificationKind!
|
||||
projectId: ID!
|
||||
projectKey: String!
|
||||
|
||||
"""Null: unread"""
|
||||
readAt: String
|
||||
summary: String!
|
||||
}
|
||||
|
||||
"""Why someone is told"""
|
||||
enum NotificationKind {
|
||||
ADDED_TO_PROJECT
|
||||
ASSIGNED
|
||||
COMMENTED
|
||||
MENTIONED
|
||||
QA_FAILED
|
||||
REMINDER
|
||||
STATUS_CHANGED
|
||||
WATCHED
|
||||
}
|
||||
|
||||
type NotificationSettings {
|
||||
"""Copies by e-mail"""
|
||||
email: Boolean!
|
||||
|
||||
"""Whether this installation can send e-mail at all"""
|
||||
emailAvailable: Boolean!
|
||||
}
|
||||
|
||||
type Project {
|
||||
"""
|
||||
Statistics of the last `days` days (7-365): created vs resolved, cumulative flow, cycle/lead time, throughput and QA
|
||||
@@ -498,8 +544,12 @@ type Query {
|
||||
"""
|
||||
myIssues(assignedToMe: Boolean! = true, categories: [StatusCategory!], limit: Int! = 50, offset: Int! = 0, statuses: [String!]): IssuePage!
|
||||
|
||||
"""My notifications, newest first, and how many are unread"""
|
||||
myNotifications(limit: Int! = 30, unreadOnly: Boolean! = false): MyNotifications!
|
||||
|
||||
"""Projects where I am a member"""
|
||||
myProjects: [Project!]!
|
||||
notificationSettings: NotificationSettings!
|
||||
|
||||
"""A project of mine, or any public one (no sign-in needed)"""
|
||||
project(
|
||||
@@ -603,6 +653,9 @@ type Subscription {
|
||||
|
||||
"""Projects I am added to / removed from"""
|
||||
myProjectsChanged: ProjectChange!
|
||||
|
||||
"""My notifications, as they happen"""
|
||||
notificationAdded: Notification!
|
||||
projectChanged(projectId: ID!): ProjectChange!
|
||||
releaseChanged(projectId: ID!): ReleaseChange!
|
||||
}
|
||||
|
||||
@@ -46,4 +46,10 @@ export const configSchema = Joi.object({
|
||||
// Attached files: where they are kept (a volume in Docker) and how big one can be.
|
||||
ATTACHMENTS_DIR: Joi.string().default('data/attachments'),
|
||||
ATTACHMENTS_MAX_MB: Joi.number().min(1).max(500).default(25),
|
||||
|
||||
// E-mail copies of notifications (optional): without SMTP_URL nothing is sent.
|
||||
SMTP_URL: Joi.string().allow('').optional(),
|
||||
MAIL_FROM: Joi.string().allow('').optional(),
|
||||
// Public address of the web app, for links in e-mails.
|
||||
WEB_URL: Joi.string().allow('').optional(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user