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>
1037 lines
24 KiB
TypeScript
1037 lines
24 KiB
TypeScript
import {
|
|
ApiProperty,
|
|
ApiPropertyOptional,
|
|
OmitType,
|
|
PartialType,
|
|
} from '@nestjs/swagger';
|
|
import {
|
|
IssueLinkType,
|
|
IssuePriority,
|
|
ProjectRole,
|
|
SETTING_COLORS,
|
|
StatusCategory,
|
|
TYPE_ICONS,
|
|
} from '@task-stream/api/tracker/domain';
|
|
import { IssueSort } from '@task-stream/api/tracker/application';
|
|
import { Transform, Type } from 'class-transformer';
|
|
import {
|
|
ArrayMaxSize,
|
|
IsArray,
|
|
IsBoolean,
|
|
IsEnum,
|
|
IsIn,
|
|
IsInt,
|
|
IsISO8601,
|
|
IsOptional,
|
|
IsUUID,
|
|
IsString,
|
|
IsUrl,
|
|
Matches,
|
|
Max,
|
|
MaxLength,
|
|
Min,
|
|
MinLength,
|
|
ValidateIf,
|
|
ValidateNested,
|
|
} from 'class-validator';
|
|
|
|
/* REST request bodies and query strings, validated like the GraphQL inputs. */
|
|
|
|
/** `?status=TODO&status=DONE` or `?status=TODO,DONE` → ['TODO', 'DONE']. */
|
|
const toList = ({ value }: { value: unknown }) =>
|
|
value === undefined || value === ''
|
|
? undefined
|
|
: ([] as unknown[])
|
|
.concat(value)
|
|
.flatMap((v) => String(v).split(','))
|
|
.map((v) => v.trim())
|
|
.filter(Boolean);
|
|
|
|
const toBoolean = ({ value }: { value: unknown }) =>
|
|
value === undefined ? undefined : value === true || value === 'true';
|
|
|
|
/** Ids of statuses, types and areas (checked against the project's settings). */
|
|
const SETTING_ID = /^[A-Za-z][A-Za-z0-9_]{0,29}$/;
|
|
|
|
// ---- Projects ---------------------------------------------------------------
|
|
|
|
export class CreateProjectDto {
|
|
@ApiProperty({
|
|
example: 'WEB',
|
|
description:
|
|
'2-10 letters/digits starting with a letter → tickets WEB-1, WEB-2…',
|
|
})
|
|
@Matches(/^[A-Za-z][A-Za-z0-9]{1,9}$/)
|
|
key!: string;
|
|
|
|
@ApiProperty({ example: 'Checkout revamp', maxLength: 120 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(120)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 5000 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
description?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
default: false,
|
|
description:
|
|
'Anyone can read it without signing in (board, tickets, comments, releases, activity, statistics)',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
isPublic?: boolean;
|
|
}
|
|
|
|
export class UpdateProjectDto extends PartialType(
|
|
OmitType(CreateProjectDto, ['key'] as const),
|
|
) {}
|
|
|
|
export class DeleteProjectQueryDto {
|
|
@ApiProperty({
|
|
example: 'WEB',
|
|
description: 'The project key, to confirm the deletion',
|
|
})
|
|
@IsString()
|
|
confirm!: string;
|
|
}
|
|
|
|
export class AddMemberDto {
|
|
@ApiProperty({ description: 'Pireon user id (see GET /users)' })
|
|
@IsString()
|
|
userId!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
enum: ProjectRole,
|
|
enumName: 'ProjectRole',
|
|
default: ProjectRole.MEMBER,
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(ProjectRole)
|
|
role: ProjectRole = ProjectRole.MEMBER;
|
|
}
|
|
|
|
// ---- Project settings -------------------------------------------------------
|
|
|
|
export class WorkflowStatusBodyDto {
|
|
@ApiProperty({
|
|
example: 'IN_REVIEW',
|
|
description: 'Never change an existing id',
|
|
})
|
|
@Matches(SETTING_ID)
|
|
id!: string;
|
|
@ApiProperty({ example: 'En revisión', maxLength: 40 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiProperty({ enum: StatusCategory, enumName: 'StatusCategory' })
|
|
@IsEnum(StatusCategory)
|
|
category!: StatusCategory;
|
|
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
|
|
@IsIn(SETTING_COLORS)
|
|
color!: string;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
requiresReason = false;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
archived = false;
|
|
@ApiPropertyOptional({
|
|
type: Number,
|
|
nullable: true,
|
|
minimum: 1,
|
|
maximum: 999,
|
|
description: 'Work-in-progress limit (null: none)',
|
|
})
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(999)
|
|
wipLimit?: number | null;
|
|
}
|
|
|
|
export class StatusTransitionsBodyDto {
|
|
@ApiProperty({ example: 'IN_QA' }) @Matches(SETTING_ID) from!: string;
|
|
@ApiProperty({ type: [String], example: ['DONE', 'QA_FAILED'] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@Matches(SETTING_ID, { each: true })
|
|
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 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiProperty({ enum: TYPE_ICONS, enumName: 'TypeIcon' })
|
|
@IsIn(TYPE_ICONS)
|
|
icon!: string;
|
|
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
|
|
@IsIn(SETTING_COLORS)
|
|
color!: string;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@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 {
|
|
@ApiProperty({ example: 'DATA' }) @Matches(SETTING_ID) id!: string;
|
|
@ApiProperty({ example: 'Datos', maxLength: 40 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
archived = false;
|
|
}
|
|
|
|
export class ProjectSettingsBodyDto {
|
|
@ApiProperty({
|
|
type: [WorkflowStatusBodyDto],
|
|
description:
|
|
'In board order. Values cannot be removed: archive them instead.',
|
|
})
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => WorkflowStatusBodyDto)
|
|
statuses!: WorkflowStatusBodyDto[];
|
|
|
|
@ApiProperty({ type: [StatusTransitionsBodyDto] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => StatusTransitionsBodyDto)
|
|
transitions!: StatusTransitionsBodyDto[];
|
|
|
|
@ApiProperty({ type: [IssueTypeOptionBodyDto] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => IssueTypeOptionBodyDto)
|
|
issueTypes!: IssueTypeOptionBodyDto[];
|
|
|
|
@ApiProperty({ type: [AreaOptionBodyDto] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => AreaOptionBodyDto)
|
|
areas!: AreaOptionBodyDto[];
|
|
}
|
|
|
|
export class AnalyticsQueryDto {
|
|
@ApiPropertyOptional({ type: Number, default: 90, minimum: 7, maximum: 365 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(7)
|
|
@Max(365)
|
|
days = 90;
|
|
}
|
|
|
|
export class ChangeMemberRoleDto {
|
|
@ApiProperty({ enum: ProjectRole, enumName: 'ProjectRole' })
|
|
@IsEnum(ProjectRole)
|
|
role!: ProjectRole;
|
|
}
|
|
|
|
export class BoardQueryDto {
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this release' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only this person (Pireon user id)' })
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string;
|
|
|
|
@ApiPropertyOptional({ example: 'FRONTEND', description: 'An area id' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
area?: string;
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Ticket type ids' })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
type?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priority?: IssuePriority[];
|
|
|
|
@ApiPropertyOptional({ type: [String] })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsString({ each: true })
|
|
label?: string[];
|
|
|
|
@ApiPropertyOptional({ description: 'Matches key, title or description' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets without assignee' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
unassigned?: boolean;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this sprint' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
sprintId?: string;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 50, minimum: 1, maximum: 200 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(200)
|
|
perColumn = 50;
|
|
}
|
|
|
|
export class ActivityQueryDto {
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this ticket' })
|
|
@IsOptional()
|
|
@IsString()
|
|
issueId?: string;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 50, minimum: 1, maximum: 200 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(200)
|
|
limit = 50;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'date-time',
|
|
description: 'Cursor: entries older than this date',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
before?: string;
|
|
}
|
|
|
|
export class PageQueryDto {
|
|
@ApiPropertyOptional({ type: Number, default: 50, minimum: 1, maximum: 200 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(200)
|
|
limit = 50;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 0, minimum: 0 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(0)
|
|
offset = 0;
|
|
}
|
|
|
|
// ---- Issues -----------------------------------------------------------------
|
|
|
|
export class MyIssuesQueryDto extends PageQueryDto {
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
default: true,
|
|
description:
|
|
'true: tickets assigned to me. false: any ticket of my projects (e.g. ?assignedToMe=false&category=QA)',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
assignedToMe = true;
|
|
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description: 'Status ids. Repeat the parameter or separate with commas',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
status?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: StatusCategory,
|
|
enumName: 'StatusCategory',
|
|
isArray: true,
|
|
description: 'Whatever each project calls its statuses',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(StatusCategory, { each: true })
|
|
category?: StatusCategory[];
|
|
}
|
|
|
|
export class UpcomingReleasesQueryDto {
|
|
@ApiPropertyOptional({ type: Number, default: 10, minimum: 1, maximum: 50 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(50)
|
|
limit = 10;
|
|
}
|
|
|
|
export class IssueSearchQueryDto extends PageQueryDto {
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description: 'Status ids. Repeat the parameter or separate with commas',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
status?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: StatusCategory,
|
|
enumName: 'StatusCategory',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(StatusCategory, { each: true })
|
|
category?: StatusCategory[];
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Ticket type ids' })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
type?: string[];
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Area ids' })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
area?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priority?: IssuePriority[];
|
|
|
|
@ApiPropertyOptional({ description: 'Pireon user id' })
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets without assignee' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
unassigned?: boolean;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string;
|
|
|
|
@ApiPropertyOptional({ type: [String] })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsString({ each: true })
|
|
label?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Only the tickets hanging from this one (id or key)',
|
|
example: 'WEB-3',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
parent?: string;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this sprint' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
sprintId?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets in no sprint' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
noSprint?: boolean;
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssueSort,
|
|
enumName: 'IssueSort',
|
|
description: 'RECENT (default): last updated first. RANK: backlog order.',
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(IssueSort)
|
|
sort?: IssueSort;
|
|
|
|
@ApiPropertyOptional({ description: 'Matches key, title or description' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string;
|
|
}
|
|
|
|
class IssueFieldsDto {
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 50000 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(50000)
|
|
description?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'FRONTEND',
|
|
description: "An area id of the project's settings",
|
|
})
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
area?: string | null;
|
|
|
|
@ApiPropertyOptional({ enum: IssuePriority, enumName: 'IssuePriority' })
|
|
@IsOptional()
|
|
@IsEnum(IssuePriority)
|
|
priority?: IssuePriority;
|
|
|
|
@ApiPropertyOptional({ type: [String], maxItems: 20 })
|
|
@IsOptional()
|
|
@ArrayMaxSize(20)
|
|
@IsString({ each: true })
|
|
@MaxLength(40, { each: true })
|
|
labels?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Figma frame (https) for UI changes',
|
|
})
|
|
@IsOptional()
|
|
@IsUrl({ protocols: ['https'] })
|
|
figmaUrl?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: Number,
|
|
nullable: true,
|
|
minimum: 0,
|
|
maximum: 100,
|
|
})
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(0)
|
|
@Max(100)
|
|
storyPoints?: number | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'date',
|
|
description: 'When work should start (roadmap)',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
startDate?: string | null;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, format: 'date' })
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
dueDate?: string | null;
|
|
}
|
|
|
|
export class CreateIssueDto extends IssueFieldsDto {
|
|
@ApiProperty({
|
|
example: 'BUG',
|
|
description: "A ticket type id of the project's settings",
|
|
})
|
|
@Matches(SETTING_ID)
|
|
type!: string;
|
|
|
|
@ApiProperty({
|
|
example: 'El botón de pagar no responde en Safari',
|
|
maxLength: 255,
|
|
})
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(255)
|
|
title!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Pireon user id',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string | null;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, format: 'uuid' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-3',
|
|
description:
|
|
'Epic or ticket of the same project the new one breaks down (id or key)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
parent?: string | null;
|
|
}
|
|
|
|
export class UpdateIssueDto extends IssueFieldsDto {
|
|
@ApiPropertyOptional({ example: 'FEATURE', description: 'A ticket type id' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
type?: string;
|
|
|
|
@ApiPropertyOptional({ maxLength: 255 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(255)
|
|
title?: string;
|
|
}
|
|
|
|
export class ChangeStatusDto {
|
|
@ApiProperty({
|
|
example: 'IN_REVIEW',
|
|
description:
|
|
"A status id of the project's settings, among the ticket's allowedTransitions",
|
|
})
|
|
@Matches(SETTING_ID)
|
|
status!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Required by statuses with requiresReason (e.g. QA_FAILED)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
reason?: string | null;
|
|
}
|
|
|
|
export class AssignIssueDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Pireon user id of a project member, or null to unassign',
|
|
})
|
|
@ValidateIf((dto: AssignIssueDto) => dto.assigneeId !== null)
|
|
@IsString()
|
|
assigneeId!: string | null;
|
|
}
|
|
|
|
export class SetIssueReleaseDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'uuid',
|
|
description: 'Planned release, or null to take the ticket out',
|
|
})
|
|
@ValidateIf((dto: SetIssueReleaseDto) => dto.releaseId !== null)
|
|
@IsString()
|
|
releaseId!: string | null;
|
|
}
|
|
|
|
export class SavedFilterCriteriaDto {
|
|
@ApiPropertyOptional({ type: [String], description: 'Ticket type ids' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID, { each: true })
|
|
types?: string[];
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Area ids' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID, { each: true })
|
|
areas?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priorities?: IssuePriority[];
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Pireon user id',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string | null;
|
|
|
|
@ApiPropertyOptional() @IsOptional() @IsBoolean() unassigned?: boolean;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, format: 'uuid' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string | null;
|
|
|
|
@ApiPropertyOptional({ type: [String] })
|
|
@IsOptional()
|
|
@ArrayMaxSize(20)
|
|
@IsString({ each: true })
|
|
labels?: string[];
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 200 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string | null;
|
|
}
|
|
|
|
export class SaveFilterDto {
|
|
@ApiProperty({ example: 'Bugs críticos de frontend', maxLength: 60 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(60)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
default: false,
|
|
description: 'Visible to the whole project (members, not viewers, share)',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
shared = false;
|
|
|
|
@ApiProperty({ type: SavedFilterCriteriaDto })
|
|
@ValidateNested()
|
|
@Type(() => SavedFilterCriteriaDto)
|
|
criteria!: SavedFilterCriteriaDto;
|
|
}
|
|
|
|
export class CreateWebhookDto {
|
|
@ApiProperty({ example: 'https://chat.example.org/hooks/taskstream' })
|
|
@IsUrl({ require_tld: false, require_protocol: true })
|
|
url!: string;
|
|
|
|
@ApiProperty({
|
|
type: [String],
|
|
example: ['issue.status_changed', 'release.*'],
|
|
description: 'Event names (GET /api/webhook-events), family.* or *',
|
|
})
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@IsString({ each: true })
|
|
events!: string[];
|
|
}
|
|
|
|
export class UpdateWebhookDto extends PartialType(CreateWebhookDto) {
|
|
@ApiPropertyOptional({ description: 'Paused webhooks get nothing' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
active?: boolean;
|
|
}
|
|
|
|
export class ImportQueryDto {
|
|
@ApiPropertyOptional({
|
|
default: false,
|
|
description: 'Only tell what would be created',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
dryRun = false;
|
|
}
|
|
|
|
export class RankIssueDto {
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-4',
|
|
description:
|
|
'The ticket that ends up right above (id or key; null: the top)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
above?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-9',
|
|
description:
|
|
'The ticket that ends up right below (id or key; null: the bottom)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
below?: string | null;
|
|
}
|
|
|
|
export class SetIssueParentDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-3',
|
|
description:
|
|
'Epic or ticket of the same project (id or key), or null to take it back to the top',
|
|
})
|
|
@ValidateIf((dto: SetIssueParentDto) => dto.parent !== null)
|
|
@IsString()
|
|
parent!: string | null;
|
|
}
|
|
|
|
export class ChecklistItemBodyDto {
|
|
@ApiProperty({ example: 'Actualizar el changelog', maxLength: 300 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(300)
|
|
text!: string;
|
|
}
|
|
|
|
export class UpdateChecklistItemDto {
|
|
@ApiPropertyOptional({ maxLength: 300 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(300)
|
|
text?: string;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
done?: boolean;
|
|
}
|
|
|
|
export class LinkIssueDto {
|
|
@ApiProperty({ enum: IssueLinkType, enumName: 'IssueLinkType' })
|
|
@IsEnum(IssueLinkType)
|
|
type!: IssueLinkType;
|
|
|
|
@ApiProperty({ example: 'API-3', description: 'Other ticket: id or key' })
|
|
@IsString()
|
|
@MinLength(1)
|
|
targetIssue!: string;
|
|
}
|
|
|
|
export class LinkTypeParamDto {
|
|
@ApiProperty({ enum: IssueLinkType, enumName: 'IssueLinkType' })
|
|
@IsIn(Object.values(IssueLinkType))
|
|
type!: IssueLinkType;
|
|
}
|
|
|
|
/** Multipart body of an upload; the file itself travels as `file`. */
|
|
export class AttachFileDto {
|
|
@ApiProperty({ type: 'string', format: 'binary', description: 'The file' })
|
|
file?: unknown;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'uuid',
|
|
description: "One of the ticket's comments, to show the file under it",
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
commentId?: string;
|
|
}
|
|
|
|
export class CommentBodyDto {
|
|
@ApiProperty({ example: 'Reproducido en Safari 17.', maxLength: 20000 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(20000)
|
|
body!: string;
|
|
}
|
|
|
|
// ---- Releases ---------------------------------------------------------------
|
|
|
|
export class CreateReleaseDto {
|
|
@ApiProperty({ example: 'v1.4.0', maxLength: 60 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(60)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 5000 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
description?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'date',
|
|
example: '2026-11-14',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
targetDate?: string | null;
|
|
}
|
|
|
|
export class UpdateReleaseDto extends PartialType(CreateReleaseDto) {}
|
|
|
|
export class ReleaseNotesDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
maxLength: 20000,
|
|
description: 'Markdown; null removes the notes',
|
|
})
|
|
@ValidateIf((dto: ReleaseNotesDto) => dto.notes !== null)
|
|
@IsString()
|
|
@MaxLength(20000)
|
|
notes!: string | null;
|
|
}
|
|
|
|
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
|
export class CreateSprintDto {
|
|
@ApiProperty({ example: 'Sprint 14', maxLength: 80 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(80)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
maxLength: 500,
|
|
example: 'Pagos con tarjeta en producción',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(500)
|
|
goal?: string | null;
|
|
|
|
@ApiProperty({ format: 'date', example: '2026-10-12' })
|
|
@Matches(DAY)
|
|
startDate!: string;
|
|
|
|
@ApiProperty({
|
|
format: 'date',
|
|
example: '2026-10-23',
|
|
description: 'Last day, included',
|
|
})
|
|
@Matches(DAY)
|
|
endDate!: string;
|
|
}
|
|
|
|
export class UpdateSprintDto extends PartialType(CreateSprintDto) {}
|
|
|
|
export class CloseSprintDto {
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'uuid',
|
|
description:
|
|
'Open sprint of the same project that takes the unfinished tickets; null or absent: back to the backlog',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
carryOverTo?: string | null;
|
|
}
|
|
|
|
export class SetIssueSprintDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'uuid',
|
|
description: 'Open sprint of the project, or null for the backlog',
|
|
})
|
|
@ValidateIf((dto: SetIssueSprintDto) => dto.sprintId !== null)
|
|
@IsUUID()
|
|
sprintId!: string | null;
|
|
}
|
|
|
|
export class PublishReleaseDto {
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
default: false,
|
|
description: 'Publish even with unfinished tickets',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
force = false;
|
|
}
|
|
|
|
// ---- Notifications ----------------------------------------------------------
|
|
|
|
export class MyNotificationsQueryDto {
|
|
@ApiPropertyOptional({ default: false, description: 'Only unread ones' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
unreadOnly = false;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 30, minimum: 1, maximum: 100 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(100)
|
|
limit = 30;
|
|
}
|
|
|
|
export class MarkNotificationsReadDto {
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description: 'Omit to mark all of them',
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(200)
|
|
@IsString({ each: true })
|
|
ids?: string[];
|
|
}
|
|
|
|
export class NotificationSettingsDto {
|
|
@ApiProperty({ description: 'Copies by e-mail' })
|
|
@IsBoolean()
|
|
email!: boolean;
|
|
}
|