feat: teams within a project

Projects keep teams of members in their settings (settings.teams; people
outside the project are dropped, teams are archived, not removed; absent
when saving means unchanged). Tickets go to a team (`team`, also filtered
in the list and in MCP search_issues), and a comment mentioning
@[Frontend](team-FRONT) tells each of its members. Settings has the teams
with their members, the ticket a «Equipo» select, the list a team filter,
and «@» offers the teams after the people.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 16:20:35 +02:00
parent 4acf629106
commit 869acd3f1f
41 changed files with 895 additions and 13 deletions

View File

@@ -86,6 +86,7 @@ const FIELD: Record<string, string> = {
description: 'la descripción',
type: 'el tipo',
area: 'el área',
team: 'el equipo',
priority: 'la prioridad',
labels: 'las etiquetas',
figmaUrl: 'el enlace de Figma',

View File

@@ -41,6 +41,7 @@ export class IssueCreatedEventHandler
key: issue.key,
type: issue.type,
area: issue.area,
team: issue.team ?? null,
title: issue.title,
description: issue.description,
status: issue.status,

View File

@@ -350,6 +350,7 @@ export class IssuesService {
title: `${issue.title} (copia)`.slice(0, 255),
description: issue.description,
area: issue.area,
team: issue.team,
priority: issue.priority,
labels: issue.labels,
figmaUrl: issue.figmaUrl,

View File

@@ -8,6 +8,9 @@ import {
IssueStatusChangedEvent,
IssueUpdatedEvent,
mentionedUserIds,
DEFAULT_PROJECT_SETTINGS,
ProjectWorkflow,
TEAM_MENTION_PREFIX,
NotificationKind,
ProjectMemberAddedEvent,
ProjectRole,
@@ -195,7 +198,12 @@ export class NotificationsEventHandler implements IEventHandler<TrackerEvent> {
if (event instanceof CommentAddedEvent) {
const said = `«${excerpt(event.comment.body)}»`;
// Only members: a mention cannot reach someone who cannot read the ticket.
for (const userId of mentionedUserIds(event.comment.body)) {
// A team's mention (`team-FRONTEND`) reaches each of its members.
const workflow = new ProjectWorkflow(
project.settings ?? DEFAULT_PROJECT_SETTINGS,
);
const named = mentionedUserIds(event.comment.body);
for (const userId of named) {
if (!memberIds.has(userId)) continue;
drafts.push({
userId,
@@ -203,6 +211,19 @@ export class NotificationsEventHandler implements IEventHandler<TrackerEvent> {
summary: `${actor} te mencionó en ${issue.key}: ${said}`,
});
}
for (const id of named.filter((n) => n.startsWith(TEAM_MENTION_PREFIX))) {
const team = workflow.teams.find(
(t) => t.id === id.slice(TEAM_MENTION_PREFIX.length),
);
for (const userId of workflow.mentionedPeople([id])) {
if (!memberIds.has(userId)) continue;
drafts.push({
userId,
kind: NotificationKind.MENTIONED,
summary: `${actor} mencionó a tu equipo ${team?.name ?? ''} en ${issue.key}: ${said}`,
});
}
}
const answered = event.comment.replyTo
? await this.findComments.findById(event.comment.replyTo)
: null;

View File

@@ -20,6 +20,8 @@ export interface IssueFilter {
categories?: StatusCategory[];
types?: string[];
areas?: string[];
/** Team ids of the project's settings. */
teams?: string[];
priorities?: IssuePriority[];
assigneeId?: string;
unassigned?: boolean;