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:
2026-10-08 13:52:43 +02:00
parent 75323115fb
commit d4da33d4e8
84 changed files with 4719 additions and 79 deletions

View File

@@ -1,4 +1,5 @@
import {
EpicReadModel,
IssuePriority,
IssueReadModel,
ProjectStatsReadModel,
@@ -18,6 +19,8 @@ export interface IssueFilter {
unassigned?: boolean;
releaseId?: string;
labels?: string[];
/** Only the tickets hanging from this one. */
parentId?: string;
search?: string;
limit: number;
offset: number;
@@ -51,6 +54,8 @@ export abstract class FindIssuesRepository {
abstract findOpenAssignedTo(userId: string): Promise<IssueReadModel[]>;
/** Every ticket of a project (statistics). */
abstract findByProject(projectId: string): Promise<IssueReadModel[]>;
/** Top-level tickets with others hanging from them, by key. */
abstract epics(projectId: string): Promise<EpicReadModel[]>;
abstract releaseProgress(
releaseId: string,
): Promise<ReleaseProgressReadModel>;

View File

@@ -10,4 +10,6 @@ export abstract class TrackerStreamsRepository {
abstract commentIdsOfIssue(issueId: string): Promise<string[]>;
/** Tickets that were ever linked to `issueId` (they may have unlinked since). */
abstract issueIdsLinkingTo(issueId: string): Promise<string[]>;
/** Tickets that were ever placed under `issueId` (they may have left since). */
abstract issueIdsEverUnder(issueId: string): Promise<string[]>;
}

View File

@@ -1,5 +1,6 @@
import {
AttachmentReadModel,
ChecklistItemReadModel,
IssueLinkReadModel,
IssueReadModel,
StatusCategory,
@@ -13,7 +14,7 @@ export abstract class UpsertMaterializedIssueRepository {
Partial<
Omit<
IssueReadModel,
'links' | 'commentCount' | 'attachments' | 'watchers'
'links' | 'commentCount' | 'attachments' | 'watchers' | 'checklist'
>
>,
): Promise<void>;
@@ -35,6 +36,19 @@ export abstract class UpsertMaterializedIssueRepository {
/** Adds a follower (idempotent). */
abstract addWatcher(issueId: string, user: UserRefReadModel): Promise<void>;
abstract removeWatcher(issueId: string, userId: string): Promise<void>;
/** Adds a checklist step (idempotent). */
abstract addChecklistItem(
issueId: string,
item: ChecklistItemReadModel,
): Promise<void>;
abstract updateChecklistItem(
issueId: string,
itemId: string,
changes: Partial<Pick<ChecklistItemReadModel, 'text' | 'done'>>,
): Promise<void>;
abstract removeChecklistItem(issueId: string, itemId: string): Promise<void>;
/** Keeps the denormalised parent title of a ticket's children in sync. */
abstract renameParent(parentId: string, title: string): Promise<void>;
abstract setCommentCount(
issueId: string,
commentCount: number,