feat: tickets I follow, and more news for followers

"Que sigo" on the home page lists the open tickets I follow, whoever
works on them; `watching` filters my issues (REST, GraphQL, MCP) and a
project's search (none without a session).

Followers now also hear when a ticket's priority or due date changes,
and when it enters or leaves a sprint. Smaller edits (title, labels…)
still do not notify.

Docs (with the REPLIED row the replies change missed), Word and PDF
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 11:29:17 +02:00
parent ea4ef82239
commit f6417248c7
29 changed files with 636 additions and 11573 deletions

View File

@@ -1,7 +1,10 @@
import {
CommentAddedEvent,
IssueAssignedEvent,
IssuePriority,
IssueSprintChangedEvent,
IssueStatusChangedEvent,
IssueUpdatedEvent,
NotificationKind,
NotificationReadModel,
NotificationSettingsReadModel,
@@ -14,6 +17,7 @@ import { FakeUserDirectory } from '../../testing/in-memory';
import { ChangeFeed } from '../ports/change-feed';
import { FindCommentsRepository } from '../ports/find-comments.repository';
import { FindIssuesRepository } from '../ports/find-issues.repository';
import { FindSprintsRepository } from '../ports/find-sprints.repository';
import { FindProjectsRepository } from '../ports/find-projects.repository';
import { NotificationMailer } from '../ports/notification-mailer';
import { NotificationsRepository } from '../ports/notifications.repository';
@@ -108,6 +112,10 @@ function setup(mailEnabled = false) {
? { id, author: { id: 'pm', name: 'PM' }, deleted: false }
: null,
} as unknown as FindCommentsRepository,
{
findById: async (id: string) =>
id === 's-14' ? { id, name: 'Sprint 14' } : null,
} as unknown as FindSprintsRepository,
);
return { repo, published, mailed, handler };
}
@@ -236,6 +244,60 @@ describe('notifications', () => {
expect(repo.all[0].summary).toContain('respondió a tu comentario');
});
it('tells followers of a new priority, due date or sprint, not of smaller edits', async () => {
const { handler, repo } = setup();
await handler.handle(
at(
new IssueUpdatedEvent('ana', 'i-1', 'p-1', {
priority: IssuePriority.HIGH,
dueDate: '2026-11-14',
}),
),
);
await handler.handle(
at(new IssueUpdatedEvent('ana', 'i-1', 'p-1', { title: 'Login' }), 3),
);
await handler.handle(
at(new IssueSprintChangedEvent('ana', 'i-1', 'p-1', 's-14', null), 4),
);
await handler.handle(
at(new IssueSprintChangedEvent('ana', 'i-1', 'p-1', null, 's-14'), 5),
);
expect(repo.all.map((n) => [n.userId, n.kind, n.summary])).toEqual([
[
'pm',
NotificationKind.WATCHED,
'user ana cambió WEB-1: Login roto: prioridad Alta, fecha límite el 14 de noviembre de 2026',
],
[
'bob',
NotificationKind.WATCHED,
'user ana cambió WEB-1: Login roto: prioridad Alta, fecha límite el 14 de noviembre de 2026',
],
[
'pm',
NotificationKind.WATCHED,
'user ana metió WEB-1: Login roto en el sprint Sprint 14',
],
[
'bob',
NotificationKind.WATCHED,
'user ana metió WEB-1: Login roto en el sprint Sprint 14',
],
[
'pm',
NotificationKind.WATCHED,
'user ana sacó WEB-1: Login roto del sprint y lo devolvió al backlog',
],
[
'bob',
NotificationKind.WATCHED,
'user ana sacó WEB-1: Login roto del sprint y lo devolvió al backlog',
],
]);
});
it('copies by e-mail only to whoever asked for it, when mail is configured', async () => {
const { handler, repo, mailed } = setup(true);
repo.emailOn.add('bob');