import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; import { ChangeDetectionStrategy, Component, computed, effect, signal, inject, untracked, } from '@angular/core'; import { RouterLink } from '@angular/router'; import { PiCheckboxComponent, PiEmptyStateComponent, PiIconButtonComponent, PiIconComponent, PiNoteComponent, PiSkeletonComponent, } from '@pi/ui'; import { BacklogFacade, ProjectFacade, provideBacklogFacade, } from '@task-stream/web/tracker/application'; import { BulkChange, IssueSummary, sumPoints, } from '@task-stream/web/tracker/domain'; import { BulkBarComponent } from '../ui/bulk-bar'; import { initials, PRIORITY, statusLook, typeLook } from '../ui/copy'; /** /p/:key/backlog — what is still to start, in the order the team takes it. */ @Component({ selector: 'ts-backlog-page', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ PiCheckboxComponent, BulkBarComponent, RouterLink, CdkDrag, CdkDropList, PiEmptyStateComponent, PiIconButtonComponent, PiIconComponent, PiNoteComponent, PiSkeletonComponent, ], providers: [provideBacklogFacade()], template: `

Lo que aún no ha empezado, en el orden en que se va a coger. Arrastra para reordenar; las columnas del tablero siguen este mismo orden.

@if (facade.loaded() && facade.issues().length) { {{ points() }} pts estimados · {{ unestimated() }} sin estimar } @if (facade.loaded() && facade.total() > facade.issues().length) { {{ facade.issues().length }} de {{ facade.total() }} }
@if (facade.error(); as error) { } @else if (!facade.loaded()) { } @else if (facade.issues().length) { @if (project.canContribute()) { }
    @for (issue of facade.issues(); track issue.id; let i = $index) {
  1. Seleccionar {{ issue.key }} {{ i + 1 }} {{ issue.key }} {{ issue.title }} {{ statusLabel(issue) }} {{ priorityLabel(issue) }} {{ issue.storyPoints ?? '—' }} {{ issue.assignee ? initialsOf(issue.assignee.name) : '—' }} @if (project.canContribute()) { }
  2. }
} @else { }

Papelera

`, styles: ` .ts-backlog { margin: 0; padding: 0.25rem 0; list-style: none; /* Selected rows have a background: kept inside the rounded corners. */ overflow: hidden; } .ts-backlog__row { display: grid; grid-template-columns: 1rem 2rem auto auto minmax(0, 1fr) auto auto 2rem auto auto; gap: 0.625rem; align-items: center; padding: 0.375rem 0.875rem; border-top: 1px solid var(--ts-line); background: var(--ts-surface); cursor: grab; &:first-child { border-top: 0; } @media (max-width: 720px) { grid-template-columns: 1rem 2rem auto minmax(0, 1fr) auto; .ts-backlog__type, .ts-backlog__points, .ts-pill, .ts-backlog__priority, .ts-backlog__who { display: none; } } } .ts-backlog__row.is-selected { background: color-mix(in srgb, var(--ts-brand) 8%, var(--ts-surface)); } .ts-backlog__trash { margin: 1rem 0 0; font-size: 0.875rem; text-align: right; } .ts-backlog__pos { color: var(--ts-muted); font-size: 0.75rem; text-align: right; } .ts-backlog__type { display: inline-flex; color: var(--tone); } .ts-backlog__title { overflow: hidden; color: inherit; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; &:hover { color: var(--ts-brand); } } .ts-backlog__priority { font-size: 0.8125rem; } .ts-backlog__points { font-size: 0.8125rem; text-align: right; } .ts-backlog__who { display: inline-grid; place-items: center; width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--ts-line); font-size: 0.625rem; font-weight: 600; } .ts-backlog__moves { display: inline-flex; } .cdk-drag-preview { box-shadow: 0 8px 24px rgb(0 0 0 / 18%); } .cdk-drag-placeholder { opacity: 0.35; } `, }) export class BacklogPage { protected readonly facade = inject(BacklogFacade); protected readonly project = inject(ProjectFacade); protected readonly initialsOf = initials; /* Bulk edit */ protected readonly selected = signal(new Set()); protected readonly allSelected = computed( () => this.facade.issues().length > 0 && this.facade.issues().every((i) => this.selected().has(i.id)), ); protected toggle(issueId: string, on: boolean): void { this.selected.update((set) => { const next = new Set(set); if (on) next.add(issueId); else next.delete(issueId); return next; }); } protected selectAll(on: boolean): void { this.selected.set(new Set(on ? this.facade.issues().map((i) => i.id) : [])); } protected applyBulk(change: BulkChange): void { this.facade.bulkUpdate([...this.selected()], change); this.selected.set(new Set()); } protected readonly points = computed(() => sumPoints(this.facade.issues())); protected readonly unestimated = computed( () => this.facade.issues().filter((i) => i.storyPoints === null).length, ); constructor() { effect(() => { const projectId = this.project.project()?.id; if (projectId) untracked(() => this.facade.open(projectId)); }); } protected drop(event: CdkDragDrop): void { this.facade.move(event.item.data.id, event.currentIndex); } protected typeIcon = (issue: IssueSummary) => typeLook(this.project.workflow(), issue.type).icon; protected typeTone = (issue: IssueSummary) => typeLook(this.project.workflow(), issue.type).tone; protected typeLabel = (issue: IssueSummary) => typeLook(this.project.workflow(), issue.type).label; protected statusTone = (issue: IssueSummary) => statusLook(this.project.workflow(), issue.status).tone; protected statusLabel = (issue: IssueSummary) => statusLook(this.project.workflow(), issue.status).label; protected priorityLabel = (issue: IssueSummary) => PRIORITY[issue.priority].label; }