feat: templates per ticket type
Each ticket type can carry a starting description and a checklist, edited in the project settings. New tickets of that type get the description when they come without one and always get the checklist; the new ticket form says what the template will add. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ export interface SettingsPayload {
|
||||
icon: string;
|
||||
color: string;
|
||||
archived: boolean;
|
||||
template?: { description?: string | null; checklist?: string[] } | null;
|
||||
}[];
|
||||
areas: { id: string; name: string; archived: boolean }[];
|
||||
}
|
||||
@@ -47,10 +48,18 @@ export function fromSettingsPayload(payload: SettingsPayload): ProjectSettings {
|
||||
transitions: Object.fromEntries(
|
||||
payload.transitions.map((t) => [t.from, t.to]),
|
||||
),
|
||||
issueTypes: payload.issueTypes.map((t) => ({
|
||||
issueTypes: payload.issueTypes.map(({ template, ...t }) => ({
|
||||
...t,
|
||||
icon: t.icon as TypeIcon,
|
||||
color: t.color as SettingColor,
|
||||
...(template
|
||||
? {
|
||||
template: {
|
||||
description: template.description ?? null,
|
||||
checklist: template.checklist ?? [],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
areas: payload.areas,
|
||||
};
|
||||
|
||||
@@ -548,6 +548,21 @@ export class StatusTransitionsInput {
|
||||
to!: string[];
|
||||
}
|
||||
|
||||
@InputType('IssueTemplateInput')
|
||||
export class IssueTemplateInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(5000)
|
||||
description?: string | null;
|
||||
@Field(() => [String], { defaultValue: [] })
|
||||
@IsArray()
|
||||
@ArrayMaxSize(20)
|
||||
@IsString({ each: true })
|
||||
@MaxLength(300, { each: true })
|
||||
checklist!: string[];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class IssueTypeOptionInput {
|
||||
@Field() @Matches(SETTING_ID) id!: string;
|
||||
@@ -555,6 +570,11 @@ export class IssueTypeOptionInput {
|
||||
@Field() @IsString() icon!: string;
|
||||
@Field() @IsString() color!: string;
|
||||
@Field({ defaultValue: false }) @IsBoolean() archived!: boolean;
|
||||
@Field(() => IssueTemplateInput, { nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => IssueTemplateInput)
|
||||
template?: IssueTemplateInput | null;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
@@ -53,6 +53,19 @@ export class StatusTransitionsGql {
|
||||
@Field(() => [String]) to!: string[];
|
||||
}
|
||||
|
||||
@ObjectType('IssueTemplate', {
|
||||
description: 'What a new ticket of the type starts with',
|
||||
})
|
||||
export class IssueTemplateGql {
|
||||
@Field(() => String, {
|
||||
nullable: true,
|
||||
description: 'Used when the ticket comes without a description',
|
||||
})
|
||||
description!: string | null;
|
||||
@Field(() => [String], { description: 'Steps added to every new ticket' })
|
||||
checklist!: string[];
|
||||
}
|
||||
|
||||
@ObjectType('IssueTypeOption')
|
||||
export class IssueTypeOptionGql {
|
||||
@Field() id!: string;
|
||||
@@ -60,6 +73,8 @@ export class IssueTypeOptionGql {
|
||||
@Field() icon!: string;
|
||||
@Field() color!: string;
|
||||
@Field() archived!: boolean;
|
||||
@Field(() => IssueTemplateGql, { nullable: true })
|
||||
template?: IssueTemplateGql | null;
|
||||
}
|
||||
|
||||
@ObjectType('AreaOption')
|
||||
|
||||
@@ -165,6 +165,21 @@ export class StatusTransitionsBodyDto {
|
||||
to!: string[];
|
||||
}
|
||||
|
||||
export class IssueTemplateBodyDto {
|
||||
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 5000 })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(5000)
|
||||
description?: string | null;
|
||||
@ApiPropertyOptional({ type: [String], maxItems: 20 })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMaxSize(20)
|
||||
@IsString({ each: true })
|
||||
@MaxLength(300, { each: true })
|
||||
checklist?: string[];
|
||||
}
|
||||
|
||||
export class IssueTypeOptionBodyDto {
|
||||
@ApiProperty({ example: 'SPIKE' }) @Matches(SETTING_ID) id!: string;
|
||||
@ApiProperty({ example: 'Spike', maxLength: 40 })
|
||||
@@ -182,6 +197,15 @@ export class IssueTypeOptionBodyDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
archived = false;
|
||||
@ApiPropertyOptional({
|
||||
type: IssueTemplateBodyDto,
|
||||
nullable: true,
|
||||
description: 'What a new ticket of the type starts with',
|
||||
})
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => IssueTemplateBodyDto)
|
||||
template?: IssueTemplateBodyDto | null;
|
||||
}
|
||||
|
||||
export class AreaOptionBodyDto {
|
||||
|
||||
@@ -53,6 +53,11 @@ export class StatusTransitionsDto {
|
||||
to!: string[];
|
||||
}
|
||||
|
||||
export class IssueTemplateDto {
|
||||
@ApiProperty({ type: String, nullable: true }) description!: string | null;
|
||||
@ApiProperty({ type: [String] }) checklist!: string[];
|
||||
}
|
||||
|
||||
export class IssueTypeOptionDto {
|
||||
@ApiProperty({ example: 'BUG' }) id!: string;
|
||||
@ApiProperty({ example: 'Bug' }) name!: string;
|
||||
@@ -60,6 +65,11 @@ export class IssueTypeOptionDto {
|
||||
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
|
||||
color!: string;
|
||||
@ApiProperty() archived!: boolean;
|
||||
@ApiPropertyOptional({
|
||||
type: IssueTemplateDto,
|
||||
description: 'What a new ticket of the type starts with',
|
||||
})
|
||||
template?: IssueTemplateDto;
|
||||
}
|
||||
|
||||
export class AreaOptionDto {
|
||||
|
||||
Reference in New Issue
Block a user