Files
task-stream/libs/api/tracker/presenters/src/lib/graphql/inputs.ts
Josep Piguillem 034704e46f feat: alerts open bugs, and repeats of the same problem go to that bug
Owners and admins turn on a secret URL per project (Settings →
Integrations), POST /api/webhooks/alerts/{project}/{token}, that Sentry,
Uptime Kuma, Prometheus Alertmanager, Grafana or any tool sending JSON
with a title can call. A new problem opens a bug as TaskStream (label
«alerta», chosen type and priority); while that bug is open, the same
fingerprint goes to it (a comment at most every 30 minutes) and
«resolved» is said on it; once it is closed, a new bug names the old
one. Alerts of a project are handled one at a time, so two at once open
one bug, and a bug not yet projected still counts as open. Unknown
project, alerts off or a bad token all answer 403.

TaskStream may now comment as the system actor, like it already opened
tickets for public proposals. Stored in Mongo (alert_integrations,
alert_traces), not in the event store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:36:55 +02:00

899 lines
23 KiB
TypeScript

import { Field, ID, InputType, Int } from '@nestjs/graphql';
import {
CustomFieldType,
IssueLinkType,
IssuePriority,
ProjectRole,
StatusCategory,
} from '@task-stream/api/tracker/domain';
import {
AutomationActionKind,
AutomationTriggerKind,
IssueSort,
} from '@task-stream/api/tracker/application';
import { Type } from 'class-transformer';
import {
ArrayMaxSize,
IsArray,
IsBoolean,
IsEnum,
IsInt,
IsISO8601,
IsOptional,
IsString,
IsUrl,
IsUUID,
Matches,
Max,
MaxLength,
Min,
MinLength,
ValidateNested,
} from 'class-validator';
/** Ids of statuses, types and areas (checked against the project's settings). */
const SETTING_ID = /^[A-Za-z][A-Za-z0-9_]{0,29}$/;
@InputType()
export class CreateProjectInput {
@Field({ description: '2-10 chars, e.g. "TS" → tickets TS-1, TS-2...' })
@Matches(/^[A-Za-z][A-Za-z0-9]{1,9}$/)
key!: string;
@Field() @IsString() @MinLength(1) @MaxLength(120) name!: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(5000)
description?: string | null;
@Field({
nullable: true,
description: 'Readable by anyone, also without signing in',
})
@IsOptional()
@IsBoolean()
isPublic?: boolean;
@Field(() => ID, {
nullable: true,
description:
'A project you own or administer to start from: its statuses, transitions, types, areas, custom fields and automation rules are copied',
})
@IsOptional()
@IsUUID()
templateProjectId?: string | null;
}
@InputType()
export class UpdateProjectInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field({ nullable: true })
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(120)
name?: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(5000)
description?: string | null;
@Field({ nullable: true })
@IsOptional()
@IsBoolean()
isPublic?: boolean;
@Field({
nullable: true,
description:
'Take bug reports and ideas from anyone through the public form (while public)',
})
@IsOptional()
@IsBoolean()
acceptsProposals?: boolean;
}
@InputType()
export class ProjectMemberInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field(() => ID, { description: 'Pireon user id' })
@IsString()
userId!: string;
@Field(() => ProjectRole, { defaultValue: ProjectRole.MEMBER })
@IsEnum(ProjectRole)
role!: ProjectRole;
}
@InputType({ description: "A custom field's value; null or '' clears it" })
export class CustomFieldValueInput {
@Field() @Matches(SETTING_ID) fieldId!: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(500)
value?: string | null;
}
@InputType({ isAbstract: true })
abstract class IssueFieldsInput {
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(50000)
description?: string | null;
@Field(() => String, { nullable: true, description: 'An area id' })
@IsOptional()
@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)
priority?: IssuePriority;
@Field(() => [String], { nullable: true })
@IsOptional()
@ArrayMaxSize(20)
@IsString({ each: true })
@MaxLength(40, { each: true })
labels?: string[];
@Field(() => String, {
nullable: true,
description: 'Figma frame for UI changes',
})
@IsOptional()
@IsUrl({ protocols: ['https'] })
figmaUrl?: string | null;
@Field(() => Int, { nullable: true })
@IsOptional()
@IsInt()
@Min(0)
@Max(100)
storyPoints?: number | null;
@Field(() => String, {
nullable: true,
description: 'When work should start (roadmap), YYYY-MM-DD',
})
@IsOptional()
@IsISO8601()
startDate?: string | null;
@Field(() => String, { nullable: true }) @IsOptional() @IsISO8601() dueDate?:
| string
| null;
@Field(() => [CustomFieldValueInput], {
nullable: true,
description: 'Only the fields to change',
})
@IsOptional()
@IsArray()
@ArrayMaxSize(30)
@ValidateNested({ each: true })
@Type(() => CustomFieldValueInput)
customFields?: CustomFieldValueInput[];
}
@InputType()
export class CreateIssueInput extends IssueFieldsInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field({ description: 'A ticket type id' })
@Matches(SETTING_ID)
type!: string;
@Field() @IsString() @MinLength(1) @MaxLength(255) title!: string;
@Field(() => ID, { nullable: true }) @IsOptional() @IsString() assigneeId?:
| string
| null;
@Field(() => ID, { nullable: true }) @IsOptional() @IsUUID() releaseId?:
| string
| null;
@Field(() => ID, {
nullable: true,
description: 'Epic or ticket of the same project the new one breaks down',
})
@IsOptional()
@IsUUID()
parentId?: string | null;
}
@InputType()
export class UpdateIssueInput extends IssueFieldsInput {
@Field(() => ID) @IsUUID() issueId!: string;
@Field({ nullable: true })
@IsOptional()
@Matches(SETTING_ID)
type?: string;
@Field({ nullable: true })
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(255)
title?: string;
}
@InputType()
export class ChangeIssueStatusInput {
@Field(() => ID) @IsUUID() issueId!: string;
@Field({ description: 'A status id' })
@Matches(SETTING_ID)
status!: string;
@Field(() => String, {
nullable: true,
description: 'Required by statuses with requiresReason',
})
@IsOptional()
@IsString()
@MaxLength(5000)
reason?: string | null;
}
const DAY = /^\d{4}-\d{2}-\d{2}$/;
@InputType()
export class CreateSprintInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field() @IsString() @MinLength(1) @MaxLength(80) name!: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(500)
goal?: string | null;
@Field({ description: 'YYYY-MM-DD' }) @Matches(DAY) startDate!: string;
@Field({ description: 'YYYY-MM-DD, included' })
@Matches(DAY)
endDate!: string;
}
@InputType()
export class UpdateSprintInput {
@Field(() => ID) @IsUUID() sprintId!: string;
@Field({ nullable: true })
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(80)
name?: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(500)
goal?: string | null;
@Field({ nullable: true }) @IsOptional() @Matches(DAY) startDate?: string;
@Field({ nullable: true }) @IsOptional() @Matches(DAY) endDate?: string;
}
@InputType()
export class SavedFilterCriteriaInput {
@Field(() => [String], { nullable: true })
@IsOptional()
@Matches(SETTING_ID, { each: true })
types?: string[];
@Field(() => [String], { nullable: true })
@IsOptional()
@Matches(SETTING_ID, { each: true })
areas?: string[];
@Field(() => [IssuePriority], { nullable: true })
@IsOptional()
@IsEnum(IssuePriority, { each: true })
priorities?: IssuePriority[];
@Field(() => ID, { nullable: true })
@IsOptional()
@IsString()
assigneeId?: string | null;
@Field({ nullable: true }) @IsOptional() @IsBoolean() unassigned?: boolean;
@Field(() => ID, { nullable: true })
@IsOptional()
@IsUUID()
releaseId?: string | null;
@Field(() => [String], { nullable: true })
@IsOptional()
@ArrayMaxSize(20)
@IsString({ each: true })
labels?: string[];
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(200)
search?: string | null;
}
@InputType()
export class SaveFilterInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field() @IsString() @MinLength(1) @MaxLength(60) name!: string;
@Field({
defaultValue: false,
description: 'Visible to the whole project (members, not viewers, share)',
})
@IsBoolean()
shared!: boolean;
@Field(() => SavedFilterCriteriaInput)
@ValidateNested()
@Type(() => SavedFilterCriteriaInput)
criteria!: SavedFilterCriteriaInput;
}
@InputType()
export class BulkUpdateIssuesInput {
@Field(() => [ID])
@IsArray()
@ArrayMaxSize(100)
@IsUUID('all', { each: true })
issueIds!: string[];
@Field({ nullable: true }) @IsOptional() @Matches(SETTING_ID) status?: string;
@Field({ nullable: true, description: 'Unassign everything' })
@IsOptional()
@IsBoolean()
unassign?: boolean;
@Field(() => ID, { nullable: true })
@IsOptional()
@IsString()
assigneeId?: string;
@Field({ nullable: true, description: 'Back to the backlog' })
@IsOptional()
@IsBoolean()
noSprint?: boolean;
@Field(() => ID, { nullable: true })
@IsOptional()
@IsUUID()
sprintId?: string;
@Field({ nullable: true, description: 'Out of their release' })
@IsOptional()
@IsBoolean()
noRelease?: boolean;
@Field(() => ID, { nullable: true })
@IsOptional()
@IsUUID()
releaseId?: string;
@Field(() => IssuePriority, { nullable: true })
@IsOptional()
@IsEnum(IssuePriority)
priority?: IssuePriority;
@Field(() => [String], { nullable: true })
@IsOptional()
@ArrayMaxSize(10)
@IsString({ each: true })
addLabels?: string[];
}
@InputType()
export class AutomationTriggerInput {
@Field(() => AutomationTriggerKind)
@IsEnum(AutomationTriggerKind)
kind!: AutomationTriggerKind;
@Field(() => String, { nullable: true })
@IsOptional()
@Matches(SETTING_ID)
status?: string | null;
}
@InputType()
export class AutomationConditionsInput {
@Field(() => [String], { nullable: true })
@IsOptional()
@Matches(SETTING_ID, { each: true })
types?: string[];
@Field(() => [IssuePriority], { nullable: true })
@IsOptional()
@IsEnum(IssuePriority, { each: true })
priorities?: IssuePriority[];
@Field(() => [String], { nullable: true })
@IsOptional()
@Matches(SETTING_ID, { each: true })
areas?: string[];
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(40)
label?: string | null;
}
@InputType()
export class AutomationActionInput {
@Field(() => AutomationActionKind)
@IsEnum(AutomationActionKind)
kind!: AutomationActionKind;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(2000)
value?: string | null;
}
@InputType()
export class CreateAutomationInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field() @IsString() @MinLength(1) @MaxLength(80) name!: string;
@Field(() => AutomationTriggerInput)
@ValidateNested()
@Type(() => AutomationTriggerInput)
trigger!: AutomationTriggerInput;
@Field(() => AutomationConditionsInput, { nullable: true })
@IsOptional()
@ValidateNested()
@Type(() => AutomationConditionsInput)
conditions?: AutomationConditionsInput;
@Field(() => AutomationActionInput)
@ValidateNested()
@Type(() => AutomationActionInput)
action!: AutomationActionInput;
}
@InputType()
export class CreateWebhookInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field() @IsUrl({ require_tld: false, require_protocol: true }) url!: string;
@Field(() => [String], { description: 'Event names, family.* or *' })
@IsArray()
@ArrayMaxSize(50)
@IsString({ each: true })
events!: string[];
}
@InputType()
export class UpdateWebhookInput {
@Field(() => ID) @IsUUID() webhookId!: string;
@Field({ nullable: true })
@IsOptional()
@IsUrl({ require_tld: false, require_protocol: true })
url?: string;
@Field(() => [String], { nullable: true })
@IsOptional()
@IsArray()
@ArrayMaxSize(50)
@IsString({ each: true })
events?: string[];
@Field({ nullable: true }) @IsOptional() @IsBoolean() active?: boolean;
}
@InputType()
export class RankIssueInput {
@Field(() => ID) @IsUUID() issueId!: string;
@Field(() => ID, {
nullable: true,
description: 'The ticket that ends up right above (null: the top)',
})
@IsOptional()
@IsUUID()
aboveIssueId?: string | null;
@Field(() => ID, {
nullable: true,
description: 'The ticket that ends up right below (null: the bottom)',
})
@IsOptional()
@IsUUID()
belowIssueId?: string | null;
}
@InputType()
export class AddChecklistItemInput {
@Field(() => ID) @IsUUID() issueId!: string;
@Field() @IsString() @MinLength(1) @MaxLength(300) text!: string;
}
@InputType()
export class UpdateChecklistItemInput {
@Field(() => ID) @IsUUID() issueId!: string;
@Field(() => ID) @IsUUID() itemId!: string;
@Field({ nullable: true })
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(300)
text?: string;
@Field({ nullable: true }) @IsOptional() @IsBoolean() done?: boolean;
}
@InputType()
export class LinkIssuesInput {
@Field(() => ID) @IsUUID() issueId!: string;
@Field(() => IssueLinkType) @IsEnum(IssueLinkType) type!: IssueLinkType;
@Field(() => ID) @IsUUID() targetIssueId!: string;
}
@InputType()
export class IssueFilterInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field(() => [String], { nullable: true })
@IsOptional()
@IsString({ each: true })
statuses?: string[];
@Field(() => [StatusCategory], { nullable: true })
@IsOptional()
@IsEnum(StatusCategory, { each: true })
categories?: StatusCategory[];
@Field(() => [String], { nullable: true })
@IsOptional()
@IsString({ each: true })
types?: string[];
@Field(() => [String], { nullable: true })
@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 })
priorities?: IssuePriority[];
@Field(() => ID, { nullable: true })
@IsOptional()
@IsString()
assigneeId?: string;
@Field({ nullable: true }) @IsOptional() @IsBoolean() unassigned?: boolean;
@Field(() => ID, { nullable: true })
@IsOptional()
@IsUUID()
releaseId?: string;
@Field(() => [String], { nullable: true })
@IsOptional()
@IsString({ each: true })
labels?: string[];
@Field(() => ID, {
nullable: true,
description: 'Only the tickets hanging from this one',
})
@IsOptional()
@IsUUID()
parentId?: string;
@Field(() => ID, { nullable: true })
@IsOptional()
@IsUUID()
sprintId?: string;
@Field({ nullable: true, description: 'Only tickets in no sprint' })
@IsOptional()
@IsBoolean()
noSprint?: boolean;
@Field({
nullable: true,
description: 'Only the tickets I follow (none when anonymous)',
})
@IsOptional()
@IsBoolean()
watching?: boolean;
@Field({
nullable: true,
description: 'Due on or after this day (YYYY-MM-DD)',
})
@IsOptional()
@Matches(/^\d{4}-\d{2}-\d{2}$/)
dueFrom?: string;
@Field({
nullable: true,
description: 'Due on or before this day (YYYY-MM-DD)',
})
@IsOptional()
@Matches(/^\d{4}-\d{2}-\d{2}$/)
dueTo?: string;
@Field({ nullable: true, description: 'Only tickets nobody estimated' })
@IsOptional()
@IsBoolean()
noEstimate?: boolean;
@Field({ nullable: true, description: 'Only tickets with no area' })
@IsOptional()
@IsBoolean()
noArea?: boolean;
@Field({
nullable: true,
description: 'Only tickets not updated since this instant (ISO)',
})
@IsOptional()
@IsISO8601()
updatedBefore?: string;
@Field(() => [CustomFieldValueInput], {
nullable: true,
description: 'Only tickets holding these custom values',
})
@IsOptional()
@IsArray()
@ArrayMaxSize(30)
@ValidateNested({ each: true })
@Type(() => CustomFieldValueInput)
customFields?: CustomFieldValueInput[];
@Field(() => IssueSort, { nullable: true, defaultValue: IssueSort.RECENT })
@IsOptional()
@IsEnum(IssueSort)
sort?: IssueSort;
@Field({ nullable: true, description: 'Matches key, title or description' })
@IsOptional()
@IsString()
@MaxLength(200)
search?: string;
}
@InputType()
export class CreateReleaseInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field({ description: 'e.g. v1.4.0' })
@IsString()
@MinLength(1)
@MaxLength(60)
name!: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(5000)
description?: string | null;
@Field(() => String, { nullable: true })
@IsOptional()
@IsISO8601()
targetDate?: string | null;
}
@InputType()
export class UpdateReleaseInput {
@Field(() => ID) @IsUUID() releaseId!: string;
@Field({ nullable: true })
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(60)
name?: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(5000)
description?: string | null;
@Field(() => String, { nullable: true })
@IsOptional()
@IsISO8601()
targetDate?: string | null;
}
/* Project settings (owners and admins). Values are archived, never deleted. */
@InputType()
export class WorkflowStatusInput {
@Field() @Matches(SETTING_ID) id!: string;
@Field() @IsString() @MinLength(1) @MaxLength(40) name!: string;
@Field(() => StatusCategory)
@IsEnum(StatusCategory)
category!: StatusCategory;
@Field() @IsString() color!: string;
@Field({ defaultValue: false }) @IsBoolean() requiresReason!: boolean;
@Field({ defaultValue: false }) @IsBoolean() archived!: boolean;
@Field(() => Int, {
nullable: true,
description: 'Work-in-progress limit (null: none)',
})
@IsOptional()
@IsInt()
@Min(1)
@Max(999)
wipLimit?: number | null;
}
@InputType()
export class StatusTransitionsInput {
@Field() @Matches(SETTING_ID) from!: string;
@Field(() => [String])
@IsArray()
@ArrayMaxSize(50)
@Matches(SETTING_ID, { each: true })
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;
@Field() @IsString() @MinLength(1) @MaxLength(40) name!: string;
@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()
export class AreaOptionInput {
@Field() @Matches(SETTING_ID) id!: string;
@Field() @IsString() @MinLength(1) @MaxLength(40) name!: string;
@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;
@Field() @IsString() @MinLength(1) @MaxLength(40) name!: string;
@Field(() => CustomFieldType) @IsEnum(CustomFieldType) type!: CustomFieldType;
@Field(() => [String], {
defaultValue: [],
description: 'The choices of a SELECT field',
})
@IsArray()
@ArrayMaxSize(50)
@IsString({ each: true })
@MaxLength(60, { each: true })
options!: string[];
@Field({ defaultValue: false }) @IsBoolean() archived!: boolean;
}
@InputType({
description:
'The whole configuration (replaces the previous one). Statuses go in board order.',
})
export class ProjectSettingsInput {
@Field(() => [WorkflowStatusInput])
@IsArray()
@ArrayMaxSize(50)
@ValidateNested({ each: true })
@Type(() => WorkflowStatusInput)
statuses!: WorkflowStatusInput[];
@Field(() => [StatusTransitionsInput])
@IsArray()
@ArrayMaxSize(50)
@ValidateNested({ each: true })
@Type(() => StatusTransitionsInput)
transitions!: StatusTransitionsInput[];
@Field(() => [IssueTypeOptionInput])
@IsArray()
@ArrayMaxSize(50)
@ValidateNested({ each: true })
@Type(() => IssueTypeOptionInput)
issueTypes!: IssueTypeOptionInput[];
@Field(() => [AreaOptionInput])
@IsArray()
@ArrayMaxSize(50)
@ValidateNested({ each: true })
@Type(() => AreaOptionInput)
areas!: AreaOptionInput[];
@Field(() => [CustomFieldDefinitionInput], {
nullable: true,
description: 'Absent: unchanged. Fields cannot be removed: archive them.',
})
@IsOptional()
@IsArray()
@ArrayMaxSize(30)
@ValidateNested({ each: true })
@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:
'Definition of done: DONE only with the checklist ticked. Absent: unchanged',
})
@IsOptional()
@IsBoolean()
checklistBeforeDone?: boolean;
}
@InputType({
description: "A browser's Web Push subscription (PushSubscription.toJSON())",
})
export class PushSubscriptionInput {
@Field()
@IsUrl({ protocols: ['https'], require_tld: false })
endpoint!: string;
@Field() @IsString() @MaxLength(200) p256dh!: string;
@Field() @IsString() @MaxLength(100) auth!: string;
}
@InputType()
export class MyViewCriteriaInput {
@Field(() => [ID], { nullable: true, description: 'Empty: all my projects' })
@IsOptional()
@IsArray()
@ArrayMaxSize(50)
@IsUUID('all', { each: true })
projectIds?: string[];
@Field({ nullable: true }) @IsOptional() @IsBoolean() assignedToMe?: boolean;
@Field({ nullable: true }) @IsOptional() @IsBoolean() watching?: boolean;
@Field(() => [StatusCategory], { nullable: true })
@IsOptional()
@IsEnum(StatusCategory, { each: true })
categories?: StatusCategory[];
@Field(() => [IssuePriority], { nullable: true })
@IsOptional()
@IsEnum(IssuePriority, { each: true })
priorities?: IssuePriority[];
@Field({ nullable: true }) @IsOptional() @IsBoolean() bugsOnly?: boolean;
@Field({ nullable: true }) @IsOptional() @IsBoolean() overdue?: boolean;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(200)
search?: string | null;
}
@InputType()
export class SaveMyViewInput {
@Field() @IsString() @MinLength(1) @MaxLength(60) name!: string;
@Field(() => MyViewCriteriaInput)
@ValidateNested()
@Type(() => MyViewCriteriaInput)
criteria!: MyViewCriteriaInput;
}
@InputType({ description: "The project's link for clients" })
export class ShareDashboardInput {
@Field(() => ID) @IsUUID() projectId!: string;
@Field(() => [String], {
description: 'Blocks to show: flow, sprint, releases, epics, activity',
})
@IsArray()
@ArrayMaxSize(10)
@IsString({ each: true })
blocks!: string[];
@Field({
nullable: true,
description: 'A new secret: the old link stops working',
})
@IsOptional()
@IsBoolean()
renew?: boolean;
}
@InputType()
export class AlertSettingsInput {
@Field(() => ID, {
nullable: true,
description: "Ticket type to open; null: the project's first bug type",
})
@IsOptional()
@IsString()
typeId?: string | null;
@Field(() => IssuePriority, { nullable: true })
@IsOptional()
@IsEnum(IssuePriority)
priority?: IssuePriority;
}