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; 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..aa0752f81 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,41 +50,51 @@ 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 - - scrapMediasToDeleteFromBucket.push(image.filePath) - scrapMediasToDelete.push(image.id) + // If media is not valid + 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) } } 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/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/cmd/backfill-attachments/index.ts b/src/cmd/backfill-attachments/index.ts new file mode 100644 index 000000000..f18bbe630 --- /dev/null +++ b/src/cmd/backfill-attachments/index.ts @@ -0,0 +1,148 @@ +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, ...commentAttachmentRequests]) + + console.info('✅ Backfill complete') +} + +run() diff --git a/src/components/cards/CommentCard.tsx b/src/components/cards/CommentCard.tsx index 3c3540f03..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,7 +330,13 @@ export const CommentCard = ({ editorClass={isReadOnly ? 'tapwrite-comment' : 'tapwrite-comment-editable'} addAttachmentButton={!isReadOnly} uploadFn={uploadFn} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} + 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 b40801ba7..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,7 +247,13 @@ export const ReplyCard = ({ editorClass={isReadOnly ? 'tapwrite-comment' : 'tapwrite-comment-editable'} addAttachmentButton={!isReadOnly} uploadFn={uploadFn} - deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} + 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/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..12d9af463 100644 --- a/src/utils/attachmentUtils.ts +++ b/src/utils/attachmentUtils.ts @@ -55,15 +55,19 @@ export const uploadAttachmentHandler = async ( export const deleteEditorAttachmentsHandler = async ( url: string, token: string, - task_id: string | null, - template_id: string | null, + 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, - taskId: task_id, - templateId: template_id, + filePath: customFilePath ?? filePath, + ...(entityType === AttachmentTypes.TASK + ? { taskId: entityId } + : entityType === AttachmentTypes.TEMPLATE + ? { templateId: entityId } + : { commentId: entityId }), } postScrapMedia(token, payload) } @@ -92,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}` +}