feat: a trash for tickets

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>
This commit is contained in:
2026-10-08 18:47:30 +02:00
parent 71b807b94b
commit bec39da46d
49 changed files with 1671 additions and 9 deletions

View File

@@ -32,6 +32,9 @@ import {
MyIssuesDocument,
RankIssueDocument,
RemoveChecklistItemDocument,
RestoreIssueDocument,
TrashDocument,
TrashIssueDocument,
SearchIssuesDocument,
SetIssueParentDocument,
SetIssueReleaseDocument,
@@ -180,6 +183,24 @@ export class GraphqlIssuesGateway extends IssuesGateway {
);
}
trash(issueId: string): Observable<CommandResult> {
return this.gql
.mutate(TrashIssueDocument, { issueId })
.pipe(map((d) => toCommandResult(d.trashIssue)));
}
restore(issueId: string): Observable<CommandResult> {
return this.gql
.mutate(RestoreIssueDocument, { issueId })
.pipe(map((d) => toCommandResult(d.restoreIssue)));
}
trashed(projectId: string): Observable<IssueSummary[]> {
return this.gql
.query(TrashDocument, { projectId })
.pipe(map((d) => d.trash.map(toIssueSummary)));
}
bulkUpdate(issueIds: string[], change: BulkChange): Observable<BulkResult> {
return this.gql
.mutate(BulkUpdateIssuesDocument, {

View File

@@ -87,6 +87,7 @@ fragment IssueSummaryFields on Issue {
storyPoints
startDate
dueDate
trashedAt
}
fragment IssueFields on Issue {

View File

@@ -481,6 +481,7 @@ export type IssueSummaryFieldsFragment = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -509,6 +510,7 @@ export type IssueFieldsFragment = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
reporter: { id: string; name: string };
links: Array<{
type: IssueLinkType;
@@ -652,6 +654,7 @@ export type BoardQuery = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -688,6 +691,7 @@ export type IssueQuery = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
reporter: { id: string; name: string };
links: Array<{
type: IssueLinkType;
@@ -747,6 +751,7 @@ export type SearchIssuesQuery = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -903,6 +908,7 @@ export type MyIssuesQuery = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -935,6 +941,7 @@ export type IssueChildrenQuery = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -967,6 +974,7 @@ export type EpicsQuery = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -1041,6 +1049,7 @@ export type BacklogQuery = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -1068,6 +1077,53 @@ export type BulkUpdateIssuesMutation = {
};
};
export type TrashIssueMutationVariables = Exact<{
issueId: string;
}>;
export type TrashIssueMutation = {
trashIssue: { id: string; key: string | null; version: number };
};
export type RestoreIssueMutationVariables = Exact<{
issueId: string;
}>;
export type RestoreIssueMutation = {
restoreIssue: { id: string; key: string | null; version: number };
};
export type TrashQueryVariables = Exact<{
projectId: string;
}>;
export type TrashQuery = {
trash: Array<{
id: string;
projectId: string;
key: string;
type: string;
area: string | null;
title: string;
status: string;
statusCategory: StatusCategory;
priority: IssuePriority;
labels: Array<string>;
commentCount: number;
updatedAt: string;
version: number;
rank: string;
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
parent: { id: string; key: string; title: string } | null;
}>;
};
export type NotificationFieldsFragment = {
id: string;
kind: NotificationKind;
@@ -1714,6 +1770,7 @@ export type IssueChangedSubscription = {
storyPoints: number | null;
startDate: string | null;
dueDate: string | null;
trashedAt: string | null;
assignee: { id: string; name: string } | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
@@ -2323,6 +2380,7 @@ export const IssueSummaryFieldsFragmentDoc = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -2555,6 +2613,7 @@ export const IssueFieldsFragmentDoc = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -4010,6 +4069,7 @@ export const BoardDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -4156,6 +4216,7 @@ export const IssueDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -4436,6 +4497,7 @@ export const SearchIssuesDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -5682,6 +5744,7 @@ export const MyIssuesDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -5842,6 +5905,7 @@ export const IssueChildrenDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -6020,6 +6084,7 @@ export const EpicsDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -6502,6 +6567,7 @@ export const BacklogDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
@@ -6646,6 +6712,287 @@ export const BulkUpdateIssuesDocument = {
BulkUpdateIssuesMutation,
BulkUpdateIssuesMutationVariables
>;
export const TrashIssueDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'TrashIssue' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'issueId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'trashIssue' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'issueId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'issueId' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'CommandResultFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'CommandResultFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'CommandResult' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
{ kind: 'Field', name: { kind: 'Name', value: 'version' } },
],
},
},
],
} as unknown as DocumentNode<TrashIssueMutation, TrashIssueMutationVariables>;
export const RestoreIssueDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'RestoreIssue' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'issueId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'restoreIssue' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'issueId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'issueId' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'CommandResultFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'CommandResultFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'CommandResult' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
{ kind: 'Field', name: { kind: 'Name', value: 'version' } },
],
},
},
],
} as unknown as DocumentNode<
RestoreIssueMutation,
RestoreIssueMutationVariables
>;
export const TrashDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'Trash' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'trash' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'projectId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectId' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'IssueSummaryFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'UserRefFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'UserRef' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'IssueSummaryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Issue' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
{ kind: 'Field', name: { kind: 'Name', value: 'type' } },
{ kind: 'Field', name: { kind: 'Name', value: 'area' } },
{ kind: 'Field', name: { kind: 'Name', value: 'title' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'statusCategory' } },
{ kind: 'Field', name: { kind: 'Name', value: 'priority' } },
{ kind: 'Field', name: { kind: 'Name', value: 'labels' } },
{ kind: 'Field', name: { kind: 'Name', value: 'commentCount' } },
{ kind: 'Field', name: { kind: 'Name', value: 'updatedAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'version' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'assignee' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'UserRefFields' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'release' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'sprint' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'parent' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
{ kind: 'Field', name: { kind: 'Name', value: 'title' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'rank' } },
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},
],
} as unknown as DocumentNode<TrashQuery, TrashQueryVariables>;
export const MyNotificationsDocument = {
kind: 'Document',
definitions: [
@@ -10421,6 +10768,7 @@ export const IssueChangedDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'storyPoints' } },
{ kind: 'Field', name: { kind: 'Name', value: 'startDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'dueDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'trashedAt' } },
],
},
},

View File

@@ -221,3 +221,21 @@ mutation BulkUpdateIssues($input: BulkUpdateIssuesInput!) {
}
}
}
mutation TrashIssue($issueId: ID!) {
trashIssue(issueId: $issueId) {
...CommandResultFields
}
}
mutation RestoreIssue($issueId: ID!) {
restoreIssue(issueId: $issueId) {
...CommandResultFields
}
}
query Trash($projectId: ID!) {
trash(projectId: $projectId) {
...IssueSummaryFields
}
}