Deleting a ticket now sends it to the trash, for whoever opened it or an admin: it disappears from boards, searches and statistics, cannot change, and comes back from the ticket page or the trash page for 30 days. After that it is deleted for good with the usual cascade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
DestroyRef,
|
|
effect,
|
|
inject,
|
|
signal,
|
|
untracked,
|
|
} from '@angular/core';
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
import { RouterLink } from '@angular/router';
|
|
import { PiActionButtonComponent, PiEmptyStateComponent } from '@pi/ui';
|
|
import { ToastService } from '@task-stream/web/shared/ui';
|
|
import { ProjectFacade } from '@task-stream/web/tracker/application';
|
|
import { IssueSummary, IssuesGateway } from '@task-stream/web/tracker/domain';
|
|
import { timeAgo } from '../ui/copy';
|
|
|
|
/** /p/:key/trash — tickets deleted in the last 30 days, to bring back. */
|
|
@Component({
|
|
selector: 'ts-trash-page',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [RouterLink, PiActionButtonComponent, PiEmptyStateComponent],
|
|
template: `
|
|
<div class="ts-section-header">
|
|
<p class="ts-muted">
|
|
Tickets borrados en los últimos 30 días. Después se borran del todo, con
|
|
sus comentarios.
|
|
</p>
|
|
</div>
|
|
@if (issues().length) {
|
|
<ul class="ts-trash ts-panel">
|
|
@for (issue of issues(); track issue.id) {
|
|
<li class="ts-trash__row">
|
|
<a class="ts-key" [routerLink]="['..', 'issues', issue.key]">{{
|
|
issue.key
|
|
}}</a>
|
|
<span class="ts-trash__title">{{ issue.title }}</span>
|
|
<span class="ts-muted">{{
|
|
issue.trashedAt ? ago(issue.trashedAt) : ''
|
|
}}</span>
|
|
@if (project.canContribute()) {
|
|
<pi-action-button
|
|
size="small"
|
|
color="grey"
|
|
appearance="outline"
|
|
text="Recuperar"
|
|
(execute)="restore(issue)"
|
|
/>
|
|
}
|
|
</li>
|
|
}
|
|
</ul>
|
|
} @else if (loaded()) {
|
|
<pi-empty-state
|
|
icon="trash"
|
|
title="La papelera está vacía"
|
|
description="Lo que se borre aparecerá aquí durante 30 días."
|
|
/>
|
|
}
|
|
`,
|
|
styles: `
|
|
.ts-trash {
|
|
margin: 0;
|
|
padding: 0.25rem 0;
|
|
list-style: none;
|
|
}
|
|
.ts-trash__row {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
padding: 0.5rem 0.875rem;
|
|
border-top: 1px solid var(--ts-line);
|
|
&:first-child {
|
|
border-top: 0;
|
|
}
|
|
}
|
|
.ts-trash__title {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
`,
|
|
})
|
|
export class TrashPage {
|
|
protected readonly project = inject(ProjectFacade);
|
|
private readonly gateway = inject(IssuesGateway);
|
|
private readonly toast = inject(ToastService);
|
|
private readonly destroyRef = inject(DestroyRef);
|
|
protected readonly issues = signal<IssueSummary[]>([]);
|
|
protected readonly loaded = signal(false);
|
|
protected readonly ago = timeAgo;
|
|
|
|
constructor() {
|
|
effect(() => {
|
|
const projectId = this.project.project()?.id;
|
|
if (projectId) untracked(() => this.reload(projectId));
|
|
});
|
|
}
|
|
|
|
protected restore(issue: IssueSummary): void {
|
|
this.gateway
|
|
.restore(issue.id)
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe({
|
|
next: () => {
|
|
this.toast.success(`${issue.key} recuperado`);
|
|
this.issues.update((list) => list.filter((i) => i.id !== issue.id));
|
|
},
|
|
error: (error) =>
|
|
this.toast.error(
|
|
(error as { message?: string })?.message?.split('\n')[0] ||
|
|
'No se pudo recuperar',
|
|
),
|
|
});
|
|
}
|
|
|
|
private reload(projectId: string): void {
|
|
this.gateway
|
|
.trashed(projectId)
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe({
|
|
next: (issues) => {
|
|
this.issues.set(issues);
|
|
this.loaded.set(true);
|
|
},
|
|
error: () => this.loaded.set(true),
|
|
});
|
|
}
|
|
}
|