feat: archive a project instead of deleting it

The owner archives a finished project (archiveProject, POST
/projects/{project}/archive) and brings it back (unarchive). While
archived it stays readable, managers still read its rules and webhooks,
but no command changes it: contributing and managing checks refuse with
422, votes and public proposals too, and automations do not run. The web
shows a read-only notice on every page of it (with «Desarchivar» for the
owner), hides what edits, marks it on the home page and lists it last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 15:46:22 +02:00
parent e598f76e1c
commit 4acf629106
42 changed files with 1057 additions and 14 deletions

View File

@@ -101,12 +101,27 @@ import { projectTone, PUBLIC_TONE, ROLE } from '../ui/copy';
<a routerLink="analytics" routerLinkActive="is-active">Estadísticas</a>
<a routerLink="activity" routerLinkActive="is-active">Actividad</a>
<a routerLink="members" routerLinkActive="is-active">Miembros</a>
@if (facade.canManage()) {
@if (facade.canManage() || facade.isOwner()) {
<a routerLink="settings" routerLinkActive="is-active"
>Configuración</a
>
}
</nav>
@if (project.archivedAt) {
<p class="ts-archived" role="status">
<strong>Proyecto archivado</strong>
Se puede ver todo, pero es de solo lectura.
@if (facade.isOwner()) {
<button
type="button"
class="ts-plain-link"
(click)="facade.setArchived(false)"
>
Desarchivar
</button>
}
</p>
}
<router-outlet />
} @else if (facade.error(); as error) {
<pi-note
@@ -142,6 +157,19 @@ import { projectTone, PUBLIC_TONE, ROLE } from '../ui/copy';
}
`,
styles: `
.ts-archived {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0 0 1rem;
padding: 0.625rem 0.875rem;
border: 1px solid var(--ts-line);
border-radius: 0.625rem;
background: var(--ts-sunken);
color: var(--ts-ink-soft);
font-size: 0.875rem;
}
.ts-eyebrow .ts-pill {
margin-left: 0.5rem;
letter-spacing: 0;

View File

@@ -693,6 +693,32 @@ function idFrom(name: string): string {
}
@if (facade.isOwner() && facade.project(); as project) {
<section
class="ts-panel ts-block ts-visibility"
aria-labelledby="archive-title"
>
<div>
<h3 id="archive-title">Archivar el proyecto</h3>
<p class="ts-muted">
@if (project.archivedAt) {
Archivado: se puede ver todo, pero nadie puede cambiar nada
hasta que lo desarchives.
} @else {
Para un proyecto terminado: queda todo a la vista (tablero,
tickets, historia, estadísticas) y en solo lectura. Se puede
desarchivar cuando quieras.
}
</p>
</div>
<button
type="button"
class="ts-secondary"
(click)="facade.setArchived(!project.archivedAt)"
>
{{ project.archivedAt ? 'Desarchivar' : 'Archivar' }}
</button>
</section>
<section
class="ts-panel ts-block ts-danger"
aria-labelledby="danger-title"

View File

@@ -25,6 +25,9 @@ import { projectTone, PUBLIC_TONE, timeAgo } from './copy';
@if (project().isPublic) {
<span class="ts-pill" [style.--tone]="publicTone">Público</span>
}
@if (project().archivedAt) {
<span class="ts-pill">Archivado</span>
}
<span class="ts-project__updated ts-mono">{{
ago(project().updatedAt)
}}</span>