feat: sprints with burndown and carry-over
Plan sprints with dates and a goal, run one at a time per project, and put tickets in them from the ticket page. The Sprints tab follows the one under way with its progress and a burndown in tickets or points against the ideal line. Closing a sprint sends what was left to the next one or back to the backlog; the board can be filtered by sprint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,12 @@ const NOBODY = '__nobody__';
|
||||
[options]="priorityFilterOptions"
|
||||
[(value)]="priorityFilter"
|
||||
/>
|
||||
<pi-select-field
|
||||
class="ts-filter"
|
||||
placeholder="Sprint"
|
||||
[options]="sprintFilterOptions()"
|
||||
[(value)]="sprintFilter"
|
||||
/>
|
||||
<pi-select-field
|
||||
class="ts-filter"
|
||||
placeholder="Release"
|
||||
@@ -335,6 +341,8 @@ export class BoardPage {
|
||||
readonly release = input<string | undefined>();
|
||||
/** `?assignee=` (from the tracking page's team list), bound by the router. */
|
||||
readonly assignee = input<string | undefined>();
|
||||
/** `?sprint=` (from the sprints page), bound by the router. */
|
||||
readonly sprint = input<string | undefined>();
|
||||
|
||||
protected readonly board = inject(BoardFacade);
|
||||
protected readonly project = inject(ProjectFacade);
|
||||
@@ -353,6 +361,16 @@ export class BoardPage {
|
||||
protected readonly releaseFilter = signal<string | null>(ALL);
|
||||
protected readonly area = signal<string | null>(ALL);
|
||||
protected readonly typeFilter = signal<string | null>(ALL);
|
||||
protected readonly sprintFilter = signal<string | null>(ALL);
|
||||
protected readonly sprintFilterOptions = computed<
|
||||
PiSelectFieldOption<string>[]
|
||||
>(() => [
|
||||
{ value: ALL, label: 'Todos los sprints' },
|
||||
...this.project.openSprints().map((s) => ({
|
||||
value: s.id,
|
||||
label: s.status === 'ACTIVE' ? `${s.name} · en curso` : s.name,
|
||||
})),
|
||||
]);
|
||||
protected readonly priorityFilter = signal<string | null>(ALL);
|
||||
/** What is typed; `search` follows it once typing pauses. */
|
||||
protected readonly searchInput = signal('');
|
||||
@@ -364,6 +382,7 @@ export class BoardPage {
|
||||
!!(
|
||||
this.assigneeFilter() ||
|
||||
this.releaseFilter() ||
|
||||
this.sprintFilter() ||
|
||||
this.area() ||
|
||||
this.typeFilter() ||
|
||||
this.priorityFilter() ||
|
||||
@@ -485,6 +504,10 @@ export class BoardPage {
|
||||
const assignee = this.assignee();
|
||||
if (assignee) untracked(() => this.assigneeFilter.set(assignee));
|
||||
});
|
||||
effect(() => {
|
||||
const sprint = this.sprint();
|
||||
if (sprint) untracked(() => this.sprintFilter.set(sprint));
|
||||
});
|
||||
effect((onCleanup) => {
|
||||
const text = this.searchInput().trim();
|
||||
const timer = setTimeout(() => this.search.set(text), 300);
|
||||
@@ -505,6 +528,7 @@ export class BoardPage {
|
||||
priorities: priority ? [priority] : undefined,
|
||||
labels: this.labels().length ? this.labels() : undefined,
|
||||
search: this.search() || undefined,
|
||||
sprintId: this.sprintFilter() || undefined,
|
||||
};
|
||||
if (!projectId) return;
|
||||
untracked(() =>
|
||||
@@ -569,6 +593,7 @@ export class BoardPage {
|
||||
filter.unassigned ? NOBODY : (filter.assigneeId ?? ALL),
|
||||
);
|
||||
this.releaseFilter.set(filter.releaseId ?? ALL);
|
||||
this.sprintFilter.set(ALL);
|
||||
this.area.set(filter.area ?? ALL);
|
||||
this.typeFilter.set(filter.types?.[0] ?? ALL);
|
||||
this.priorityFilter.set(filter.priorities?.[0] ?? ALL);
|
||||
@@ -581,6 +606,7 @@ export class BoardPage {
|
||||
for (const filter of [
|
||||
this.assigneeFilter,
|
||||
this.releaseFilter,
|
||||
this.sprintFilter,
|
||||
this.area,
|
||||
this.typeFilter,
|
||||
this.priorityFilter,
|
||||
|
||||
Reference in New Issue
Block a user