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

@@ -128,6 +128,11 @@ abstract class IssueFieldsInput {
@Matches(SETTING_ID)
area?: string | null;
@Field(() => String, { nullable: true, description: 'A team id' })
@IsOptional()
@Matches(SETTING_ID)
team?: string | null;
@Field(() => IssuePriority, { nullable: true })
@IsOptional()
@IsEnum(IssuePriority)
@@ -515,6 +520,10 @@ export class IssueFilterInput {
@IsOptional()
@IsString({ each: true })
areas?: string[];
@Field(() => [String], { nullable: true, description: 'Team ids' })
@IsOptional()
@IsString({ each: true })
teams?: string[];
@Field(() => [IssuePriority], { nullable: true })
@IsOptional()
@IsEnum(IssuePriority, { each: true })
@@ -712,6 +721,18 @@ export class AreaOptionInput {
@Field({ defaultValue: false }) @IsBoolean() archived!: boolean;
}
@InputType()
export class TeamInput {
@Field() @Matches(SETTING_ID) id!: string;
@Field() @IsString() @MinLength(1) @MaxLength(40) name!: string;
@Field(() => [ID], { defaultValue: [] })
@IsArray()
@ArrayMaxSize(200)
@IsString({ each: true })
memberIds!: string[];
@Field({ defaultValue: false }) @IsBoolean() archived!: boolean;
}
@InputType()
export class CustomFieldDefinitionInput {
@Field() @Matches(SETTING_ID) id!: string;
@@ -773,6 +794,17 @@ export class ProjectSettingsInput {
@Type(() => CustomFieldDefinitionInput)
customFields?: CustomFieldDefinitionInput[];
@Field(() => [TeamInput], {
nullable: true,
description: 'Absent: unchanged. Teams cannot be removed: archive them.',
})
@IsOptional()
@IsArray()
@ArrayMaxSize(30)
@ValidateNested({ each: true })
@Type(() => TeamInput)
teams?: TeamInput[];
@Field({
nullable: true,
description:

View File

@@ -104,6 +104,17 @@ export class CustomFieldValueGql {
@Field() value!: string;
}
@ObjectType('Team', {
description:
'A group of members: tickets go to it; @team mentions tell them all',
})
export class TeamGql {
@Field() id!: string;
@Field() name!: string;
@Field(() => [ID]) memberIds!: string[];
@Field() archived!: boolean;
}
@ObjectType('ProjectSettings', {
description:
'Statuses (board order), transitions, ticket types, areas and custom fields',
@@ -120,6 +131,7 @@ export class ProjectSettingsGql {
'Definition of done: tickets reach DONE only with their checklist ticked (owners and admins may override)',
})
checklistBeforeDone!: boolean;
@Field(() => [TeamGql]) teams!: TeamGql[];
}
@ObjectType('Project')
@@ -366,6 +378,8 @@ export class IssueGql {
type!: string;
@Field(() => String, { nullable: true, description: 'An area id' })
area!: string | null;
@Field(() => String, { nullable: true, description: 'A team id' })
team!: string | null;
@Field() title!: string;
@Field(() => String, { nullable: true }) description!: string | null;
@Field({ description: "A status id of the project's settings" })