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
|
||||
}
|
||||
|
||||
@@ -1777,6 +1777,14 @@ export interface components {
|
||||
options: string[];
|
||||
archived: boolean;
|
||||
};
|
||||
TeamDto: {
|
||||
/** @example FRONTEND */
|
||||
id: string;
|
||||
/** @example Frontend */
|
||||
name: string;
|
||||
memberIds: string[];
|
||||
archived: boolean;
|
||||
};
|
||||
ProjectSettingsDto: {
|
||||
/** @description In board order */
|
||||
statuses: components["schemas"]["WorkflowStatusDto"][];
|
||||
@@ -1786,6 +1794,7 @@ export interface components {
|
||||
customFields: components["schemas"]["CustomFieldDefinitionDto"][];
|
||||
/** @description Definition of done: tickets reach DONE only with their checklist ticked */
|
||||
checklistBeforeDone: boolean;
|
||||
teams: components["schemas"]["TeamDto"][];
|
||||
};
|
||||
ProjectDto: {
|
||||
/** Format: uuid */
|
||||
@@ -1931,6 +1940,16 @@ export interface components {
|
||||
/** @default false */
|
||||
archived: boolean;
|
||||
};
|
||||
TeamBodyDto: {
|
||||
/** @example FRONTEND */
|
||||
id: string;
|
||||
/** @example Frontend */
|
||||
name: string;
|
||||
/** @description Pireon user ids of project members (others are dropped) */
|
||||
memberIds?: string[];
|
||||
/** @default false */
|
||||
archived: boolean;
|
||||
};
|
||||
ProjectSettingsBodyDto: {
|
||||
/** @description In board order. Values cannot be removed: archive them instead. */
|
||||
statuses: components["schemas"]["WorkflowStatusBodyDto"][];
|
||||
@@ -1939,6 +1958,8 @@ export interface components {
|
||||
areas: components["schemas"]["AreaOptionBodyDto"][];
|
||||
/** @description Absent: unchanged. Fields cannot be removed: archive them. */
|
||||
customFields?: components["schemas"]["CustomFieldDefinitionBodyDto"][];
|
||||
/** @description Absent: unchanged. Teams cannot be removed: archive them. */
|
||||
teams?: components["schemas"]["TeamBodyDto"][];
|
||||
/** @description Definition of done: tickets reach a DONE status only with their checklist ticked (owners and admins may override). Absent: unchanged */
|
||||
checklistBeforeDone?: boolean;
|
||||
};
|
||||
@@ -2051,6 +2072,8 @@ export interface components {
|
||||
type: string;
|
||||
/** @example FRONTEND */
|
||||
area: string | null;
|
||||
/** @description A team id */
|
||||
team: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
/**
|
||||
@@ -2410,6 +2433,11 @@ export interface components {
|
||||
* @example FRONTEND
|
||||
*/
|
||||
area?: string | null;
|
||||
/**
|
||||
* @description A team id of the project's settings
|
||||
* @example FRONTEND
|
||||
*/
|
||||
team?: string | null;
|
||||
priority?: components["schemas"]["IssuePriority"];
|
||||
labels?: string[];
|
||||
/** @description Figma frame (https) for UI changes */
|
||||
@@ -2467,6 +2495,11 @@ export interface components {
|
||||
* @example FRONTEND
|
||||
*/
|
||||
area?: string | null;
|
||||
/**
|
||||
* @description A team id of the project's settings
|
||||
* @example FRONTEND
|
||||
*/
|
||||
team?: string | null;
|
||||
priority?: components["schemas"]["IssuePriority"];
|
||||
labels?: string[];
|
||||
/** @description Figma frame (https) for UI changes */
|
||||
@@ -4946,6 +4979,8 @@ export interface operations {
|
||||
type?: string[];
|
||||
/** @description Area ids */
|
||||
area?: string[];
|
||||
/** @description Team ids */
|
||||
team?: string[];
|
||||
priority?: components["schemas"]["IssuePriority"][];
|
||||
/** @description Pireon user id */
|
||||
assigneeId?: string;
|
||||
|
||||
@@ -37,6 +37,10 @@ const ids = (what: string) =>
|
||||
const issueFields = {
|
||||
description: z.string().max(50000).optional(),
|
||||
area: z.string().optional().describe('Area id (see get_project)'),
|
||||
team: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Team id (see get_project settings.teams)'),
|
||||
priority: PRIORITY.optional(),
|
||||
labels: z.array(z.string().max(40)).max(20).optional(),
|
||||
figmaUrl: z.string().url().optional().describe('Figma frame (https)'),
|
||||
@@ -57,6 +61,7 @@ const issueFields = {
|
||||
type IssueFields = {
|
||||
description?: string;
|
||||
area?: string;
|
||||
team?: string;
|
||||
priority?: z.infer<typeof PRIORITY>;
|
||||
labels?: string[];
|
||||
figmaUrl?: string;
|
||||
@@ -144,6 +149,7 @@ export class IssuesResolver {
|
||||
categories: z.array(CATEGORY).optional(),
|
||||
types: ids('Ticket type'),
|
||||
areas: ids('Area'),
|
||||
teams: ids('Team'),
|
||||
priorities: z.array(PRIORITY).optional(),
|
||||
assigneeId: z.string().optional().describe('User id'),
|
||||
unassigned: z.boolean().optional(),
|
||||
@@ -187,6 +193,7 @@ export class IssuesResolver {
|
||||
categories?: z.infer<typeof CATEGORY>[];
|
||||
types?: string[];
|
||||
areas?: string[];
|
||||
teams?: string[];
|
||||
priorities?: z.infer<typeof PRIORITY>[];
|
||||
assigneeId?: string;
|
||||
unassigned?: boolean;
|
||||
@@ -216,6 +223,7 @@ export class IssuesResolver {
|
||||
category: params.categories,
|
||||
type: params.types,
|
||||
area: params.areas,
|
||||
team: params.teams,
|
||||
priority: params.priorities,
|
||||
assigneeId: params.assigneeId,
|
||||
unassigned: params.unassigned,
|
||||
|
||||
Reference in New Issue
Block a user