feat: an ordered backlog
A Backlog tab lists the tickets still to start in the order the team will take them; drag them, or use the arrows, to reorder. Moving a ticket only rewrites that ticket's rank, read from the event store, and board columns follow the same order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { IssueCreatedEvent } from '../events/issue-created.event';
|
||||
import { IssueDeletedEvent } from '../events/issue-deleted.event';
|
||||
import { IssueLinkedEvent } from '../events/issue-linked.event';
|
||||
import { IssueParentChangedEvent } from '../events/issue-parent-changed.event';
|
||||
import { IssueRankedEvent } from '../events/issue-ranked.event';
|
||||
import { IssueReleaseChangedEvent } from '../events/issue-release-changed.event';
|
||||
import { IssueStatusChangedEvent } from '../events/issue-status-changed.event';
|
||||
import { IssueUnlinkedEvent } from '../events/issue-unlinked.event';
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
normalizeChecklistText,
|
||||
} from './issue-checklist';
|
||||
import { ParentCandidate, assertCanNest } from './issue-hierarchy';
|
||||
import { implicitRank, rankBetween } from './issue-rank';
|
||||
import { IssueLinkType, IssuePriority } from './issue.enums';
|
||||
|
||||
export interface IssueLink {
|
||||
@@ -108,6 +110,8 @@ export class Issue extends VersionedAggregateRoot implements IssueProps {
|
||||
/** The epic or ticket this one breaks down. */
|
||||
public parentId: string | null = null;
|
||||
public checklist: ChecklistItem[] = [];
|
||||
/** Place in the backlog once someone moved it (see `backlogRank`). */
|
||||
public rank: string | null = null;
|
||||
public createdAt!: string;
|
||||
public updatedAt!: string;
|
||||
public deleted = false;
|
||||
@@ -316,6 +320,28 @@ export class Issue extends VersionedAggregateRoot implements IssueProps {
|
||||
);
|
||||
}
|
||||
|
||||
/** Where it sits in the backlog: where it was put, or its creation order. */
|
||||
get backlogRank(): string {
|
||||
return this.rank ?? implicitRank(this.number);
|
||||
}
|
||||
|
||||
/** Puts the ticket between two others of the backlog (null: an end). */
|
||||
moveBetween(actorId: string, above: Issue | null, below: Issue | null): void {
|
||||
for (const neighbour of [above, below]) {
|
||||
if (neighbour?.id === this.id)
|
||||
throw new BusinessRuleError(`${this.key} cannot move next to itself`);
|
||||
if (neighbour && neighbour.projectId !== this.projectId)
|
||||
throw new BusinessRuleError(
|
||||
`${neighbour.key} belongs to another project`,
|
||||
);
|
||||
}
|
||||
const rank = rankBetween(
|
||||
above?.backlogRank ?? null,
|
||||
below?.backlogRank ?? null,
|
||||
);
|
||||
this.apply(new IssueRankedEvent(actorId, this.id, this.projectId, rank));
|
||||
}
|
||||
|
||||
/** Adds a step to the checklist; `id` is generated by the caller. */
|
||||
addChecklistItem(actorId: string, id: string, text: string): void {
|
||||
if (this.checklist.some((i) => i.id === id)) return;
|
||||
@@ -463,6 +489,11 @@ export class Issue extends VersionedAggregateRoot implements IssueProps {
|
||||
this.updatedAt = event.occurredAt;
|
||||
}
|
||||
|
||||
onIssueRankedEvent(event: IssueRankedEvent): void {
|
||||
// Reordering is not a change to the ticket: `updatedAt` stays.
|
||||
this.rank = event.rank;
|
||||
}
|
||||
|
||||
onIssueChecklistItemAddedEvent(event: IssueChecklistItemAddedEvent): void {
|
||||
this.checklist = [...this.checklist, { ...event.item }];
|
||||
this.updatedAt = event.occurredAt;
|
||||
|
||||
Reference in New Issue
Block a user