1 parent
afc37f8fdb
commit
c5691da2b8
47 files changed
+278
-9
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "issue" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'TASK';
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "issue_projectId_type_idx" ON "issue"("projectId", "type");
|
||||||
@@ -88,6 +88,7 @@ model Issue {
|
|||||||
content String?
|
content String?
|
||||||
status String
|
status String
|
||||||
priority Int @default(0)
|
priority Int @default(0)
|
||||||
|
type String @default("TASK")
|
||||||
assigneeId String?
|
assigneeId String?
|
||||||
assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: SetNull)
|
assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: SetNull)
|
||||||
reporterId String?
|
reporterId String?
|
||||||
@@ -109,6 +110,7 @@ model Issue {
|
|||||||
@@index([projectId, createdAt])
|
@@index([projectId, createdAt])
|
||||||
@@index([dueDate])
|
@@index([dueDate])
|
||||||
@@index([projectId, dueDate])
|
@@index([projectId, dueDate])
|
||||||
|
@@index([projectId, type])
|
||||||
@@map("issue")
|
@@map("issue")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IssueModule } from './modules/issue/issue.module.js';
|
|||||||
import { ProjectModule } from './modules/project/project.module.js';
|
import { ProjectModule } from './modules/project/project.module.js';
|
||||||
import { UserModule } from './modules/user/user.module.js';
|
import { UserModule } from './modules/user/user.module.js';
|
||||||
import { StatusModule } from './modules/status/status.module.js';
|
import { StatusModule } from './modules/status/status.module.js';
|
||||||
|
import { IssueTypeModule } from './modules/issue-type/issue-type.module.js';
|
||||||
import { NotificationModule } from './modules/notification/notification.module.js';
|
import { NotificationModule } from './modules/notification/notification.module.js';
|
||||||
import { CommentModule } from './modules/comment/comment.module.js';
|
import { CommentModule } from './modules/comment/comment.module.js';
|
||||||
import { FileModule } from './modules/file/file.module.js';
|
import { FileModule } from './modules/file/file.module.js';
|
||||||
@@ -52,6 +53,7 @@ import { join } from 'node:path';
|
|||||||
ProjectModule,
|
ProjectModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
StatusModule,
|
StatusModule,
|
||||||
|
IssueTypeModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
CommentModule,
|
CommentModule,
|
||||||
FileModule,
|
FileModule,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const buildIssue = () =>
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
+1
@@ -19,6 +19,7 @@ const buildIssue = () =>
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ISSUE_TYPES } from '../../../../issue/domain/value-objects/issue-type.vo.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ListIssueTypesUseCase {
|
||||||
|
execute(): typeof ISSUE_TYPES {
|
||||||
|
return ISSUE_TYPES;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { IssueTypeController } from './presentation/controllers/issue-type.controller.js';
|
||||||
|
import { IssueTypeResolver } from './presentation/resolvers/issue-type.resolver.js';
|
||||||
|
import { ListIssueTypesUseCase } from './application/use-cases/list-issue-types/list-issue-types.use-case.js';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [IssueTypeController],
|
||||||
|
providers: [ListIssueTypesUseCase, IssueTypeResolver],
|
||||||
|
})
|
||||||
|
export class IssueTypeModule {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ListIssueTypesUseCase } from '../../application/use-cases/list-issue-types/list-issue-types.use-case.js';
|
||||||
|
import { ISSUE_TYPES } from '../../../issue/domain/value-objects/issue-type.vo.js';
|
||||||
|
|
||||||
|
@Controller('issue-types')
|
||||||
|
export class IssueTypeController {
|
||||||
|
constructor(private readonly listIssueTypesUseCase: ListIssueTypesUseCase) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
list(): typeof ISSUE_TYPES {
|
||||||
|
return this.listIssueTypesUseCase.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
/** Не `IssueType` — это имя занято value-объектом типа задачи */
|
||||||
|
@ObjectType('IssueTypeDefinition')
|
||||||
|
export class IssueTypeDefinitionType {
|
||||||
|
@Field()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
alias: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Query, Resolver } from '@nestjs/graphql';
|
||||||
|
import { ListIssueTypesUseCase } from '../../application/use-cases/list-issue-types/list-issue-types.use-case.js';
|
||||||
|
import { ISSUE_TYPES } from '../../../issue/domain/value-objects/issue-type.vo.js';
|
||||||
|
import { IssueTypeDefinitionType } from '../graphql/issue-type-definition.type.js';
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class IssueTypeResolver {
|
||||||
|
constructor(private readonly listIssueTypesUseCase: ListIssueTypesUseCase) {}
|
||||||
|
|
||||||
|
@Query(() => [IssueTypeDefinitionType], { name: 'issueTypes' })
|
||||||
|
list(): typeof ISSUE_TYPES {
|
||||||
|
return this.listIssueTypesUseCase.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export class IssueWithUsersDto {
|
|||||||
content: string | null;
|
content: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
type: string;
|
||||||
assigneeId: string | null;
|
assigneeId: string | null;
|
||||||
reporterId: string | null;
|
reporterId: string | null;
|
||||||
assignee: UserDto | null;
|
assignee: UserDto | null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export class IssueDto {
|
|||||||
content: string | null;
|
content: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
type: string;
|
||||||
assigneeId: string | null;
|
assigneeId: string | null;
|
||||||
reporterId: string | null;
|
reporterId: string | null;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export class IssueApplicationMapper {
|
|||||||
content: issue.content,
|
content: issue.content,
|
||||||
status: issue.status,
|
status: issue.status,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
|
type: issue.type,
|
||||||
assigneeId: issue.assignee,
|
assigneeId: issue.assignee,
|
||||||
reporterId: issue.reporter,
|
reporterId: issue.reporter,
|
||||||
projectId: issue.projectId,
|
projectId: issue.projectId,
|
||||||
@@ -30,6 +31,7 @@ export class IssueApplicationMapper {
|
|||||||
content: issue.content,
|
content: issue.content,
|
||||||
status: issue.status,
|
status: issue.status,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
|
type: issue.type,
|
||||||
assigneeId: issue.assignee,
|
assigneeId: issue.assignee,
|
||||||
reporterId: issue.reporter,
|
reporterId: issue.reporter,
|
||||||
assignee: issue.assignee ? (usersById.get(issue.assignee) ?? null) : null,
|
assignee: issue.assignee ? (usersById.get(issue.assignee) ?? null) : null,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const buildIssue = (overrides: Partial<{ assignee: string | null; reporter: stri
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: 'assignee' in overrides ? (overrides.assignee ?? null) : 'user-2',
|
assignee: 'assignee' in overrides ? (overrides.assignee ?? null) : 'user-2',
|
||||||
reporter: overrides.reporter ?? 'user-1',
|
reporter: overrides.reporter ?? 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ export class CreateIssueCommand {
|
|||||||
public readonly priority?: number,
|
public readonly priority?: number,
|
||||||
public readonly assignee?: string,
|
public readonly assignee?: string,
|
||||||
public readonly dueDate?: Date,
|
public readonly dueDate?: Date,
|
||||||
|
public readonly type?: string,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,7 @@ describe('CreateIssueUseCase', () => {
|
|||||||
content: issue.content,
|
content: issue.content,
|
||||||
status: issue.status,
|
status: issue.status,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
|
type: issue.type,
|
||||||
assignee: issue.assignee,
|
assignee: issue.assignee,
|
||||||
reporter: issue.reporter,
|
reporter: issue.reporter,
|
||||||
projectId: issue.projectId,
|
projectId: issue.projectId,
|
||||||
@@ -82,6 +83,7 @@ describe('CreateIssueUseCase', () => {
|
|||||||
title: 'Fix login bug',
|
title: 'Fix login bug',
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
reporterId: 'user-1',
|
reporterId: 'user-1',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -113,6 +115,7 @@ describe('CreateIssueUseCase', () => {
|
|||||||
content: issue.content,
|
content: issue.content,
|
||||||
status: issue.status,
|
status: issue.status,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
|
type: issue.type,
|
||||||
assignee: issue.assignee,
|
assignee: issue.assignee,
|
||||||
reporter: issue.reporter,
|
reporter: issue.reporter,
|
||||||
projectId: issue.projectId,
|
projectId: issue.projectId,
|
||||||
@@ -161,4 +164,26 @@ describe('CreateIssueUseCase', () => {
|
|||||||
|
|
||||||
expect(notificationsService.notify).not.toHaveBeenCalled();
|
expect(notificationsService.notify).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates a TASK by default and accepts an explicit type', async () => {
|
||||||
|
vi.mocked(repository.create).mockImplementation(async (issue) => issue);
|
||||||
|
|
||||||
|
const task = await useCase.execute(new CreateIssueCommand('ISSUE-4', 'Something', 'user-1', 'project-1'));
|
||||||
|
const bug = await useCase.execute(
|
||||||
|
new CreateIssueCommand(
|
||||||
|
'ISSUE-5',
|
||||||
|
'Crash on start',
|
||||||
|
'user-1',
|
||||||
|
'project-1',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'BUG',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(task.type).toBe('TASK');
|
||||||
|
expect(bug.type).toBe('BUG');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -30,6 +30,7 @@ export class CreateIssueUseCase {
|
|||||||
title: command.title,
|
title: command.title,
|
||||||
content: command.content,
|
content: command.content,
|
||||||
priority: command.priority,
|
priority: command.priority,
|
||||||
|
type: command.type,
|
||||||
assignee: command.assignee,
|
assignee: command.assignee,
|
||||||
reporter: command.reporter,
|
reporter: command.reporter,
|
||||||
projectId: command.projectId,
|
projectId: command.projectId,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const buildIssue = () =>
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
+1
@@ -15,6 +15,7 @@ const buildIssue = () =>
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: 'user-2',
|
assignee: 'user-2',
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const buildIssue = () =>
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: 'user-2',
|
assignee: 'user-2',
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
+1
@@ -18,6 +18,7 @@ const buildIssue = (id: number) =>
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ export class ListIssuesQuery {
|
|||||||
public readonly text?: string,
|
public readonly text?: string,
|
||||||
public readonly assigneeId?: string,
|
public readonly assigneeId?: string,
|
||||||
public readonly hasDueDate?: boolean,
|
public readonly hasDueDate?: boolean,
|
||||||
|
public readonly type?: string[],
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ const buildIssue = (id: number) =>
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class ListIssuesUseCase {
|
|||||||
text: query.text,
|
text: query.text,
|
||||||
assigneeId: query.assigneeId,
|
assigneeId: query.assigneeId,
|
||||||
hasDueDate: query.hasDueDate,
|
hasDueDate: query.hasDueDate,
|
||||||
|
type: query.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { list: await this.issueUserExpander.expandAll(list), meta };
|
return { list: await this.issueUserExpander.expandAll(list), meta };
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ export class UpdateIssueCommand {
|
|||||||
public readonly priority?: number,
|
public readonly priority?: number,
|
||||||
public readonly assignee?: string | null,
|
public readonly assignee?: string | null,
|
||||||
public readonly dueDate?: Date | null,
|
public readonly dueDate?: Date | null,
|
||||||
|
public readonly type?: string,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@ const buildIssue = () =>
|
|||||||
content: 'Original content',
|
content: 'Original content',
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
@@ -155,4 +156,14 @@ describe('UpdateIssueUseCase', () => {
|
|||||||
|
|
||||||
expect(notificationsService.notify).not.toHaveBeenCalled();
|
expect(notificationsService.notify).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('changes the issue type', async () => {
|
||||||
|
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
|
||||||
|
|
||||||
|
const dto = await useCase.execute(
|
||||||
|
new UpdateIssueCommand(1, 'user-1', undefined, undefined, undefined, undefined, undefined, undefined, 'BUG'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(dto.type).toBe('BUG');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -47,6 +47,10 @@ export class UpdateIssueUseCase {
|
|||||||
issue.changePriority(command.priority);
|
issue.changePriority(command.priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.type !== undefined) {
|
||||||
|
issue.changeType(command.type);
|
||||||
|
}
|
||||||
|
|
||||||
if (command.assignee !== undefined) {
|
if (command.assignee !== undefined) {
|
||||||
if (command.assignee === null || command.assignee === '') {
|
if (command.assignee === null || command.assignee === '') {
|
||||||
issue.unassign();
|
issue.unassign();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const ISSUE_CHANGE_FIELDS = [
|
|||||||
'content',
|
'content',
|
||||||
'status',
|
'status',
|
||||||
'priority',
|
'priority',
|
||||||
|
'type',
|
||||||
'assignee',
|
'assignee',
|
||||||
'dueDate',
|
'dueDate',
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ describe('Issue', () => {
|
|||||||
expect(issue.content).toBeNull();
|
expect(issue.content).toBeNull();
|
||||||
expect(issue.status).toBe('OPEN');
|
expect(issue.status).toBe('OPEN');
|
||||||
expect(issue.priority).toBe(0);
|
expect(issue.priority).toBe(0);
|
||||||
|
expect(issue.type).toBe('TASK');
|
||||||
expect(issue.assignee).toBeNull();
|
expect(issue.assignee).toBeNull();
|
||||||
expect(issue.reporter).toBe('user-1');
|
expect(issue.reporter).toBe('user-1');
|
||||||
});
|
});
|
||||||
@@ -31,11 +32,13 @@ describe('Issue', () => {
|
|||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
content: 'Details here',
|
content: 'Details here',
|
||||||
priority: 3,
|
priority: 3,
|
||||||
|
type: 'BUG',
|
||||||
assignee: 'user-2',
|
assignee: 'user-2',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(issue.content).toBe('Details here');
|
expect(issue.content).toBe('Details here');
|
||||||
expect(issue.priority).toBe(3);
|
expect(issue.priority).toBe(3);
|
||||||
|
expect(issue.type).toBe('BUG');
|
||||||
expect(issue.assignee).toBe('user-2');
|
expect(issue.assignee).toBe('user-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,6 +73,7 @@ describe('Issue', () => {
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'IN_PROGRESS',
|
status: 'IN_PROGRESS',
|
||||||
priority: 2,
|
priority: 2,
|
||||||
|
type: 'TASK',
|
||||||
assignee: 'user-3',
|
assignee: 'user-3',
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
@@ -93,6 +97,7 @@ describe('Issue', () => {
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
@@ -139,6 +144,15 @@ describe('Issue', () => {
|
|||||||
expect(() => issue.changePriority(10)).toThrow();
|
expect(() => issue.changePriority(10)).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('changes the type through the value object rules', () => {
|
||||||
|
const issue = build();
|
||||||
|
|
||||||
|
issue.changeType('FEATURE');
|
||||||
|
expect(issue.type).toBe('FEATURE');
|
||||||
|
|
||||||
|
expect(() => issue.changeType('EPIC')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('assigns and unassigns an issue', () => {
|
it('assigns and unassigns an issue', () => {
|
||||||
const issue = build();
|
const issue = build();
|
||||||
|
|
||||||
@@ -169,6 +183,7 @@ describe('Issue', () => {
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
reporter: 'user-1',
|
reporter: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
@@ -193,6 +208,7 @@ describe('Issue', () => {
|
|||||||
issue.updateContent('Text');
|
issue.updateContent('Text');
|
||||||
issue.changeStatus('IN_PROGRESS');
|
issue.changeStatus('IN_PROGRESS');
|
||||||
issue.changePriority(3);
|
issue.changePriority(3);
|
||||||
|
issue.changeType('BUG');
|
||||||
issue.assignTo('user-2');
|
issue.assignTo('user-2');
|
||||||
issue.updateDueDate(new Date('2026-02-01T00:00:00.000Z'));
|
issue.updateDueDate(new Date('2026-02-01T00:00:00.000Z'));
|
||||||
|
|
||||||
@@ -201,6 +217,7 @@ describe('Issue', () => {
|
|||||||
{ field: 'content', oldValue: null, newValue: 'Text' },
|
{ field: 'content', oldValue: null, newValue: 'Text' },
|
||||||
{ field: 'status', oldValue: 'OPEN', newValue: 'IN_PROGRESS' },
|
{ field: 'status', oldValue: 'OPEN', newValue: 'IN_PROGRESS' },
|
||||||
{ field: 'priority', oldValue: '0', newValue: '3' },
|
{ field: 'priority', oldValue: '0', newValue: '3' },
|
||||||
|
{ field: 'type', oldValue: 'TASK', newValue: 'BUG' },
|
||||||
{ field: 'assignee', oldValue: null, newValue: 'user-2' },
|
{ field: 'assignee', oldValue: null, newValue: 'user-2' },
|
||||||
{ field: 'dueDate', oldValue: null, newValue: '2026-02-01T00:00:00.000Z' },
|
{ field: 'dueDate', oldValue: null, newValue: '2026-02-01T00:00:00.000Z' },
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IssueTitle } from '../value-objects/issue-title.vo.js';
|
import { IssueTitle } from '../value-objects/issue-title.vo.js';
|
||||||
import { IssueStatus } from '../value-objects/issue-status.vo.js';
|
import { IssueStatus } from '../value-objects/issue-status.vo.js';
|
||||||
import { IssuePriority } from '../value-objects/issue-priority.vo.js';
|
import { IssuePriority } from '../value-objects/issue-priority.vo.js';
|
||||||
|
import { IssueType } from '../value-objects/issue-type.vo.js';
|
||||||
import { IssueCreatedEvent } from '../events/issue-created.event.js';
|
import { IssueCreatedEvent } from '../events/issue-created.event.js';
|
||||||
import { IssueUpdatedEvent } from '../events/issue-updated.event.js';
|
import { IssueUpdatedEvent } from '../events/issue-updated.event.js';
|
||||||
import { IssueAssignedEvent } from '../events/issue-assigned.event.js';
|
import { IssueAssignedEvent } from '../events/issue-assigned.event.js';
|
||||||
@@ -13,6 +14,7 @@ interface IssueProps {
|
|||||||
content: string | null;
|
content: string | null;
|
||||||
status: IssueStatus;
|
status: IssueStatus;
|
||||||
priority: IssuePriority;
|
priority: IssuePriority;
|
||||||
|
type: IssueType;
|
||||||
assignee: string | null;
|
assignee: string | null;
|
||||||
/** null — автор задачи удалил аккаунт */
|
/** null — автор задачи удалил аккаунт */
|
||||||
reporter: string | null;
|
reporter: string | null;
|
||||||
@@ -46,6 +48,7 @@ export class Issue {
|
|||||||
title: string;
|
title: string;
|
||||||
content?: string | null;
|
content?: string | null;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
type?: string;
|
||||||
assignee?: string | null;
|
assignee?: string | null;
|
||||||
reporter: string;
|
reporter: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -58,6 +61,7 @@ export class Issue {
|
|||||||
content: params.content ?? null,
|
content: params.content ?? null,
|
||||||
status: IssueStatus.open(),
|
status: IssueStatus.open(),
|
||||||
priority: params.priority !== undefined ? IssuePriority.create(params.priority) : IssuePriority.default(),
|
priority: params.priority !== undefined ? IssuePriority.create(params.priority) : IssuePriority.default(),
|
||||||
|
type: params.type !== undefined ? IssueType.create(params.type) : IssueType.default(),
|
||||||
assignee: params.assignee ?? null,
|
assignee: params.assignee ?? null,
|
||||||
reporter: params.reporter,
|
reporter: params.reporter,
|
||||||
projectId: params.projectId,
|
projectId: params.projectId,
|
||||||
@@ -84,6 +88,7 @@ export class Issue {
|
|||||||
content: string | null;
|
content: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
type: string;
|
||||||
assignee: string | null;
|
assignee: string | null;
|
||||||
reporter: string | null;
|
reporter: string | null;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -98,6 +103,7 @@ export class Issue {
|
|||||||
content: props.content,
|
content: props.content,
|
||||||
status: IssueStatus.create(props.status),
|
status: IssueStatus.create(props.status),
|
||||||
priority: IssuePriority.create(props.priority),
|
priority: IssuePriority.create(props.priority),
|
||||||
|
type: IssueType.create(props.type),
|
||||||
assignee: props.assignee,
|
assignee: props.assignee,
|
||||||
reporter: props.reporter,
|
reporter: props.reporter,
|
||||||
projectId: props.projectId,
|
projectId: props.projectId,
|
||||||
@@ -131,6 +137,10 @@ export class Issue {
|
|||||||
return this.props.priority.value;
|
return this.props.priority.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get type(): string {
|
||||||
|
return this.props.type.value;
|
||||||
|
}
|
||||||
|
|
||||||
get assignee(): string | null {
|
get assignee(): string | null {
|
||||||
return this.props.assignee;
|
return this.props.assignee;
|
||||||
}
|
}
|
||||||
@@ -178,6 +188,12 @@ export class Issue {
|
|||||||
this.props.priority = next;
|
this.props.priority = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeType(type: string): void {
|
||||||
|
const next = IssueType.create(type);
|
||||||
|
this.recordChange('type', this.type, next.value);
|
||||||
|
this.props.type = next;
|
||||||
|
}
|
||||||
|
|
||||||
assignTo(userId: string): void {
|
assignTo(userId: string): void {
|
||||||
if (this.assignee === userId) return;
|
if (this.assignee === userId) return;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface IIssueRepository {
|
|||||||
assigneeId?: string;
|
assigneeId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
hasDueDate?: boolean;
|
hasDueDate?: boolean;
|
||||||
|
type?: string[];
|
||||||
}): Promise<{ list: Issue[]; meta: PaginationMeta }>;
|
}): Promise<{ list: Issue[]; meta: PaginationMeta }>;
|
||||||
/** Задачи с dueDate в [from, to), по возрастанию dueDate, не больше limit */
|
/** Задачи с dueDate в [from, to), по возрастанию dueDate, не больше limit */
|
||||||
findByDueDateRange(params: {
|
findByDueDateRange(params: {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { IssueType, ISSUE_TYPES } from './issue-type.vo.js';
|
||||||
|
|
||||||
|
describe('IssueType', () => {
|
||||||
|
it.each(ISSUE_TYPES.map((type) => type.alias))('accepts the valid type "%s"', (alias) => {
|
||||||
|
expect(IssueType.create(alias).value).toBe(alias);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for an unknown type', () => {
|
||||||
|
expect(() => IssueType.create('EPIC')).toThrow(/Недопустимый тип задачи/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to TASK', () => {
|
||||||
|
expect(IssueType.default().value).toBe('TASK');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('compares two types by value', () => {
|
||||||
|
expect(IssueType.create('BUG').equals(IssueType.create('BUG'))).toBe(true);
|
||||||
|
expect(IssueType.create('BUG').equals(IssueType.create('TASK'))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export const ISSUE_TYPES = [
|
||||||
|
{ name: 'Task', alias: 'TASK', color: 'primary' },
|
||||||
|
{ name: 'Bug', alias: 'BUG', color: 'error' },
|
||||||
|
{ name: 'Feature', alias: 'FEATURE', color: 'success' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type IssueTypeValue = (typeof ISSUE_TYPES)[number]['alias'];
|
||||||
|
|
||||||
|
export const ISSUE_TYPE_ALIASES: readonly IssueTypeValue[] = ISSUE_TYPES.map((type) => type.alias);
|
||||||
|
|
||||||
|
export class IssueType {
|
||||||
|
private constructor(private readonly _value: IssueTypeValue) {}
|
||||||
|
|
||||||
|
static create(value: string): IssueType {
|
||||||
|
if (!ISSUE_TYPE_ALIASES.includes(value as IssueTypeValue)) {
|
||||||
|
throw new Error(`Недопустимый тип задачи: "${value}". Разрешены: ${ISSUE_TYPE_ALIASES.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new IssueType(value as IssueTypeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
static default(): IssueType {
|
||||||
|
return new IssueType('TASK');
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): IssueTypeValue {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(other: IssueType): boolean {
|
||||||
|
return this._value === other.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ describe('IssuePrismaMapper', () => {
|
|||||||
content: 'Details',
|
content: 'Details',
|
||||||
status: 'IN_PROGRESS',
|
status: 'IN_PROGRESS',
|
||||||
priority: 2,
|
priority: 2,
|
||||||
|
type: 'BUG',
|
||||||
assigneeId: 'user-2',
|
assigneeId: 'user-2',
|
||||||
reporterId: 'user-1',
|
reporterId: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
@@ -26,6 +27,7 @@ describe('IssuePrismaMapper', () => {
|
|||||||
expect(issue.title).toBe('Fix login bug');
|
expect(issue.title).toBe('Fix login bug');
|
||||||
expect(issue.status).toBe('IN_PROGRESS');
|
expect(issue.status).toBe('IN_PROGRESS');
|
||||||
expect(issue.priority).toBe(2);
|
expect(issue.priority).toBe(2);
|
||||||
|
expect(issue.type).toBe('BUG');
|
||||||
expect(issue.assignee).toBe('user-2');
|
expect(issue.assignee).toBe('user-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@ describe('IssuePrismaMapper', () => {
|
|||||||
content: null,
|
content: null,
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
type: 'TASK',
|
||||||
assigneeId: null,
|
assigneeId: null,
|
||||||
reporterId: 'user-1',
|
reporterId: 'user-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
@@ -61,6 +64,7 @@ describe('IssuePrismaMapper', () => {
|
|||||||
title: 'Updated title',
|
title: 'Updated title',
|
||||||
status: 'IN_PROGRESS',
|
status: 'IN_PROGRESS',
|
||||||
priority: 2,
|
priority: 2,
|
||||||
|
type: 'BUG',
|
||||||
assigneeId: 'user-2',
|
assigneeId: 'user-2',
|
||||||
});
|
});
|
||||||
expect(input).not.toHaveProperty('id');
|
expect(input).not.toHaveProperty('id');
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class IssuePrismaMapper {
|
|||||||
content: record.content,
|
content: record.content,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
priority: record.priority,
|
priority: record.priority,
|
||||||
|
type: record.type,
|
||||||
assignee: record.assigneeId,
|
assignee: record.assigneeId,
|
||||||
reporter: record.reporterId,
|
reporter: record.reporterId,
|
||||||
projectId: record.projectId,
|
projectId: record.projectId,
|
||||||
@@ -27,6 +28,7 @@ export class IssuePrismaMapper {
|
|||||||
content: issue.content,
|
content: issue.content,
|
||||||
status: issue.status,
|
status: issue.status,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
|
type: issue.type,
|
||||||
assigneeId: issue.assignee,
|
assigneeId: issue.assignee,
|
||||||
reporterId: issue.reporter,
|
reporterId: issue.reporter,
|
||||||
projectId: issue.projectId,
|
projectId: issue.projectId,
|
||||||
@@ -42,6 +44,7 @@ export class IssuePrismaMapper {
|
|||||||
content: issue.content,
|
content: issue.content,
|
||||||
status: issue.status,
|
status: issue.status,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
|
type: issue.type,
|
||||||
assigneeId: issue.assignee,
|
assigneeId: issue.assignee,
|
||||||
dueDate: issue.dueDate,
|
dueDate: issue.dueDate,
|
||||||
updatedAt: issue.updatedAt,
|
updatedAt: issue.updatedAt,
|
||||||
|
|||||||
+2
@@ -73,6 +73,7 @@ export class IssuePrismaRepository implements IIssueRepository {
|
|||||||
assigneeId?: string;
|
assigneeId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
hasDueDate?: boolean;
|
hasDueDate?: boolean;
|
||||||
|
type?: string[];
|
||||||
}): Promise<{ list: Issue[]; meta: PaginationMeta }> {
|
}): Promise<{ list: Issue[]; meta: PaginationMeta }> {
|
||||||
const page = Math.max(1, params?.page);
|
const page = Math.max(1, params?.page);
|
||||||
const limit = Math.max(1, params?.limit);
|
const limit = Math.max(1, params?.limit);
|
||||||
@@ -81,6 +82,7 @@ export class IssuePrismaRepository implements IIssueRepository {
|
|||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
...commonFilters(params),
|
...commonFilters(params),
|
||||||
|
...(params.type?.length ? [{ type: { in: params.type } }] : []),
|
||||||
...(params.hasDueDate !== undefined ? [{ dueDate: params.hasDueDate ? { not: null } : null }] : []),
|
...(params.hasDueDate !== undefined ? [{ dueDate: params.hasDueDate ? { not: null } : null }] : []),
|
||||||
...(params.text
|
...(params.text
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export class IssueController {
|
|||||||
dto.priority,
|
dto.priority,
|
||||||
dto.assignee,
|
dto.assignee,
|
||||||
dto.dueDate !== undefined ? new Date(dto.dueDate) : undefined,
|
dto.dueDate !== undefined ? new Date(dto.dueDate) : undefined,
|
||||||
|
dto.type,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -87,6 +88,7 @@ export class IssueController {
|
|||||||
query.text,
|
query.text,
|
||||||
query.assigneeId,
|
query.assigneeId,
|
||||||
query.hasDueDate,
|
query.hasDueDate,
|
||||||
|
query.type,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -167,6 +169,7 @@ export class IssueController {
|
|||||||
dto.priority,
|
dto.priority,
|
||||||
dto.assignee,
|
dto.assignee,
|
||||||
dto.dueDate === undefined ? undefined : dto.dueDate === null ? null : new Date(dto.dueDate),
|
dto.dueDate === undefined ? undefined : dto.dueDate === null ? null : new Date(dto.dueDate),
|
||||||
|
dto.type,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IsISO8601, Matches } from 'class-validator';
|
import { IsISO8601, Matches } from 'class-validator';
|
||||||
import { StringDecorator } from '../../../../common/decorators/validators.js';
|
import { StringDecorator } from '../../../../common/decorators/validators.js';
|
||||||
import { IssueStatusListDecorator } from './issue-status-list.decorator.js';
|
import { IssueStatusListDecorator } from './issue-alias-list.decorator.js';
|
||||||
|
|
||||||
/** Без смещения дата трактовалась бы в часовом поясе сервера */
|
/** Без смещения дата трактовалась бы в часовом поясе сервера */
|
||||||
const WITH_OFFSET = /(Z|[+-]\d{2}:\d{2})$/;
|
const WITH_OFFSET = /(Z|[+-]\d{2}:\d{2})$/;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IsDateString, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
import { IsDateString, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
||||||
|
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
|
||||||
|
|
||||||
export class CreateIssueRequestDto {
|
export class CreateIssueRequestDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -24,6 +25,10 @@ export class CreateIssueRequestDto {
|
|||||||
@Max(4)
|
@Max(4)
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(ISSUE_TYPE_ALIASES)
|
||||||
|
type?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
assignee?: string;
|
assignee?: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BooleanDecorator, IntDecorator, StringDecorator } from '../../../../common/decorators/validators.js';
|
import { BooleanDecorator, IntDecorator, StringDecorator } from '../../../../common/decorators/validators.js';
|
||||||
import { IssueStatusListDecorator } from './issue-status-list.decorator.js';
|
import { IssueStatusListDecorator, IssueTypeListDecorator } from './issue-alias-list.decorator.js';
|
||||||
|
|
||||||
export class FilterIssueDto {
|
export class FilterIssueDto {
|
||||||
@IntDecorator({ isOptional: true })
|
@IntDecorator({ isOptional: true })
|
||||||
@@ -17,6 +17,9 @@ export class FilterIssueDto {
|
|||||||
@IssueStatusListDecorator()
|
@IssueStatusListDecorator()
|
||||||
status?: string[];
|
status?: string[];
|
||||||
|
|
||||||
|
@IssueTypeListDecorator()
|
||||||
|
type?: string[];
|
||||||
|
|
||||||
@StringDecorator({ isOptional: true })
|
@StringDecorator({ isOptional: true })
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -2,9 +2,10 @@ import { applyDecorators } from '@nestjs/common';
|
|||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { ArrayNotEmpty, IsArray, IsIn, IsOptional } from 'class-validator';
|
import { ArrayNotEmpty, IsArray, IsIn, IsOptional } from 'class-validator';
|
||||||
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
|
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
|
||||||
|
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
|
||||||
|
|
||||||
/** Comma-separated (`?status=OPEN,IN_PROGRESS`) or repeated (`?status=OPEN&status=IN_PROGRESS`) */
|
/** Comma-separated (`?status=OPEN,IN_PROGRESS`) or repeated (`?status=OPEN&status=IN_PROGRESS`) */
|
||||||
export function IssueStatusListDecorator() {
|
function AliasListDecorator(aliases: readonly string[]) {
|
||||||
return applyDecorators(
|
return applyDecorators(
|
||||||
IsOptional(),
|
IsOptional(),
|
||||||
Transform(({ value }) =>
|
Transform(({ value }) =>
|
||||||
@@ -18,9 +19,15 @@ export function IssueStatusListDecorator() {
|
|||||||
),
|
),
|
||||||
IsArray(),
|
IsArray(),
|
||||||
ArrayNotEmpty(),
|
ArrayNotEmpty(),
|
||||||
IsIn(
|
IsIn(aliases, { each: true }),
|
||||||
ISSUE_STATUSES.map((status) => status.alias),
|
|
||||||
{ each: true },
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IssueStatusListDecorator() {
|
||||||
|
return AliasListDecorator(ISSUE_STATUSES.map((status) => status.alias));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `?type=BUG,FEATURE` или `?type=BUG&type=FEATURE` */
|
||||||
|
export function IssueTypeListDecorator() {
|
||||||
|
return AliasListDecorator(ISSUE_TYPE_ALIASES);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IsDateString, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
import { IsDateString, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
||||||
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
|
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
|
||||||
|
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
|
||||||
|
|
||||||
export class UpdateIssueRequestDto {
|
export class UpdateIssueRequestDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -21,6 +22,10 @@ export class UpdateIssueRequestDto {
|
|||||||
@Max(4)
|
@Max(4)
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(ISSUE_TYPE_ALIASES)
|
||||||
|
type?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
assignee?: string | null;
|
assignee?: string | null;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||||
import { IsDate, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
import { IsDate, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
||||||
|
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class CreateIssueInput {
|
export class CreateIssueInput {
|
||||||
@@ -31,6 +32,11 @@ export class CreateIssueInput {
|
|||||||
@Max(4)
|
@Max(4)
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(ISSUE_TYPE_ALIASES)
|
||||||
|
type?: string;
|
||||||
|
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export class IssueDetailType {
|
|||||||
@Field(() => Int)
|
@Field(() => Int)
|
||||||
priority: number;
|
priority: number;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
type: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
assigneeId: string | null;
|
assigneeId: string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export class IssueType {
|
|||||||
@Field(() => Int)
|
@Field(() => Int)
|
||||||
priority: number;
|
priority: number;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
type: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
assigneeId: string | null;
|
assigneeId: string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||||
import { IsDate, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
import { IsDate, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
||||||
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
|
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
|
||||||
|
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class UpdateIssueInput {
|
export class UpdateIssueInput {
|
||||||
@@ -27,6 +28,11 @@ export class UpdateIssueInput {
|
|||||||
@Max(4)
|
@Max(4)
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(ISSUE_TYPE_ALIASES)
|
||||||
|
type?: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export class IssueResolver {
|
|||||||
@Session() session: UserSession,
|
@Session() session: UserSession,
|
||||||
@Args('projectId', { type: () => String, nullable: true }) projectId?: string,
|
@Args('projectId', { type: () => String, nullable: true }) projectId?: string,
|
||||||
@Args('hasDueDate', { type: () => Boolean, nullable: true }) hasDueDate?: boolean,
|
@Args('hasDueDate', { type: () => Boolean, nullable: true }) hasDueDate?: boolean,
|
||||||
|
@Args('type', { type: () => [String], nullable: true }) type?: string[],
|
||||||
): Promise<{ list: IssueWithUsersDto[]; meta: PaginationMeta }> {
|
): Promise<{ list: IssueWithUsersDto[]; meta: PaginationMeta }> {
|
||||||
return this.listIssuesUseCase.execute(
|
return this.listIssuesUseCase.execute(
|
||||||
new ListIssuesQuery(
|
new ListIssuesQuery(
|
||||||
@@ -68,6 +69,7 @@ export class IssueResolver {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
hasDueDate ?? undefined,
|
hasDueDate ?? undefined,
|
||||||
|
type ?? undefined,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,6 +104,7 @@ export class IssueResolver {
|
|||||||
input.priority,
|
input.priority,
|
||||||
input.assignee,
|
input.assignee,
|
||||||
input.dueDate,
|
input.dueDate,
|
||||||
|
input.type,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -124,6 +127,7 @@ export class IssueResolver {
|
|||||||
input.priority,
|
input.priority,
|
||||||
input.assignee,
|
input.assignee,
|
||||||
input.dueDate,
|
input.dueDate,
|
||||||
|
input.type,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user