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: `
@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) {
-
Seleccionar {{ issue.key }}
{{ i + 1 }}
{{
issue.key
}}
{{ issue.title }}
{{
statusLabel(issue)
}}
{{
priorityLabel(issue)
}}
{{
issue.storyPoints ?? '—'
}}
{{
issue.assignee ? initialsOf(issue.assignee.name) : '—'
}}
@if (project.canContribute()) {
}
}
} @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;
}