Creating a project can name one the creator owns or administers: its statuses, transitions, types, areas, custom fields and automation rules are copied once (paused rules stay paused; rules assigning to someone who is not in the new project are left out). GraphQL templateProjectId, REST template (id or key), MCP create_project template, and a select in the web's new-project form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
772 lines
20 KiB
TypeScript
772 lines
20 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(() => 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(() => [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(() => [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 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[];
|
|
}
|
|
|
|
@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;
|
|
}
|