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

@@ -219,6 +219,27 @@ export class ProjectResolver {
return this.projects.delete(me.id, projectId, confirmKey);
}
@Mutation(() => CommandResultGql, {
description:
'Archive the project (owner only): everything stays readable, nothing changes until it is unarchived',
})
archiveProject(
@CurrentUser() me: User,
@Args('projectId', { type: () => ID }) projectId: string,
) {
return this.projects.setArchived(me.id, projectId, true);
}
@Mutation(() => CommandResultGql, {
description: 'Bring an archived project back (owner only)',
})
unarchiveProject(
@CurrentUser() me: User,
@Args('projectId', { type: () => ID }) projectId: string,
) {
return this.projects.setArchived(me.id, projectId, false);
}
@Mutation(() => CommandResultGql, {
description: 'Add a Pireon user to the project',
})

View File

@@ -137,6 +137,11 @@ export class ProjectGql {
@Field(() => UserRefGql) owner!: UserRefGql;
@Field(() => [ProjectMemberGql]) members!: ProjectMemberGql[];
@Field() createdAt!: string;
@Field(() => String, {
nullable: true,
description: 'Archived since then (read-only); null while active',
})
archivedAt!: string | null;
@Field() updatedAt!: string;
@Field(() => Int) version!: number;
}

View File

@@ -128,6 +128,13 @@ export class ProjectDto {
@ApiProperty({ type: ProjectSettingsDto })
settings!: ProjectSettingsDto;
@ApiProperty({ format: 'date-time' }) createdAt!: string;
@ApiProperty({
type: String,
nullable: true,
format: 'date-time',
description: 'Archived since then (read-only); null while active',
})
archivedAt!: string | null;
@ApiProperty({ format: 'date-time' }) updatedAt!: string;
@ApiProperty({ description: 'Version of the aggregate (event count)' })
version!: number;

View File

@@ -5,6 +5,7 @@ import {
Get,
Param,
Patch,
HttpCode,
Post,
Put,
Query,
@@ -148,6 +149,38 @@ export class ProjectsController {
);
}
@Post(':project/archive')
@HttpCode(200)
@PROJECT
@ApiOperation({
summary: 'Archive a project (owner only)',
description:
'It stays readable (board, tickets, history, statistics); nothing can change until it is unarchived.',
})
@ApiOkResponse({ type: CommandResultDto })
@ApiCommandErrors()
async archive(@CurrentUser() me: User, @Param('project') project: string) {
return this.projects.setArchived(
me.id,
await this.refs.projectId(me.id, project),
true,
);
}
@Post(':project/unarchive')
@HttpCode(200)
@PROJECT
@ApiOperation({ summary: 'Bring an archived project back (owner only)' })
@ApiOkResponse({ type: CommandResultDto })
@ApiCommandErrors()
async unarchive(@CurrentUser() me: User, @Param('project') project: string) {
return this.projects.setArchived(
me.id,
await this.refs.projectId(me.id, project),
false,
);
}
@Put(':project/settings')
@PROJECT
@ApiOperation({