feat: trash for deleted pages in space (#325)

* initial commit

* added recycle bin modal, updated api routes

* updated page service & controller, recycle bin modal

* updated page-query.ts, use-tree-mutation.ts, recycled-pages.ts

* removed quotes from openRestorePageModal prompt

* Updated page.repo.ts

* move button to space menu

* fix react issues

* opted to reload to enact changes in the client

* lint

* hide deleted pages in recents, handle restore child page

* fix null check

* WIP

* WIP

* feat: implement dedicated trash page
- Replace modal-based trash view with dedicated route `/s/:spaceSlug/trash`
- Add pagination support for deleted pages
- Other improvements

* fix translation

* trash cleanup cron

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
This commit is contained in:
Eddy Oyieko
2025-07-29 23:20:49 +03:00
committed by GitHub
parent 28fcb11cb4
commit ec12e80423
22 changed files with 1062 additions and 200 deletions

View File

@ -12,7 +12,7 @@ export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy {
super();
}
async process(job: Job<Space, void>): Promise<void> {
async process(job: Job<any, void>): Promise<void> {
try {
if (job.name === QueueJob.DELETE_SPACE_ATTACHMENTS) {
await this.attachmentService.handleDeleteSpaceAttachments(job.data.id);
@ -20,6 +20,11 @@ export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy {
if (job.name === QueueJob.DELETE_USER_AVATARS) {
await this.attachmentService.handleDeleteUserAvatars(job.data.id);
}
if (job.name === QueueJob.DELETE_PAGE_ATTACHMENTS) {
await this.attachmentService.handleDeletePageAttachments(
job.data.pageId,
);
}
} catch (err) {
throw err;
}

View File

@ -321,4 +321,50 @@ export class AttachmentService {
throw err;
}
}
async handleDeletePageAttachments(pageId: string) {
try {
// Fetch attachments for this page from database
const attachments = await this.db
.selectFrom('attachments')
.select(['id', 'filePath'])
.where('pageId', '=', pageId)
.execute();
if (!attachments || attachments.length === 0) {
return;
}
const failedDeletions = [];
await Promise.all(
attachments.map(async (attachment) => {
try {
// Delete from storage
await this.storageService.delete(attachment.filePath);
// Delete from database
await this.attachmentRepo.deleteAttachmentById(attachment.id);
} catch (err) {
failedDeletions.push(attachment.id);
this.logger.error(
`Failed to delete attachment ${attachment.id} for page ${pageId}:`,
err,
);
}
}),
);
if (failedDeletions.length > 0) {
this.logger.warn(
`Failed to delete ${failedDeletions.length} attachments for page ${pageId}`,
);
}
} catch (err) {
this.logger.error(
`Error in handleDeletePageAttachments for page ${pageId}:`,
err,
);
throw err;
}
}
}