From f8425cf521443abe0b433f8acc8df9f0454bb3b1 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Tue, 20 Jan 2026 17:02:32 +0545 Subject: [PATCH 1/6] feat(OUT-2915): backfilling script for attachments table from all tasks and comments that arent deleted --- src/cmd/backfill-attachments/index.ts | 149 ++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/cmd/backfill-attachments/index.ts diff --git a/src/cmd/backfill-attachments/index.ts b/src/cmd/backfill-attachments/index.ts new file mode 100644 index 000000000..f82fc7838 --- /dev/null +++ b/src/cmd/backfill-attachments/index.ts @@ -0,0 +1,149 @@ +import DBClient from '@/lib/db' +import { CreateAttachmentRequestSchema } from '@/types/dto/attachments.dto' +import { getFilePathFromUrl } from '@/utils/signedUrlReplacer' +import { SupabaseActions } from '@/utils/SupabaseActions' +import { Task, Comment } from '@prisma/client' + +const ATTACHMENT_TAG_REGEX = /<\s*[a-zA-Z]+\s+[^>]*data-type="attachment"[^>]*src="([^"]+)"[^>]*>/g +const IMG_TAG_REGEX = /]*src="([^"]+)"[^>]*>/g + +interface AttachmentRequest { + createdById: string + workspaceId: string + attachmentRequest: ReturnType +} + +interface ProcessedAttachments { + taskAttachmentRequests: AttachmentRequest[] + commentAttachmentRequests: AttachmentRequest[] + filesNotFoundInBucket: string[] +} + +async function extractAttachmentsFromContent( + content: string, + supabaseActions: SupabaseActions, + filesNotFound: string[], +): Promise> { + const attachments: Array<{ filePath: string; fileSize?: number; fileType?: string; fileName?: string }> = [] + const regexes = [IMG_TAG_REGEX, ATTACHMENT_TAG_REGEX] + + for (const regex of regexes) { + let match + regex.lastIndex = 0 + while ((match = regex.exec(content)) !== null) { + const originalSrc = match[1] + const filePath = getFilePathFromUrl(originalSrc) + if (!filePath) continue + const fileMetaData = await supabaseActions.getMetaData(filePath) + if (!fileMetaData) { + filesNotFound.push(filePath) + continue + } + const fileName = filePath.split('/').pop() + attachments.push({ + filePath, + fileSize: fileMetaData.size, + fileType: fileMetaData.contentType, + fileName, + }) + } + } + return attachments +} + +async function createAttachmentRequests(tasks: Task[], comments: Comment[]): Promise { + const taskAttachmentRequests: AttachmentRequest[] = [] + const commentAttachmentRequests: AttachmentRequest[] = [] + const filesNotFoundInBucket: string[] = [] + const supabaseActions = new SupabaseActions() + + for (const task of tasks) { + const bodyString = task.body ?? '' + const attachments = await extractAttachmentsFromContent(bodyString, supabaseActions, filesNotFoundInBucket) + for (const attachment of attachments) { + taskAttachmentRequests.push({ + createdById: task.createdById, + workspaceId: task.workspaceId, + attachmentRequest: CreateAttachmentRequestSchema.parse({ + taskId: task.id, + ...attachment, + }), + }) + } + } + + for (const comment of comments) { + const contentString = comment.content ?? '' + const attachments = await extractAttachmentsFromContent(contentString, supabaseActions, filesNotFoundInBucket) + for (const attachment of attachments) { + commentAttachmentRequests.push({ + createdById: comment.initiatorId, + workspaceId: comment.workspaceId, + attachmentRequest: CreateAttachmentRequestSchema.parse({ + commentId: comment.id, + ...attachment, + }), + }) + } + } + + if (taskAttachmentRequests.length) { + console.info('🔥 Task attachments to be populated:', taskAttachmentRequests.length) + } + if (commentAttachmentRequests.length) { + console.info('🔥 Comment attachments to be populated:', commentAttachmentRequests.length) + } + if (filesNotFoundInBucket.length) { + console.warn('⚠️ Files not found in bucket:', filesNotFoundInBucket) + } + + return { taskAttachmentRequests, commentAttachmentRequests, filesNotFoundInBucket } +} + +async function createAttachmentsInDatabase( + db: ReturnType, + attachmentRequests: AttachmentRequest[], +) { + let created = 0 + let skipped = 0 + + for (const { createdById, workspaceId, attachmentRequest } of attachmentRequests) { + try { + const existing = await db.attachment.findFirst({ + where: { filePath: attachmentRequest.filePath }, + }) + if (existing) { + skipped++ + continue + } + await db.attachment.create({ + data: { + ...attachmentRequest, + createdById, + workspaceId, + }, + }) + created++ + } catch (error) { + console.error('❌ Failed to create attachment:', attachmentRequest, error) + } + } + + console.info(`📊 Created: ${created}, Skipped (already exists): ${skipped}`) +} + +async function run() { + console.info('🧑🏻‍💻 Backfilling attachment entries for tasks and comments') + + const db = DBClient.getInstance() + const [tasks, comments] = await Promise.all([db.task.findMany(), db.comment.findMany()]) + + const { taskAttachmentRequests, commentAttachmentRequests } = await createAttachmentRequests(tasks, comments) + + await createAttachmentsInDatabase(db, taskAttachmentRequests) + await createAttachmentsInDatabase(db, commentAttachmentRequests) + + console.info('✅ Backfill complete') +} + +run() From d54a5f707fc8b2da63ec6a060816af95a66d29c1 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 23 Jan 2026 11:18:39 +0545 Subject: [PATCH 2/6] fix(OUT-2915): parallely inserted taskAttachmentRequests and commentAttachmentRequests in db --- src/cmd/backfill-attachments/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmd/backfill-attachments/index.ts b/src/cmd/backfill-attachments/index.ts index f82fc7838..b728f5d37 100644 --- a/src/cmd/backfill-attachments/index.ts +++ b/src/cmd/backfill-attachments/index.ts @@ -140,8 +140,10 @@ async function run() { const { taskAttachmentRequests, commentAttachmentRequests } = await createAttachmentRequests(tasks, comments) - await createAttachmentsInDatabase(db, taskAttachmentRequests) - await createAttachmentsInDatabase(db, commentAttachmentRequests) + await Promise.all([ + createAttachmentsInDatabase(db, taskAttachmentRequests), + createAttachmentsInDatabase(db, commentAttachmentRequests), + ]) console.info('✅ Backfill complete') } From 23a00a1e1e5b7fb46e3800fe206b54302c7e703d Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 23 Jan 2026 11:19:41 +0545 Subject: [PATCH 3/6] fix(OUT-2915): parallely inserted taskAttachmentRequests and commentAttachmentRequests in db --- src/cmd/backfill-attachments/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cmd/backfill-attachments/index.ts b/src/cmd/backfill-attachments/index.ts index b728f5d37..f18bbe630 100644 --- a/src/cmd/backfill-attachments/index.ts +++ b/src/cmd/backfill-attachments/index.ts @@ -140,10 +140,7 @@ async function run() { const { taskAttachmentRequests, commentAttachmentRequests } = await createAttachmentRequests(tasks, comments) - await Promise.all([ - createAttachmentsInDatabase(db, taskAttachmentRequests), - createAttachmentsInDatabase(db, commentAttachmentRequests), - ]) + await createAttachmentsInDatabase(db, [...taskAttachmentRequests, ...commentAttachmentRequests]) console.info('✅ Backfill complete') } From 89f25068e717728370acf86c5952fda235a27a5d Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Mon, 26 Jan 2026 17:12:26 +0545 Subject: [PATCH 4/6] fix(OUT-2939): added support for comments attachments deletion from supabase buckets in scrap medias. cleaned out some services --- prisma/schema/comment.prisma | 4 ++ prisma/schema/scrapMedia.prisma | 4 ++ src/app/api/comments/comment.service.ts | 22 ++++----- .../scrap-medias/scrap-medias.service.ts | 47 ++++++++++++------- src/app/detail/ui/NewTaskCard.tsx | 4 +- src/app/detail/ui/TaskEditor.tsx | 4 +- .../manage-templates/ui/NewTemplateCard.tsx | 2 +- .../manage-templates/ui/TemplateDetails.tsx | 4 +- src/app/manage-templates/ui/TemplateForm.tsx | 9 +--- src/app/ui/NewTaskForm.tsx | 3 +- src/components/cards/CommentCard.tsx | 9 +++- src/components/cards/ReplyCard.tsx | 9 +++- src/components/inputs/CommentInput.tsx | 3 +- src/components/inputs/ReplyInput.tsx | 3 +- src/types/common.ts | 5 +- src/utils/attachmentUtils.ts | 13 +++-- 16 files changed, 92 insertions(+), 53 deletions(-) diff --git a/prisma/schema/comment.prisma b/prisma/schema/comment.prisma index 430c3ac68..f365abf48 100644 --- a/prisma/schema/comment.prisma +++ b/prisma/schema/comment.prisma @@ -21,6 +21,10 @@ model Comment { updatedAt DateTime @updatedAt @db.Timestamptz() deletedAt DateTime? @db.Timestamptz() + + scrapMedias ScrapMedia[] + + @@map("Comments") @@index([taskId, workspaceId, createdAt(sort: Desc)], name: "IX_Comments_taskId_workspaceId_createdAt") } diff --git a/prisma/schema/scrapMedia.prisma b/prisma/schema/scrapMedia.prisma index 738f5c499..2f49c8eea 100644 --- a/prisma/schema/scrapMedia.prisma +++ b/prisma/schema/scrapMedia.prisma @@ -8,6 +8,10 @@ model ScrapMedia { deletedAt DateTime? @db.Timestamptz() templateId String? @db.Uuid template TaskTemplate? @relation(fields: [templateId], references: [id], onDelete: Cascade) + commentId String? @db.Uuid + comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) + + @@index([createdAt]) @@index([filePath]) diff --git a/src/app/api/comments/comment.service.ts b/src/app/api/comments/comment.service.ts index e9a8d61c8..fe8286143 100755 --- a/src/app/api/comments/comment.service.ts +++ b/src/app/api/comments/comment.service.ts @@ -388,17 +388,17 @@ export class CommentService extends BaseService { for (const { originalSrc, newUrl } of replacements) { htmlString = htmlString.replace(originalSrc, newUrl) } - // const filePaths = newFilePaths.map(({ newFilePath }) => newFilePath) - // await this.db.scrapMedia.updateMany({ - // where: { - // filePath: { - // in: filePaths, - // }, - // }, - // data: { - // taskId: task_id, - // }, - // }) //todo: add support for commentId in scrapMedias. + const filePaths = newFilePaths.map(({ newFilePath }) => newFilePath) + await this.db.scrapMedia.updateMany({ + where: { + filePath: { + in: filePaths, + }, + }, + data: { + commentId: commentId, + }, + }) return htmlString } //todo: make this resuable since this is highly similar to what we are doing on tasks. diff --git a/src/app/api/workers/scrap-medias/scrap-medias.service.ts b/src/app/api/workers/scrap-medias/scrap-medias.service.ts index ca637f7e8..5b3038ecb 100644 --- a/src/app/api/workers/scrap-medias/scrap-medias.service.ts +++ b/src/app/api/workers/scrap-medias/scrap-medias.service.ts @@ -29,6 +29,10 @@ export class ScrapMediaService { .map((image) => image.templateId) .filter((templateId): templateId is string => templateId !== null) + const commentIds = scrapMedias + .map((medias) => medias.commentId) + .filter((commentId): commentId is string => commentId !== null) + const tasks = taskIds.length ? await db.task.findMany({ where: { @@ -46,35 +50,46 @@ export class ScrapMediaService { }) : [] + const comments = + commentIds.length > 0 + ? await db.comment.findMany({ + where: { + id: { in: commentIds }, + }, + }) + : [] + const scrapMediasToDelete = [] const scrapMediasToDeleteFromBucket = [] - for (const image of scrapMedias) { + for (const media of scrapMedias) { try { // For each scrap image, check if the task or taskTemplate still has the img url in its body - const task = tasks.find((_task) => _task.id === image.taskId) - const taskTemplate = taskTemplates.find((_template) => _template.id === image.templateId) + const task = tasks.find((_task) => _task.id === media.taskId) + const taskTemplate = taskTemplates.find((_template) => _template.id === media.templateId) + const comment = comments.find((_comment) => _comment.id === media.commentId) - const isInTaskBody = task && (task.body || '').includes(image.filePath) - const isInTemplateBody = taskTemplate && (taskTemplate.body || '').includes(image.filePath) + const isInTaskBody = task && (task.body || '').includes(media.filePath) + const isInTemplateBody = taskTemplate && (taskTemplate.body || '').includes(media.filePath) + const isInCommentBody = comment && (comment.content || '').includes(media.filePath) - if (!task && !taskTemplate) { - console.error('Could not find task for scrap image', image) - scrapMediasToDelete.push(image.id) - scrapMediasToDeleteFromBucket.push(image.filePath) + if (!task && !taskTemplate && !comment) { + console.error('Could not find location of scrap media', media) + scrapMediasToDelete.push(media.id) + scrapMediasToDeleteFromBucket.push(media.filePath) continue } - // If image is in task body - if (isInTaskBody || isInTemplateBody) { - scrapMediasToDelete.push(image.id) + // If media is valid + if (isInTaskBody || isInTemplateBody || isInCommentBody) { + scrapMediasToDelete.push(media.id) continue } - // If image is not in task body + // If media is not valid - scrapMediasToDeleteFromBucket.push(image.filePath) - scrapMediasToDelete.push(image.id) + scrapMediasToDeleteFromBucket.push(media.filePath) + scrapMediasToDelete.push(media.id) } catch (e: unknown) { - console.error('Error processing scrap image', e) + console.error('Error processing scrap media', e) } } diff --git a/src/app/detail/ui/NewTaskCard.tsx b/src/app/detail/ui/NewTaskCard.tsx index c5105b79e..a115d2f50 100644 --- a/src/app/detail/ui/NewTaskCard.tsx +++ b/src/app/detail/ui/NewTaskCard.tsx @@ -20,7 +20,7 @@ import { selectCreateTemplate } from '@/redux/features/templateSlice' import { DateString } from '@/types/date' import { CreateTaskRequest, Viewers } from '@/types/dto/tasks.dto' import { WorkflowStateResponse } from '@/types/dto/workflowStates.dto' -import { FilterByOptions, IAssigneeCombined, InputValue, ITemplate, UserIds } from '@/types/interfaces' +import { AttachmentTypes, FilterByOptions, IAssigneeCombined, InputValue, ITemplate, UserIds } from '@/types/interfaces' import { getAssigneeName, UserIdsType } from '@/utils/assignee' import { deleteEditorAttachmentsHandler, uploadAttachmentHandler } from '@/utils/attachmentUtils' import { createUploadFn } from '@/utils/createUploadFn' @@ -340,7 +340,7 @@ export const NewTaskCard = ({ placeholder="Add description.." editorClass="tapwrite-task-editor" uploadFn={uploadFn} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', null, null)} + deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.TASK)} attachmentLayout={(props) => ( )} diff --git a/src/app/detail/ui/TaskEditor.tsx b/src/app/detail/ui/TaskEditor.tsx index e264e5a07..1108b9504 100644 --- a/src/app/detail/ui/TaskEditor.tsx +++ b/src/app/detail/ui/TaskEditor.tsx @@ -12,7 +12,7 @@ import { selectTaskDetails, setOpenImage, setShowConfirmDeleteModal } from '@/re import store from '@/redux/store' import { CreateAttachmentRequest } from '@/types/dto/attachments.dto' import { TaskResponse } from '@/types/dto/tasks.dto' -import { UserType } from '@/types/interfaces' +import { AttachmentTypes, UserType } from '@/types/interfaces' import { getDeleteMessage } from '@/utils/dialogMessages' import { deleteEditorAttachmentsHandler, getAttachmentPayload, uploadAttachmentHandler } from '@/utils/attachmentUtils' import { Box } from '@mui/material' @@ -195,7 +195,7 @@ export const TaskEditor = ({ placeholder="Add description..." uploadFn={uploadFn} handleImageDoubleClick={handleImagePreview} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} + deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.TASK, task_id)} attachmentLayout={(props) => } addAttachmentButton maxUploadLimit={MAX_UPLOAD_LIMIT} diff --git a/src/app/manage-templates/ui/NewTemplateCard.tsx b/src/app/manage-templates/ui/NewTemplateCard.tsx index 919afd292..c50aa1183 100644 --- a/src/app/manage-templates/ui/NewTemplateCard.tsx +++ b/src/app/manage-templates/ui/NewTemplateCard.tsx @@ -156,7 +156,7 @@ export const NewTemplateCard = ({ placeholder="Add description.." editorClass="tapwrite-task-editor" uploadFn={uploadFn} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', null, null)} + deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.TEMPLATE)} attachmentLayout={(props) => ( )} diff --git a/src/app/manage-templates/ui/TemplateDetails.tsx b/src/app/manage-templates/ui/TemplateDetails.tsx index b9d6dfcba..836b8dd10 100644 --- a/src/app/manage-templates/ui/TemplateDetails.tsx +++ b/src/app/manage-templates/ui/TemplateDetails.tsx @@ -164,7 +164,9 @@ export default function TemplateDetails({ placeholder="Add description..." uploadFn={uploadFn} handleImageDoubleClick={handleImagePreview} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', template_id, null)} + deleteEditorAttachments={(url) => + deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.TEMPLATE, template_id) + } attachmentLayout={(props) => } addAttachmentButton maxUploadLimit={MAX_UPLOAD_LIMIT} diff --git a/src/app/manage-templates/ui/TemplateForm.tsx b/src/app/manage-templates/ui/TemplateForm.tsx index b5ff8d4f5..34bdfbe9d 100644 --- a/src/app/manage-templates/ui/TemplateForm.tsx +++ b/src/app/manage-templates/ui/TemplateForm.tsx @@ -160,14 +160,7 @@ const NewTemplateFormInputs = () => { placeholder="Add description.." editorClass="tapwrite-description-h-full" uploadFn={uploadFn} - deleteEditorAttachments={(url) => - deleteEditorAttachmentsHandler( - url, - token ?? '', - null, - targetMethod == TargetMethod.POST ? null : targetTemplateId, - ) - } + deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.TEMPLATE)} attachmentLayout={(props) => } maxUploadLimit={MAX_UPLOAD_LIMIT} parentContainerStyle={{ gap: '0px', minHeight: '60px' }} diff --git a/src/app/ui/NewTaskForm.tsx b/src/app/ui/NewTaskForm.tsx index 016ea5790..feb3ef7d0 100644 --- a/src/app/ui/NewTaskForm.tsx +++ b/src/app/ui/NewTaskForm.tsx @@ -32,6 +32,7 @@ import store from '@/redux/store' import { HomeParamActions } from '@/types/constants' import { WorkflowStateResponse } from '@/types/dto/workflowStates.dto' import { + AttachmentTypes, CreateTaskErrors, FilterByOptions, FilterOptions, @@ -619,7 +620,7 @@ const NewTaskFormInputs = ({ isEditorReadonly }: NewTaskFormInputsProps) => { editorClass="tapwrite-description-h-full" uploadFn={uploadFn} readonly={isEditorReadonly} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', null, null)} + deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.TASK)} attachmentLayout={(props) => } maxUploadLimit={MAX_UPLOAD_LIMIT} parentContainerStyle={{ gap: '0px', minHeight: '60px' }} diff --git a/src/components/cards/CommentCard.tsx b/src/components/cards/CommentCard.tsx index 3c3540f03..70a6bb643 100644 --- a/src/components/cards/CommentCard.tsx +++ b/src/components/cards/CommentCard.tsx @@ -330,7 +330,14 @@ export const CommentCard = ({ editorClass={isReadOnly ? 'tapwrite-comment' : 'tapwrite-comment-editable'} addAttachmentButton={!isReadOnly} uploadFn={uploadFn} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} + deleteEditorAttachments={(url) => + deleteEditorAttachmentsHandler( + url, + token ?? '', + AttachmentTypes.COMMENT, + z.string().parse(commentIdRef.current), + ) + } maxUploadLimit={MAX_UPLOAD_LIMIT} attachmentLayout={(props) => } hardbreak diff --git a/src/components/cards/ReplyCard.tsx b/src/components/cards/ReplyCard.tsx index b40801ba7..4c445f4d3 100644 --- a/src/components/cards/ReplyCard.tsx +++ b/src/components/cards/ReplyCard.tsx @@ -247,7 +247,14 @@ export const ReplyCard = ({ editorClass={isReadOnly ? 'tapwrite-comment' : 'tapwrite-comment-editable'} addAttachmentButton={!isReadOnly} uploadFn={uploadFn} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} + deleteEditorAttachments={(url) => + deleteEditorAttachmentsHandler( + url, + token ?? '', + AttachmentTypes.COMMENT, + z.string().parse(commentIdRef.current), + ) + } maxUploadLimit={MAX_UPLOAD_LIMIT} attachmentLayout={(props) => } hardbreak diff --git a/src/components/inputs/CommentInput.tsx b/src/components/inputs/CommentInput.tsx index 3d0995d9c..9b4592f98 100644 --- a/src/components/inputs/CommentInput.tsx +++ b/src/components/inputs/CommentInput.tsx @@ -10,6 +10,7 @@ import { selectAuthDetails } from '@/redux/features/authDetailsSlice' import { selectTaskBoard } from '@/redux/features/taskBoardSlice' import { CreateAttachmentRequest } from '@/types/dto/attachments.dto' import { CreateComment } from '@/types/dto/comment.dto' +import { AttachmentTypes } from '@/types/interfaces' import { deleteEditorAttachmentsHandler, uploadAttachmentHandler } from '@/utils/attachmentUtils' import { createUploadFn } from '@/utils/createUploadFn' import { getMentionsList } from '@/utils/getMentionList' @@ -171,7 +172,7 @@ export const CommentInput = ({ createComment, task_id, token }: Prop) => { whiteSpace: 'pre-wrap', }} uploadFn={uploadFn} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} + deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.COMMENT)} attachmentLayout={(props) => ( )} diff --git a/src/components/inputs/ReplyInput.tsx b/src/components/inputs/ReplyInput.tsx index 3deb1bf01..fd1b125b1 100644 --- a/src/components/inputs/ReplyInput.tsx +++ b/src/components/inputs/ReplyInput.tsx @@ -17,6 +17,7 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } fr import { useSelector } from 'react-redux' import { Tapwrite } from 'tapwrite' import { createUploadFn } from '@/utils/createUploadFn' +import { AttachmentTypes } from '@/types/interfaces' interface ReplyInputProps { token: string @@ -210,7 +211,7 @@ export const ReplyInput = ({ flexGrow: 1, }} addAttachmentButton - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} + deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.COMMENT)} uploadFn={uploadFn} maxUploadLimit={MAX_UPLOAD_LIMIT} endButtons={} diff --git a/src/types/common.ts b/src/types/common.ts index eb18881fc..55146a247 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -216,8 +216,9 @@ export const NotificationRequestBodySchema = z export const ScrapMediaRequestSchema = z.object({ filePath: z.string(), - taskId: z.string().uuid().nullable(), - templateId: z.string().uuid().nullable(), + taskId: z.string().uuid().optional(), + templateId: z.string().uuid().optional(), + commentId: z.string().uuid().optional(), }) export type ScrapMediaRequest = z.infer diff --git a/src/utils/attachmentUtils.ts b/src/utils/attachmentUtils.ts index d2ff7edad..ccc172da0 100644 --- a/src/utils/attachmentUtils.ts +++ b/src/utils/attachmentUtils.ts @@ -55,15 +55,18 @@ export const uploadAttachmentHandler = async ( export const deleteEditorAttachmentsHandler = async ( url: string, token: string, - task_id: string | null, - template_id: string | null, + entityType: AttachmentTypes, + entityId?: string, ) => { const filePath = getFilePathFromUrl(url) if (filePath) { const payload: ScrapMediaRequest = { - filePath: filePath, - taskId: task_id, - templateId: template_id, + filePath, + ...(entityType === AttachmentTypes.TASK + ? { taskId: entityId } + : entityType === AttachmentTypes.TEMPLATE + ? { templateId: entityId } + : { commentId: entityId }), } postScrapMedia(token, payload) } From 70bd98c45324ae80c722d5505fb11bf02ea548ac Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Mon, 26 Jan 2026 17:12:47 +0545 Subject: [PATCH 5/6] fix(OUT-2939): added support for comments attachments deletion from supabase buckets in scrap medias. cleaned out some services --- .../20260126103335_comment_id_in_scrap_medias/migration.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 prisma/migrations/20260126103335_comment_id_in_scrap_medias/migration.sql diff --git a/prisma/migrations/20260126103335_comment_id_in_scrap_medias/migration.sql b/prisma/migrations/20260126103335_comment_id_in_scrap_medias/migration.sql new file mode 100644 index 000000000..4e5f2a216 --- /dev/null +++ b/prisma/migrations/20260126103335_comment_id_in_scrap_medias/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "ScrapMedias" ADD COLUMN "commentId" UUID; + +-- AddForeignKey +ALTER TABLE "ScrapMedias" ADD CONSTRAINT "ScrapMedias_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comments"("id") ON DELETE CASCADE ON UPDATE CASCADE; From 4c896c7b451eae987192517d8623ff7143f5583d Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Thu, 29 Jan 2026 15:04:51 +0545 Subject: [PATCH 6/6] fix(OUT-2939): comments attachments deletion from supabase bucket. fixed the issue of stale url going to scrapMedias table for only comments and replies --- .../scrap-medias/scrap-medias.service.ts | 3 +-- src/components/cards/CommentCard.tsx | 17 ++++++++--------- src/components/cards/ReplyCard.tsx | 17 ++++++++--------- src/utils/attachmentUtils.ts | 12 +++++++++++- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/app/api/workers/scrap-medias/scrap-medias.service.ts b/src/app/api/workers/scrap-medias/scrap-medias.service.ts index 5b3038ecb..aa0752f81 100644 --- a/src/app/api/workers/scrap-medias/scrap-medias.service.ts +++ b/src/app/api/workers/scrap-medias/scrap-medias.service.ts @@ -85,7 +85,6 @@ export class ScrapMediaService { continue } // If media is not valid - scrapMediasToDeleteFromBucket.push(media.filePath) scrapMediasToDelete.push(media.id) } catch (e: unknown) { @@ -95,7 +94,7 @@ export class ScrapMediaService { if (!!scrapMediasToDeleteFromBucket.length) await db.attachment.deleteMany({ where: { filePath: { in: scrapMediasToDeleteFromBucket } } }) - + console.info('ScrapMediaWorker#deleteFromBucket | Deleting these medias', scrapMediasToDeleteFromBucket) // remove attachments from bucket await supabase.removeAttachmentsFromBucket(scrapMediasToDeleteFromBucket) diff --git a/src/components/cards/CommentCard.tsx b/src/components/cards/CommentCard.tsx index 70a6bb643..d408aed74 100644 --- a/src/components/cards/CommentCard.tsx +++ b/src/components/cards/CommentCard.tsx @@ -29,7 +29,7 @@ import store from '@/redux/store' import { CommentResponse, CreateComment, UpdateComment } from '@/types/dto/comment.dto' import { AttachmentTypes, IAssigneeCombined } from '@/types/interfaces' import { getAssigneeName } from '@/utils/assignee' -import { deleteEditorAttachmentsHandler, getAttachmentPayload } from '@/utils/attachmentUtils' +import { deleteEditorAttachmentsHandler, getAttachmentPayload, getCustomFilePath } from '@/utils/attachmentUtils' import { createUploadFn } from '@/utils/createUploadFn' import { fetcher } from '@/utils/fetcher' import { getTimeDifference } from '@/utils/getTimeDifference' @@ -330,14 +330,13 @@ export const CommentCard = ({ editorClass={isReadOnly ? 'tapwrite-comment' : 'tapwrite-comment-editable'} addAttachmentButton={!isReadOnly} uploadFn={uploadFn} - deleteEditorAttachments={(url) => - deleteEditorAttachmentsHandler( - url, - token ?? '', - AttachmentTypes.COMMENT, - z.string().parse(commentIdRef.current), - ) - } + deleteEditorAttachments={(url) => { + const commentId = z.string().parse(commentIdRef.current) + const customFilePath = tokenPayload?.workspaceId + ? getCustomFilePath(tokenPayload?.workspaceId, task_id, commentId, url) + : undefined + return deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.COMMENT, commentId, customFilePath) + }} maxUploadLimit={MAX_UPLOAD_LIMIT} attachmentLayout={(props) => } hardbreak diff --git a/src/components/cards/ReplyCard.tsx b/src/components/cards/ReplyCard.tsx index 4c445f4d3..23a5a8d1e 100644 --- a/src/components/cards/ReplyCard.tsx +++ b/src/components/cards/ReplyCard.tsx @@ -19,7 +19,7 @@ import { selectTaskBoard } from '@/redux/features/taskBoardSlice' import { UpdateComment } from '@/types/dto/comment.dto' import { AttachmentTypes, IAssigneeCombined } from '@/types/interfaces' import { getAssigneeName } from '@/utils/assignee' -import { deleteEditorAttachmentsHandler, getAttachmentPayload } from '@/utils/attachmentUtils' +import { deleteEditorAttachmentsHandler, getAttachmentPayload, getCustomFilePath } from '@/utils/attachmentUtils' import { createUploadFn } from '@/utils/createUploadFn' import { getTimeDifference } from '@/utils/getTimeDifference' import { isTapwriteContentEmpty } from '@/utils/isTapwriteContentEmpty' @@ -247,14 +247,13 @@ export const ReplyCard = ({ editorClass={isReadOnly ? 'tapwrite-comment' : 'tapwrite-comment-editable'} addAttachmentButton={!isReadOnly} uploadFn={uploadFn} - deleteEditorAttachments={(url) => - deleteEditorAttachmentsHandler( - url, - token ?? '', - AttachmentTypes.COMMENT, - z.string().parse(commentIdRef.current), - ) - } + deleteEditorAttachments={(url) => { + const commentId = z.string().parse(commentIdRef.current) + const customFilePath = tokenPayload?.workspaceId + ? getCustomFilePath(tokenPayload?.workspaceId, task_id, commentId, url) + : undefined + return deleteEditorAttachmentsHandler(url, token ?? '', AttachmentTypes.COMMENT, commentId, customFilePath) + }} maxUploadLimit={MAX_UPLOAD_LIMIT} attachmentLayout={(props) => } hardbreak diff --git a/src/utils/attachmentUtils.ts b/src/utils/attachmentUtils.ts index ccc172da0..12d9af463 100644 --- a/src/utils/attachmentUtils.ts +++ b/src/utils/attachmentUtils.ts @@ -57,11 +57,12 @@ export const deleteEditorAttachmentsHandler = async ( token: string, entityType: AttachmentTypes, entityId?: string, + customFilePath?: string, //used only for comments and replies. Because newly created comments and replies have mismatched urls. And the url doesnt refresh without a page refresh. ) => { const filePath = getFilePathFromUrl(url) if (filePath) { const payload: ScrapMediaRequest = { - filePath, + filePath: customFilePath ?? filePath, ...(entityType === AttachmentTypes.TASK ? { taskId: entityId } : entityType === AttachmentTypes.TEMPLATE @@ -95,3 +96,12 @@ export const getFileNameFromPath = (path: string): string => { const segments = path.split('/').filter(Boolean) return segments[segments.length - 1] || '' } + +export const getCustomFilePath = (workspaceId: string, task_id: string, commentId: string, url: string) => { + const filePath = getFilePathFromUrl(url) + if (!filePath) { + return undefined + } + const fileName = getFileNameFromPath(filePath) + return `${workspaceId}/${task_id}/comments/${commentId}/${fileName}` +}