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:
@@ -1641,4 +1641,63 @@ describe('task-stream (e2e)', () => {
|
||||
).status,
|
||||
).toBe(200);
|
||||
});
|
||||
|
||||
it('groups members in teams: tickets go to them and @team tells them', async () => {
|
||||
const key = 'T' + Date.now().toString(36).slice(-5).toUpperCase();
|
||||
await rest('token-ana', 'POST', '/projects', { key, name: key });
|
||||
const project = await eventually(
|
||||
() => rest('token-ana', 'GET', `/projects/${key}`),
|
||||
(r) => r.status === 200,
|
||||
);
|
||||
await rest('token-ana', 'POST', `/projects/${key}/members`, {
|
||||
userId: '102',
|
||||
});
|
||||
expect(
|
||||
(
|
||||
await rest('token-ana', 'PUT', `/projects/${key}/settings`, {
|
||||
...project.body['settings'],
|
||||
teams: [
|
||||
{ id: 'FRONT', name: 'Frontend', memberIds: ['102', 'nobody'] },
|
||||
],
|
||||
})
|
||||
).status,
|
||||
).toBe(200);
|
||||
const settings = await eventually(
|
||||
() => rest('token-ana', 'GET', `/projects/${key}`),
|
||||
(r) => r.body['settings'].teams.length === 1,
|
||||
);
|
||||
expect(settings.body['settings'].teams[0].memberIds).toEqual(['102']);
|
||||
|
||||
const created = await rest('token-ana', 'POST', `/projects/${key}/issues`, {
|
||||
type: 'TASK',
|
||||
title: 'Para el equipo',
|
||||
team: 'FRONT',
|
||||
});
|
||||
const ticket = created.body['key'];
|
||||
await rest('token-ana', 'POST', `/projects/${key}/issues`, {
|
||||
type: 'TASK',
|
||||
title: 'Sin equipo',
|
||||
});
|
||||
const mine = await eventually(
|
||||
() => rest('token-ana', 'GET', `/projects/${key}/issues?team=FRONT`),
|
||||
(r) => r.body?.['total'] === 1,
|
||||
);
|
||||
expect(mine.body['items'][0]).toMatchObject({ key: ticket, team: 'FRONT' });
|
||||
|
||||
await rest('token-ana', 'POST', `/issues/${ticket}/comments`, {
|
||||
body: '¿Lo miráis, @[Frontend](team-FRONT)?',
|
||||
});
|
||||
const told = await eventually(
|
||||
() => rest('token-bob', 'GET', '/me/notifications'),
|
||||
(r) =>
|
||||
r.body?.items?.some(
|
||||
(n: { kind: string; issueKey: string }) =>
|
||||
n.kind === 'MENTIONED' && n.issueKey === ticket,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
told.body.items.find((n: { kind: string }) => n.kind === 'MENTIONED')
|
||||
.summary,
|
||||
).toMatch(/tu equipo Frontend/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2654,6 +2654,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "team",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Team ids",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "priority",
|
||||
"required": false,
|
||||
@@ -11152,6 +11164,34 @@
|
||||
"archived"
|
||||
]
|
||||
},
|
||||
"TeamDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "FRONTEND"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Frontend"
|
||||
},
|
||||
"memberIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"archived": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"memberIds",
|
||||
"archived"
|
||||
]
|
||||
},
|
||||
"ProjectSettingsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11189,6 +11229,12 @@
|
||||
"checklistBeforeDone": {
|
||||
"type": "boolean",
|
||||
"description": "Definition of done: tickets reach DONE only with their checklist ticked"
|
||||
},
|
||||
"teams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TeamDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11197,7 +11243,8 @@
|
||||
"issueTypes",
|
||||
"areas",
|
||||
"customFields",
|
||||
"checklistBeforeDone"
|
||||
"checklistBeforeDone",
|
||||
"teams"
|
||||
]
|
||||
},
|
||||
"ProjectDto": {
|
||||
@@ -11560,6 +11607,35 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"TeamBodyDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "FRONTEND"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Frontend",
|
||||
"maxLength": 40
|
||||
},
|
||||
"memberIds": {
|
||||
"description": "Pireon user ids of project members (others are dropped)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"ProjectSettingsBodyDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11595,6 +11671,13 @@
|
||||
"$ref": "#/components/schemas/CustomFieldDefinitionBodyDto"
|
||||
}
|
||||
},
|
||||
"teams": {
|
||||
"description": "Absent: unchanged. Teams cannot be removed: archive them.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TeamBodyDto"
|
||||
}
|
||||
},
|
||||
"checklistBeforeDone": {
|
||||
"type": "boolean",
|
||||
"description": "Definition of done: tickets reach a DONE status only with their checklist ticked (owners and admins may override). Absent: unchanged"
|
||||
@@ -11899,6 +11982,11 @@
|
||||
"nullable": true,
|
||||
"example": "FRONTEND"
|
||||
},
|
||||
"team": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "A team id"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -12088,6 +12176,7 @@
|
||||
"key",
|
||||
"type",
|
||||
"area",
|
||||
"team",
|
||||
"title",
|
||||
"description",
|
||||
"status",
|
||||
@@ -13199,6 +13288,12 @@
|
||||
"example": "FRONTEND",
|
||||
"description": "An area id of the project's settings"
|
||||
},
|
||||
"team": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "FRONTEND",
|
||||
"description": "A team id of the project's settings"
|
||||
},
|
||||
"priority": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -13337,6 +13432,12 @@
|
||||
"example": "FRONTEND",
|
||||
"description": "An area id of the project's settings"
|
||||
},
|
||||
"team": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "FRONTEND",
|
||||
"description": "A team id of the project's settings"
|
||||
},
|
||||
"priority": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -339,6 +339,9 @@ input CreateIssueInput {
|
||||
"""When work should start (roadmap), YYYY-MM-DD"""
|
||||
startDate: String
|
||||
storyPoints: Int
|
||||
|
||||
"""A team id"""
|
||||
team: String
|
||||
title: String!
|
||||
|
||||
"""A ticket type id"""
|
||||
@@ -607,6 +610,9 @@ type Issue {
|
||||
status: String!
|
||||
statusCategory: StatusCategory!
|
||||
storyPoints: Int
|
||||
|
||||
"""A team id"""
|
||||
team: String
|
||||
title: String!
|
||||
|
||||
"""In the trash since then; null when it is not"""
|
||||
@@ -669,6 +675,9 @@ input IssueFilterInput {
|
||||
sort: IssueSort = RECENT
|
||||
sprintId: ID
|
||||
statuses: [String!]
|
||||
|
||||
"""Team ids"""
|
||||
teams: [String!]
|
||||
types: [String!]
|
||||
unassigned: Boolean
|
||||
|
||||
@@ -1204,6 +1213,7 @@ type ProjectSettings {
|
||||
customFields: [CustomFieldDefinition!]!
|
||||
issueTypes: [IssueTypeOption!]!
|
||||
statuses: [WorkflowStatus!]!
|
||||
teams: [Team!]!
|
||||
transitions: [StatusTransitions!]!
|
||||
}
|
||||
|
||||
@@ -1222,6 +1232,9 @@ input ProjectSettingsInput {
|
||||
customFields: [CustomFieldDefinitionInput!]
|
||||
issueTypes: [IssueTypeOptionInput!]!
|
||||
statuses: [WorkflowStatusInput!]!
|
||||
|
||||
"""Absent: unchanged. Teams cannot be removed: archive them."""
|
||||
teams: [TeamInput!]
|
||||
transitions: [StatusTransitionsInput!]!
|
||||
}
|
||||
|
||||
@@ -1654,6 +1667,21 @@ type Subscription {
|
||||
sprintChanged(projectId: ID!): SprintChange!
|
||||
}
|
||||
|
||||
"""A group of members: tickets go to it; @team mentions tell them all"""
|
||||
type Team {
|
||||
archived: Boolean!
|
||||
id: String!
|
||||
memberIds: [ID!]!
|
||||
name: String!
|
||||
}
|
||||
|
||||
input TeamInput {
|
||||
archived: Boolean! = false
|
||||
id: String!
|
||||
memberIds: [ID!]! = []
|
||||
name: String!
|
||||
}
|
||||
|
||||
type TeamLoad {
|
||||
blocked: Int!
|
||||
openPoints: Int!
|
||||
@@ -1696,6 +1724,9 @@ input UpdateIssueInput {
|
||||
"""When work should start (roadmap), YYYY-MM-DD"""
|
||||
startDate: String
|
||||
storyPoints: Int
|
||||
|
||||
"""A team id"""
|
||||
team: String
|
||||
title: String
|
||||
type: String
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user