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:
@@ -156,6 +156,7 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
if (filter.projectIds.length === 0) return { items: [], total: 0 };
|
||||
const query: Record<string, unknown> = {
|
||||
projectId: { $in: filter.projectIds },
|
||||
trashedAt: null,
|
||||
};
|
||||
if (filter.assigneeId) query['assignee.id'] = filter.assigneeId;
|
||||
if (filter.statuses?.length) query['status'] = { $in: filter.statuses };
|
||||
@@ -195,7 +196,7 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
byArea: { _id: string | null; count: number }[];
|
||||
byAssignee: { _id: UserRefReadModel | null; count: number }[];
|
||||
}>([
|
||||
{ $match: { projectId } },
|
||||
{ $match: { projectId, trashedAt: null } },
|
||||
{
|
||||
$facet: {
|
||||
total: [{ $count: 'count' }],
|
||||
@@ -233,7 +234,11 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
|
||||
async findOpenAssignedTo(userId: string): Promise<IssueReadModel[]> {
|
||||
const docs = await this.issues
|
||||
.find({ 'assignee.id': userId, statusCategory: { $nin: FINISHED } })
|
||||
.find({
|
||||
'assignee.id': userId,
|
||||
statusCategory: { $nin: FINISHED },
|
||||
trashedAt: null,
|
||||
})
|
||||
.lean<IssueDoc[]>();
|
||||
return docs.map(issueFromDoc);
|
||||
}
|
||||
@@ -242,6 +247,7 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
const docs = await this.issues
|
||||
.find({
|
||||
dueDate: { $ne: null, $lte: `${day}\uffff` },
|
||||
trashedAt: null,
|
||||
statusCategory: { $nin: FINISHED },
|
||||
$or: [{ assignee: { $ne: null } }, { 'watchers.0': { $exists: true } }],
|
||||
})
|
||||
@@ -251,7 +257,26 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
}
|
||||
|
||||
async findByProject(projectId: string): Promise<IssueReadModel[]> {
|
||||
const docs = await this.issues.find({ projectId }).lean<IssueDoc[]>();
|
||||
const docs = await this.issues
|
||||
.find({ projectId, trashedAt: null })
|
||||
.lean<IssueDoc[]>();
|
||||
return docs.map(issueFromDoc);
|
||||
}
|
||||
|
||||
async findTrashed(projectId: string): Promise<IssueReadModel[]> {
|
||||
const docs = await this.issues
|
||||
.find({ projectId, trashedAt: { $ne: null } })
|
||||
.sort({ trashedAt: -1 })
|
||||
.limit(500)
|
||||
.lean<IssueDoc[]>();
|
||||
return docs.map(issueFromDoc);
|
||||
}
|
||||
|
||||
async findTrashedBefore(before: string): Promise<IssueReadModel[]> {
|
||||
const docs = await this.issues
|
||||
.find({ trashedAt: { $ne: null, $lt: before } })
|
||||
.limit(1000)
|
||||
.lean<IssueDoc[]>();
|
||||
return docs.map(issueFromDoc);
|
||||
}
|
||||
|
||||
@@ -265,7 +290,13 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
storyPoints: number;
|
||||
finishedStoryPoints: number;
|
||||
}>([
|
||||
{ $match: { projectId, 'parent.id': { $type: 'string' } } },
|
||||
{
|
||||
$match: {
|
||||
projectId,
|
||||
'parent.id': { $type: 'string' },
|
||||
trashedAt: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent.id',
|
||||
@@ -280,7 +311,11 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
const progress = new Map(rows.map((row) => [row._id, row]));
|
||||
// Only the top of each tree: a ticket with subtasks under an epic is not one.
|
||||
const docs = await this.issues
|
||||
.find({ _id: { $in: [...progress.keys()] }, 'parent.id': null })
|
||||
.find({
|
||||
_id: { $in: [...progress.keys()] },
|
||||
'parent.id': null,
|
||||
trashedAt: null,
|
||||
})
|
||||
.sort({ number: 1 })
|
||||
.lean<IssueDoc[]>();
|
||||
return docs.map((doc) => {
|
||||
@@ -311,7 +346,7 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
storyPoints: number;
|
||||
finishedStoryPoints: number;
|
||||
}>([
|
||||
{ $match: { 'sprint.id': sprintId } },
|
||||
{ $match: { 'sprint.id': sprintId, trashedAt: null } },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
@@ -338,7 +373,7 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
_id: { status: string; category: StatusCategory };
|
||||
count: number;
|
||||
}>([
|
||||
{ $match: { 'release.id': releaseId } },
|
||||
{ $match: { 'release.id': releaseId, trashedAt: null } },
|
||||
{
|
||||
$group: {
|
||||
_id: { status: '$status', category: '$statusCategory' },
|
||||
@@ -363,7 +398,11 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
}
|
||||
|
||||
function toMongoFilter(filter: IssueFilter): FilterQuery<IssueDoc> {
|
||||
const query: FilterQuery<IssueDoc> = { projectId: filter.projectId };
|
||||
// Tickets in the trash are only listed by the trash itself.
|
||||
const query: FilterQuery<IssueDoc> = {
|
||||
projectId: filter.projectId,
|
||||
trashedAt: null,
|
||||
};
|
||||
if (filter.statuses?.length) query.status = { $in: filter.statuses };
|
||||
if (filter.categories?.length)
|
||||
query['statusCategory'] = { $in: filter.categories };
|
||||
|
||||
@@ -133,6 +133,7 @@ IssueSchema.index({ 'parent.id': 1 });
|
||||
IssueSchema.index({ projectId: 1, rank: 1 });
|
||||
IssueSchema.index({ 'release.id': 1 });
|
||||
IssueSchema.index({ 'sprint.id': 1 });
|
||||
IssueSchema.index({ trashedAt: 1 }, { sparse: true });
|
||||
IssueSchema.index({ title: 'text', description: 'text', key: 'text' });
|
||||
|
||||
export const CommentSchema = new Schema<CommentDoc>(
|
||||
@@ -285,6 +286,7 @@ export function issueFromDoc(doc: IssueDoc): IssueReadModel {
|
||||
startDate: rest.startDate ?? null,
|
||||
sprint: rest.sprint?.id ? rest.sprint : null,
|
||||
devLinks: rest.devLinks ?? [],
|
||||
trashedAt: rest.trashedAt ?? null,
|
||||
checklist: rest.checklist ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user