Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
9d43cf7
chore: prep for Next.js 16 upgrade
arpandhakal Dec 29, 2025
97c974b
feat(OUT-2844): upgrade next version to 16
arpandhakal Dec 30, 2025
d0a0ced
feat(OUT-2844): upgrade next version to 16
arpandhakal Dec 30, 2025
653741a
feat(OUT-2844): upgrade next version to 16
arpandhakal Dec 30, 2025
a91fe7d
feat(OUT-2844): upgrade next version to 16
arpandhakal Dec 30, 2025
e5b7539
fix(OUT-2844): stylish js text-table dependency issue
arpandhakal Dec 30, 2025
31290a6
feat(OUT-2844): upgrade next version to 16
arpandhakal Dec 30, 2025
0d9715d
feat(OUT-2844): upgrade next version to 16
arpandhakal Dec 30, 2025
135f14d
feat(OUT-2844): upgrade next version to 16
arpandhakal Dec 30, 2025
a59b647
fix(OUT-2844): added text-table as dev dependency to fix linting issu…
arpandhakal Dec 30, 2025
c36179d
fix(OUT-2844): added react-hooks/static-components rule and fixed rel…
arpandhakal Dec 30, 2025
ae6ad91
fix(OUT-2844): reverted turning off reactCompiler in next config
arpandhakal Dec 31, 2025
6e74af5
fix(OUT-2844): fixed nested link tags, caching and removed customScro…
arpandhakal Jan 12, 2026
a616deb
fix(OUT-2848): refactored tempalte realtime pattern.
arpandhakal Jan 5, 2026
827d762
fix(OUT-2847): seggregate task service for public and web
arpandhakal Jan 6, 2026
464f2ab
fix(OUT-2847): refactored createSubtasksFromTemplate to be resuable f…
arpandhakal Jan 7, 2026
b30416c
fix(OUT-2847): file naming convention followed and changed access sco…
arpandhakal Jan 12, 2026
314fd7b
fix(OUT-2911): sidebar skeleton layout fixed
arpandhakal Jan 13, 2026
7a26c7e
feat(OUT-2929): disable sending errors to sentry from preview apps
arpandhakal Jan 14, 2026
166d3b0
fix(OUT-2929): cleanup
arpandhakal Jan 14, 2026
b1b4960
fix(OUT-2929): enabled transaction events for preview app in sentry c…
arpandhakal Jan 14, 2026
28a0d4b
tryfix: only suppress error type telemetry
rrojan Jan 14, 2026
f6f2d26
tryfix: only suppress error type telemetry
rrojan Jan 14, 2026
4553aa3
tryfix: only suppress error type telemetry
rrojan Jan 14, 2026
60f2780
fix(OUT-2937): manage template button missing.
arpandhakal Jan 16, 2026
f109e2e
fix(out-2943): fix alignment of search and filter icons (#1095)
priosshrsth Jan 19, 2026
7f335fc
fix(OUT-2944): enabled feature flag to retry assembly api calls on 40…
arpandhakal Jan 19, 2026
e838a62
fix(OUT-2944): instantiate copilotAPI if apikey mismatch
SandipBajracharya Jan 22, 2026
f38eca2
fix(OUT-2944-re): removed globalThis on base service
arpandhakal Jan 23, 2026
4a2b05a
hotfix: increase max duration to 300s for validate count
SandipBajracharya Jan 27, 2026
de05282
feat(OUT-2914): store to attachment table when attachment is uploaded.
arpandhakal Jan 15, 2026
1ade7fd
feat(OUT-2914): attachments table create and delete entries progress
arpandhakal Jan 16, 2026
f61fb7c
fix(OUT-2914): refactoring + fixed replied/comments attachment creati…
arpandhakal Jan 19, 2026
1a9e501
fix(OUT-2914): refactoring + fixed replied/comments attachment creati…
arpandhakal Jan 19, 2026
685c684
fix(OUT-2914): applied requested changes, heavy refactoring
arpandhakal Jan 20, 2026
8be8e9d
fix(OUT-2914): added a check for workspace id before uploading an att…
arpandhakal Jan 20, 2026
3116f9d
feat(OUT-2917): public api to list comments of a task
SandipBajracharya Jan 14, 2026
b263def
refactor(OUT-2917): expose comments list route as sub-resource on tasks
SandipBajracharya Jan 14, 2026
b7dcd97
fix(OUT-2917): await path params
SandipBajracharya Jan 15, 2026
16a8565
refactor(OUT-2917): implemented proper typing, validation
SandipBajracharya Jan 15, 2026
7be51f8
perf(OUT-2917): index comment table and get multiple signed urls from…
SandipBajracharya Jan 15, 2026
e200100
fix(OUT-2917): sequentially map the attachments
SandipBajracharya Jan 15, 2026
c540cac
feat(OUT-2919): create public api to read single comment of a task
SandipBajracharya Jan 15, 2026
9ae1fcf
fix(OUT-2919): await path params
SandipBajracharya Jan 15, 2026
9921f34
refactor(OUT-2919): use object parameter in function
SandipBajracharya Jan 15, 2026
8f32709
feat(OUT-2917): public api to list comments of a task
SandipBajracharya Jan 14, 2026
a7ddc27
feat(OUT-2920): create public API to delete a comment
SandipBajracharya Jan 15, 2026
2fe0114
fix(OUT-2920): remove double file import
SandipBajracharya Jan 21, 2026
e85fa68
fix(OUT-2920): file import error
SandipBajracharya Jan 21, 2026
a1bd130
feat(OUT-2938): secure public comments api
SandipBajracharya Jan 21, 2026
823528f
feat(OUT-2920): create public API to delete a comment
SandipBajracharya Jan 15, 2026
9efad99
feat(OUT-2940): delete attachments from bucket when a comment is deleted
SandipBajracharya Jan 16, 2026
a360a61
fix(OUT-2940): remove double file import
SandipBajracharya Jan 21, 2026
09d7826
fix(OUT-2940): not create sign url when attachment is deleted
SandipBajracharya Jan 22, 2026
ab40343
feat(OUT-2921): dispatch webhook event when comment added on task
SandipBajracharya Jan 19, 2026
19b8cf9
refactor(OUT-2921): include attachments in create comment response
SandipBajracharya Jan 19, 2026
22a8568
fix(OUT-2921): remove double file import
SandipBajracharya Jan 26, 2026
de058a8
feat(OUT-2923): include attachments attribute in public tasks api
SandipBajracharya Jan 19, 2026
13c153f
chore(OUT-2923): change download url to have null value if the attach…
SandipBajracharya Jan 19, 2026
2d96d8f
chore(OUT-2923): code cleanup
SandipBajracharya Jan 19, 2026
6e9fc0b
feat(OUT-2923): delete attachments of task when task is deleted
SandipBajracharya Jan 22, 2026
2b3c037
fix(OUT-2923): return download url null for deleted attachments
SandipBajracharya Jan 22, 2026
6cfb215
feat(OUT-2923): filter out attachments that are not available in the …
SandipBajracharya Jan 22, 2026
746c026
feat(OUT-2923): remove commentId null condition
SandipBajracharya Jan 26, 2026
66b9a95
refactor(OUT-2923): rename classes, remove functions with same functi…
SandipBajracharya Jan 26, 2026
63f4e81
feat(OUT-2923): remove attachments from the bucket when a task is del…
SandipBajracharya Jan 26, 2026
6394e92
chore(OUT-2923): remove deletedDate attribute from the attachment res…
SandipBajracharya Jan 26, 2026
98b2457
fix(OUT-2961): include CU to create attachments
SandipBajracharya Jan 26, 2026
cdd4a58
fix(OUT-2961): dispatch comment.created webhook with signed attachments
SandipBajracharya Jan 26, 2026
e40835e
feat(OUT-3009): removed the requirement of taskId in comment endpoints
arpandhakal Jan 27, 2026
a1d3a9d
fix(OUT-3009): applied requested changes, changed api route from api/…
arpandhakal Jan 27, 2026
701d538
fix(OUT-3009): applied requested changes, changed api route from api/…
arpandhakal Jan 27, 2026
82642b2
fix(OUT-3009): some cleaning jobs
arpandhakal Jan 28, 2026
af47c14
fix(OUT-3002): sanitized the contents and body of tasks and comments …
arpandhakal Jan 27, 2026
b7fc575
fix(OUT-3002): added jsdoc to sanitizeContent util
arpandhakal Jan 27, 2026
e6d6673
fix(OUT-3004): Comment attachment fileName should be clean filename.
arpandhakal Jan 28, 2026
3f1bd1b
fix(OUT-3004): applied requested changes
arpandhakal Jan 28, 2026
4936609
fix(OUT-3000): added a backfill script to populate initiatorIds for o…
arpandhakal Jan 28, 2026
268658b
fix(OUT-3000): used object map for quick lookup instead of storing iu…
arpandhakal Jan 28, 2026
18e76be
fix(OUT-3033): if comment not found, threw a 404 error with proper er…
arpandhakal Jan 30, 2026
79944d3
fix(OUT-3060): sub task title in client board truncated very early
arpandhakal Feb 6, 2026
dc21a76
Merge branch 'main' of https://github.com/copilot-platforms/tasks-app…
arpandhakal Feb 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
"cmd:delete-duplicate-notifications": "tsx ./src/cmd/delete-duplicate-notifications",
"cmd:normalize-filterOptions-assignee": "tsx ./src/cmd/normalize-filterOptions-assignee",
"cmd:post-deploy-m15": "tsx ./src/cmd/post-deploy-m15",
"cmd:backfill-attachments": "tsx ./src/cmd/backfill-attachments",
"cmd:backfill-initiatorType-in-comments": "tsx ./src/cmd/backfill-initiatorType-in-comments",
"db:grant-supabase-privileges": "node src/lib/supabase-privilege",
"deploy": "npx trigger.dev@latest deploy",
"dev": "next dev",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "IX_Comments_taskId_workspaceId_createdAt" ON "Comments"("taskId", "workspaceId", "createdAt" DESC);
1 change: 1 addition & 0 deletions prisma/schema/comment.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ model Comment {
deletedAt DateTime? @db.Timestamptz()

@@map("Comments")
@@index([taskId, workspaceId, createdAt(sort: Desc)], name: "IX_Comments_taskId_workspaceId_createdAt")
}
14 changes: 7 additions & 7 deletions sentry.client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ if (dsn) {
integrations: [
Sentry.browserTracingIntegration({
beforeStartSpan: (e) => {
console.info("SentryBrowserTracingSpan", e.name);
return e;
console.info('SentryBrowserTracingSpan', e.name)
return e
},
}),
// Sentry.replayIntegration({
Expand All @@ -43,14 +43,14 @@ if (dsn) {

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null;
return null
}
event.tags = {
...event.tags,
// Adding additional app_env tag for cross-checking
app_env: isProd ? "production" : vercelEnv || "development",
};
return event;
app_env: isProd ? 'production' : vercelEnv || 'development',
}
return event
},
});
})
}
12 changes: 6 additions & 6 deletions sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import * as Sentry from "@sentry/nextjs";

