feat: epics, subtasks and checklists
Tickets can hang from another one of the same project, three levels deep at most (epic → ticket → subtask). The ticket page lists its subtasks with their progress, creates new ones and moves the ticket under an epic; a new Épicas tab shows every epic and how far it has got. Each ticket also gets a checklist for the small steps that don't deserve a ticket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||
import {
|
||||
ActivityReadModel,
|
||||
EpicReadModel,
|
||||
IssueDetails,
|
||||
IssueLinkType,
|
||||
IssueReadModel,
|
||||
@@ -13,6 +14,12 @@ import {
|
||||
} from './commands/attach-file.command';
|
||||
import { DetachFileCommand } from './commands/detach-file.command';
|
||||
import { WatchIssueCommand } from './commands/watch-issue.command';
|
||||
import { SetIssueParentCommand } from './commands/set-issue-parent.command';
|
||||
import {
|
||||
AddChecklistItemCommand,
|
||||
RemoveChecklistItemCommand,
|
||||
UpdateChecklistItemCommand,
|
||||
} from './commands/change-checklist.command';
|
||||
import { ChangeIssueStatusCommand } from './commands/change-issue-status.command';
|
||||
import { CommandResult } from './commands/command-result';
|
||||
import { CreateIssueCommand } from './commands/create-issue.command';
|
||||
@@ -29,6 +36,7 @@ import {
|
||||
import { GetIssueQuery } from './queries/get-issue.query';
|
||||
import { GetIssuesByIdsQuery } from './queries/get-issues-by-ids.query';
|
||||
import { SearchIssuesQuery } from './queries/search-issues.query';
|
||||
import { GetEpicsQuery } from './queries/get-epics.query';
|
||||
import {
|
||||
MyIssuesFilter,
|
||||
SearchMyIssuesQuery,
|
||||
@@ -41,6 +49,8 @@ export interface CreateIssueInput extends Omit<IssueDetails, 'title' | 'type'> {
|
||||
title: string;
|
||||
assigneeId?: string | null;
|
||||
releaseId?: string | null;
|
||||
/** Epic or ticket the new one breaks down. */
|
||||
parentId?: string | null;
|
||||
}
|
||||
|
||||
export interface IssueLinkInput {
|
||||
@@ -57,7 +67,15 @@ export class IssuesService {
|
||||
) {}
|
||||
|
||||
create(actorId: string, input: CreateIssueInput): Promise<CommandResult> {
|
||||
const { projectId, type, title, assigneeId, releaseId, ...details } = input;
|
||||
const {
|
||||
projectId,
|
||||
type,
|
||||
title,
|
||||
assigneeId,
|
||||
releaseId,
|
||||
parentId,
|
||||
...details
|
||||
} = input;
|
||||
return this.commandBus.execute(
|
||||
new CreateIssueCommand(
|
||||
actorId,
|
||||
@@ -67,6 +85,7 @@ export class IssuesService {
|
||||
details,
|
||||
assigneeId,
|
||||
releaseId,
|
||||
parentId,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -188,6 +207,52 @@ export class IssuesService {
|
||||
return this.queryBus.execute(new GetIssuesByIdsQuery(viewerId, ids));
|
||||
}
|
||||
|
||||
/** Places the ticket under `parentId` (null: back to the top). */
|
||||
setParent(
|
||||
actorId: string,
|
||||
issueId: string,
|
||||
parentId: string | null,
|
||||
): Promise<CommandResult> {
|
||||
return this.commandBus.execute(
|
||||
new SetIssueParentCommand(actorId, issueId, parentId),
|
||||
);
|
||||
}
|
||||
|
||||
addChecklistItem(
|
||||
actorId: string,
|
||||
issueId: string,
|
||||
text: string,
|
||||
): Promise<CommandResult> {
|
||||
return this.commandBus.execute(
|
||||
new AddChecklistItemCommand(actorId, issueId, text),
|
||||
);
|
||||
}
|
||||
|
||||
updateChecklistItem(
|
||||
actorId: string,
|
||||
issueId: string,
|
||||
itemId: string,
|
||||
changes: { text?: string; done?: boolean },
|
||||
): Promise<CommandResult> {
|
||||
return this.commandBus.execute(
|
||||
new UpdateChecklistItemCommand(actorId, issueId, itemId, changes),
|
||||
);
|
||||
}
|
||||
|
||||
removeChecklistItem(
|
||||
actorId: string,
|
||||
issueId: string,
|
||||
itemId: string,
|
||||
): Promise<CommandResult> {
|
||||
return this.commandBus.execute(
|
||||
new RemoveChecklistItemCommand(actorId, issueId, itemId),
|
||||
);
|
||||
}
|
||||
|
||||
epics(viewerId: string | null, projectId: string): Promise<EpicReadModel[]> {
|
||||
return this.queryBus.execute(new GetEpicsQuery(viewerId, projectId));
|
||||
}
|
||||
|
||||
search(
|
||||
viewerId: string | null,
|
||||
filter: IssueFilter,
|
||||
|
||||
Reference in New Issue
Block a user