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:
@@ -27,6 +27,13 @@ export interface SettingsPayload {
|
||||
template?: { description?: string | null; checklist?: string[] } | null;
|
||||
}[];
|
||||
areas: { id: string; name: string; archived: boolean }[];
|
||||
/** Absent when saving: unchanged. */
|
||||
teams?: {
|
||||
id: string;
|
||||
name: string;
|
||||
memberIds: string[];
|
||||
archived: boolean;
|
||||
}[];
|
||||
/** Definition of done. Absent when saving: unchanged. */
|
||||
checklistBeforeDone?: boolean;
|
||||
/** Absent when saving: unchanged. */
|
||||
@@ -44,6 +51,7 @@ export function toSettingsPayload(settings: ProjectSettings): SettingsPayload {
|
||||
...settings,
|
||||
customFields: settings.customFields ?? [],
|
||||
checklistBeforeDone: !!settings.checklistBeforeDone,
|
||||
teams: settings.teams ?? [],
|
||||
transitions: settings.statuses.map((s) => ({
|
||||
from: s.id,
|
||||
to: settings.transitions[s.id] ?? [],
|
||||
@@ -75,6 +83,7 @@ export function fromSettingsPayload(payload: SettingsPayload): ProjectSettings {
|
||||
: {}),
|
||||
})),
|
||||
areas: payload.areas,
|
||||
...(payload.teams ? { teams: payload.teams } : {}),
|
||||
...(payload.checklistBeforeDone != null
|
||||
? { checklistBeforeDone: payload.checklistBeforeDone }
|
||||
: {}),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -243,6 +243,28 @@ export class AreaOptionBodyDto {
|
||||
archived = false;
|
||||
}
|
||||
|
||||
export class TeamBodyDto {
|
||||
@ApiProperty({ example: 'FRONTEND' }) @Matches(SETTING_ID) id!: string;
|
||||
@ApiProperty({ example: 'Frontend', maxLength: 40 })
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(40)
|
||||
name!: string;
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
description: 'Pireon user ids of project members (others are dropped)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMaxSize(200)
|
||||
@IsString({ each: true })
|
||||
memberIds: string[] = [];
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
archived = false;
|
||||
}
|
||||
|
||||
export class CustomFieldDefinitionBodyDto {
|
||||
@ApiProperty({ example: 'BROWSER' }) @Matches(SETTING_ID) id!: string;
|
||||
@ApiProperty({ example: 'Navegador', maxLength: 40 })
|
||||
@@ -319,6 +341,17 @@ export class ProjectSettingsBodyDto {
|
||||
@Type(() => CustomFieldDefinitionBodyDto)
|
||||
customFields?: CustomFieldDefinitionBodyDto[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: [TeamBodyDto],
|
||||
description: 'Absent: unchanged. Teams cannot be removed: archive them.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMaxSize(30)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TeamBodyDto)
|
||||
teams?: TeamBodyDto[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Definition of done: tickets reach a DONE status only with their checklist ticked (owners and admins may override). Absent: unchanged',
|
||||
@@ -584,6 +617,12 @@ export class IssueSearchQueryDto extends PageQueryDto {
|
||||
@Matches(SETTING_ID, { each: true })
|
||||
area?: string[];
|
||||
|
||||
@ApiPropertyOptional({ type: [String], description: 'Team ids' })
|
||||
@IsOptional()
|
||||
@Transform(toList)
|
||||
@Matches(SETTING_ID, { each: true })
|
||||
team?: string[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: IssuePriority,
|
||||
enumName: 'IssuePriority',
|
||||
@@ -729,6 +768,16 @@ class IssueFieldsDto {
|
||||
@Matches(SETTING_ID)
|
||||
area?: string | null;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
nullable: true,
|
||||
example: 'FRONTEND',
|
||||
description: "A team id of the project's settings",
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(SETTING_ID)
|
||||
team?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ enum: IssuePriority, enumName: 'IssuePriority' })
|
||||
@IsOptional()
|
||||
@IsEnum(IssuePriority)
|
||||
|
||||
@@ -79,6 +79,13 @@ export class AreaOptionDto {
|
||||
@ApiProperty() archived!: boolean;
|
||||
}
|
||||
|
||||
export class TeamDto {
|
||||
@ApiProperty({ example: 'FRONTEND' }) id!: string;
|
||||
@ApiProperty({ example: 'Frontend' }) name!: string;
|
||||
@ApiProperty({ type: [String] }) memberIds!: string[];
|
||||
@ApiProperty() archived!: boolean;
|
||||
}
|
||||
|
||||
export class CustomFieldDefinitionDto {
|
||||
@ApiProperty({ example: 'BROWSER' }) id!: string;
|
||||
@ApiProperty({ example: 'Navegador' }) name!: string;
|
||||
@@ -108,6 +115,7 @@ export class ProjectSettingsDto {
|
||||
'Definition of done: tickets reach DONE only with their checklist ticked',
|
||||
})
|
||||
checklistBeforeDone!: boolean;
|
||||
@ApiProperty({ type: [TeamDto] }) teams!: TeamDto[];
|
||||
}
|
||||
|
||||
export class ProjectDto {
|
||||
@@ -422,6 +430,8 @@ export class IssueDto {
|
||||
type!: string;
|
||||
@ApiProperty({ type: String, nullable: true, example: 'FRONTEND' })
|
||||
area!: string | null;
|
||||
@ApiProperty({ type: String, nullable: true, description: 'A team id' })
|
||||
team!: string | null;
|
||||
@ApiProperty() title!: string;
|
||||
@ApiProperty({ type: String, nullable: true }) description!: string | null;
|
||||
@ApiProperty({ example: 'IN_PROGRESS', description: 'A status id' })
|
||||
|
||||
@@ -165,6 +165,7 @@ export class IssuesController {
|
||||
categories: query.category,
|
||||
types: query.type,
|
||||
areas: query.area,
|
||||
teams: query.team,
|
||||
priorities: query.priority,
|
||||
assigneeId: query.assigneeId,
|
||||
unassigned: query.unassigned,
|
||||
|
||||
Reference in New Issue
Block a user