const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN;
const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV;
const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN
const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV
const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'

if (dsn) {
Sentry.init({
Expand All @@ -24,9 +24,9 @@ if (dsn) {

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null;
return null
}
return event;
return event
},
});
})
}
2 changes: 1 addition & 1 deletion src/app/api/activity-logs/services/activity-log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
SchemaByActivityType,
} from '@api/activity-logs/const'
import { LogResponse, LogResponseSchema } from '@api/activity-logs/schemas/LogResponseSchema'
import { CommentService } from '@api/comment/comment.service'
import { CommentService } from '@/app/api/comments/comment.service'
import APIError from '@api/core/exceptions/api'
import User from '@api/core/models/User.model'
import { BaseService } from '@api/core/services/base.service'
Expand Down
62 changes: 59 additions & 3 deletions src/app/api/attachments/attachments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import APIError from '@api/core/exceptions/api'
import httpStatus from 'http-status'
import { SupabaseService } from '@api/core/services/supabase.service'
import { signedUrlTtl } from '@/constants/attachments'
import { PrismaClient } from '@prisma/client'

export class AttachmentsService extends BaseService {
async getAttachments(taskId: string) {
Expand All @@ -30,7 +31,7 @@ export class AttachmentsService extends BaseService {
const newAttachment = await this.db.attachment.create({
data: {
...data,
createdById: z.string().parse(this.user.internalUserId),
createdById: z.string().parse(this.user.internalUserId || this.user.clientId), // CU are also allowed to create attachments
workspaceId: this.user.workspaceId,
},
})
Expand All @@ -40,15 +41,15 @@ export class AttachmentsService extends BaseService {
async createMultipleAttachments(data: CreateAttachmentRequest[]) {
const policyGate = new PoliciesService(this.user)
policyGate.authorize(UserAction.Create, Resource.Attachments)
const userId = z.string().parse(this.user.internalUserId)

// TODO: @arpandhakal - $transaction here could consume a lot of sequential db connections, better to use Promise.all
// and reuse active connections instead.
const newAttachments = await this.db.$transaction(async (prisma) => {
const createPromises = data.map((attachmentData) =>
prisma.attachment.create({
data: {
...attachmentData,
createdById: userId,
createdById: z.string().parse(this.user.internalUserId || this.user.clientId), // CU are also allowed to create attachments
workspaceId: this.user.workspaceId,
},
}),
Expand Down Expand Up @@ -86,4 +87,59 @@ export class AttachmentsService extends BaseService {
const { data } = await supabase.supabase.storage.from(supabaseBucket).createSignedUrl(filePath, signedUrlTtl)
return data?.signedUrl
}

async deleteAttachmentsOfComment(commentId: string) {
const policyGate = new PoliciesService(this.user)
policyGate.authorize(UserAction.Delete, Resource.Attachments)

const commentAttachment = await this.db.$transaction(async (tx) => {
const commentAttachment = await tx.attachment.findMany({
where: { commentId: commentId, workspaceId: this.user.workspaceId },
select: { filePath: true },
})

await tx.attachment.deleteMany({
where: { commentId: commentId, workspaceId: this.user.workspaceId },
})

return commentAttachment
})

// directly delete attachments from bucket when deleting comments.
// Postgres transaction is not valid for supabase object so placing it after record deletion from db
const filePathArray = commentAttachment.map((el) => el.filePath)
const supabase = new SupabaseService()
await supabase.removeAttachmentsFromBucket(filePathArray)
}

async deleteAttachmentsOfTask(taskIds: string[]) {
const taskAttachment = await this.db.$transaction(async (tx) => {
const taskAttachment = await tx.attachment.findMany({
where: {
taskId: {
in: taskIds,
},
workspaceId: this.user.workspaceId,
},
select: { filePath: true },
})

await tx.attachment.deleteMany({
where: {
taskId: {
in: taskIds,
},
workspaceId: this.user.workspaceId,
},
})

return taskAttachment
})

// directly delete attachments from bucket when deleting comments.
// Postgres transaction is not valid for supabase object so placing it after record deletion from db
const filePathArray = taskAttachment.map((el) => el.filePath)
const supabase = new SupabaseService()
await supabase.removeAttachmentsFromBucket(filePathArray)
}
}
16 changes: 16 additions & 0 deletions src/app/api/attachments/public/public.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RFC3339DateSchema } from '@/types/common'
import { AssigneeType } from '@prisma/client'
import z from 'zod'

export const PublicAttachmentDtoSchema = z.object({
id: z.string().uuid(),
fileName: z.string(),
fileSize: z.number(),
mimeType: z.string(),
downloadUrl: z.string().url().nullable(),
uploadedBy: z.string().uuid(),
uploadedByUserType: z.nativeEnum(AssigneeType).nullable(),
uploadedDate: RFC3339DateSchema,
})

export type PublicAttachmentDto = z.infer<typeof PublicAttachmentDtoSchema>
65 changes: 65 additions & 0 deletions src/app/api/attachments/public/public.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { PublicAttachmentDto } from '@/app/api/attachments/public/public.dto'
import { RFC3339DateSchema } from '@/types/common'
import { toRFC3339 } from '@/utils/dateHelper'
import { sanitizeFileName } from '@/utils/sanitizeFileName'
import { createSignedUrls } from '@/utils/signUrl'
import { Attachment, CommentInitiator } from '@prisma/client'
import z from 'zod'

export class PublicAttachmentSerializer {
/**
*
* @param attachments array of Attachment
* @param uploadedBy id of the one who commented
* @param uploadedByUserType usertype of the one who commented
* @returns Array of PublicAttachmentDto
*/
static async serializeAttachments({
attachments,
uploadedByUserType,
content,
uploadedBy,
}: {
attachments: Attachment[]
uploadedByUserType: CommentInitiator | null
content: string | null
uploadedBy?: string
}): Promise<PublicAttachmentDto[]> {
// check if attachments are in the content. If yes
const attachmentPaths = attachments
.map((attachment) => {
return attachment.filePath
})
.filter((path) => content?.includes(path))

const signedUrls = await PublicAttachmentSerializer.getFormattedSignedUrls(attachmentPaths)

return attachments
.map((attachment) => {
const url = signedUrls.find((item) => item.path === attachment.filePath)?.url
if (!url) return null
return {
id: attachment.id,
fileName: sanitizeFileName(attachment.fileName),
fileSize: attachment.fileSize,
mimeType: attachment.fileType,
downloadUrl: attachment.deletedAt
? null
: z
.string()
.url({ message: `Invalid downloadUrl for attachment with id ${attachment.id}` })
.parse(url),
uploadedBy: uploadedBy || attachment.createdById,
uploadedByUserType: uploadedByUserType,
uploadedDate: RFC3339DateSchema.parse(toRFC3339(attachment.createdAt)),
}
})
.filter((attachment) => attachment !== null)
}

static async getFormattedSignedUrls(attachmentPaths: string[]) {
if (!attachmentPaths.length) return []
const signedUrls = await createSignedUrls(attachmentPaths)
return signedUrls.map((item) => ({ path: item.path, url: item.signedUrl }))
}
}
Loading
Loading