Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
37d7140
feat(OUT-3072): support task association while creating task
SandipBajracharya Feb 6, 2026
93eeb08
chore(OUT-3072): replace viewers by associations
SandipBajracharya Feb 6, 2026
ecacb5f
fix(OUT-3072): omit associations and isShared attributes
SandipBajracharya Feb 6, 2026
caa24b1
refactor(OUT-3072): use appropriate component name, break function to…
SandipBajracharya Feb 6, 2026
9d0fb00
Merge branch 'feature/api-improvements' of github.com:copilot-platfor…
SandipBajracharya Feb 10, 2026
fdd8414
chore(OUT-3058): add latest @assembly-js/node-sdk
SandipBajracharya Feb 6, 2026
962dbd0
feat(OUT-3073): support task association in task detail page
SandipBajracharya Feb 10, 2026
a112877
feat(OUT-3127): show confirmation dialog when assignee/association is…
SandipBajracharya Feb 12, 2026
3691345
feat(OUT-3127): show confirmation dialog in tasak list/board view
SandipBajracharya Feb 12, 2026
19bab42
refactor(OUT-3127): rename functions, linting
SandipBajracharya Feb 12, 2026
37e5094
feat(OUT-3014): add association filter in task board/list view
SandipBajracharya Feb 12, 2026
979e017
feat(OUT-3014): add shared with filter in task board and list view
SandipBajracharya Feb 13, 2026
71b4b58
feat(OUT-3136): update associations in CU and CRM view
SandipBajracharya Feb 13, 2026
4979cb9
feat(OUT-3136): add disable option in copilot toggle component and di…
SandipBajracharya Feb 13, 2026
9a1c376
feat(OUT-3160): add an unassigned view in IU and CRM dashboard
SandipBajracharya Feb 17, 2026
7d8a1f4
feat(OUT-3013): notification for client/company when task is shared
arpandhakal Feb 19, 2026
dafa5aa
fix: await async assemblyApi() factory with lazy init pattern
foleyatwork Feb 17, 2026
6691021
fix: derive assemblyApi return type to fix TS build
foleyatwork Feb 18, 2026
2506a87
refactor(OUT-3127): follow best pratices
SandipBajracharya Feb 13, 2026
25d6a65
feat(OUT-3015): supported association and isShared on public task cre…
arpandhakal Feb 19, 2026
2572c63
fix(OUT-3170): Changing association when task is shared removes assoc…
arpandhakal Feb 19, 2026
ad5da81
fix(OUT-3178): allowed task to be created with no assignee from publi…
arpandhakal Feb 20, 2026
e775816
fix(OUT-3179): Resetting association from public API on task update d…
arpandhakal Feb 20, 2026
6773836
fix(OUT-3179): separate utility for handling association update and i…
arpandhakal Feb 23, 2026
2acafb5
fix(OUT-3186): added viewers on task response on public api
arpandhakal Feb 23, 2026
ee5c688
fix(OUT-3013): changed viewers to associations
arpandhakal Feb 19, 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
@@ -1,5 +1,6 @@
{
"dependencies": {
"@assembly-js/node-sdk": "^3.19.1",
"@cyntler/react-doc-viewer": "^1.17.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
Expand Down Expand Up @@ -70,6 +71,7 @@
"nodemon": "^3.1.9",
"open": "^10.1.0",
"prettier": "^3.1.1",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.3.0",
"text-table": "^0.2.0",
"tsx": "^4.16.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
- This query renames viewers to associations and add isShared column.
*/
-- AlterTable
ALTER TABLE "Tasks"
RENAME COLUMN "viewers" TO "associations";

ALTER TABLE "Tasks"
ADD COLUMN "isShared" BOOLEAN NOT NULL DEFAULT false;
3 changes: 2 additions & 1 deletion prisma/schema/task.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ model Task {

taskUpdateBacklogs TaskUpdateBacklog[]

viewers Json[] @db.JsonB @default([])
associations Json[] @db.JsonB @default([])
isShared Boolean @default(false)

@@index([path], type: Gist)
@@map("Tasks")
Expand Down
96 changes: 48 additions & 48 deletions sentry.client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,55 @@
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from "@sentry/nextjs";
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({
dsn,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: isProd ? 0.2 : 1,
profilesSampleRate: 0.1,
// NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// You can remove this option if you're not planning to use the Sentry Session Replay feature:
// NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least
// replaysOnErrorSampleRate: 1.0,
// replaysSessionSampleRate: 0,
integrations: [
Sentry.browserTracingIntegration({
beforeStartSpan: (e) => {
console.info('SentryBrowserTracingSpan', e.name)
return e
},
}),
// Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
// maskAllText: true,
// blockAllMedia: true,
// }),
],

// ignoreErrors: [/fetch failed/i],
ignoreErrors: [/fetch failed/i],

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null
}
event.tags = {
...event.tags,
// Adding additional app_env tag for cross-checking
app_env: isProd ? 'production' : vercelEnv || 'development',
}
return event
},
})
Sentry.init({
dsn,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: isProd ? 0.2 : 1,
profilesSampleRate: 0.1,
// NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// You can remove this option if you're not planning to use the Sentry Session Replay feature:
// NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least
// replaysOnErrorSampleRate: 1.0,
// replaysSessionSampleRate: 0,
integrations: [
Sentry.browserTracingIntegration({
beforeStartSpan: (e) => {
console.info('SentryBrowserTracingSpan', e.name)
return e
},
}),
// Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
// maskAllText: true,
// blockAllMedia: true,
// }),
],

// ignoreErrors: [/fetch failed/i],
ignoreErrors: [/fetch failed/i],

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null
}
event.tags = {
...event.tags,
// Adding additional app_env tag for cross-checking
app_env: isProd ? 'production' : vercelEnv || 'development',
}
return event
},
})
}
34 changes: 17 additions & 17 deletions sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from "@sentry/nextjs";
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'

if (dsn) {
Sentry.init({
dsn,
Sentry.init({
dsn,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
ignoreErrors: [/fetch failed/i],
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
ignoreErrors: [/fetch failed/i],

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null
}
return event
},
})
beforeSend(event) {
if (!isProd && event.type === undefined) {
return null
}
return event
},
})
}
40 changes: 22 additions & 18 deletions src/app/api/comments/comment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AttachmentsService } from '@/app/api/attachments/attachments.service'
import { PublicCommentSerializer } from '@/app/api/comments/public/public.serializer'
import { sendCommentCreateNotifications } from '@/jobs/notifications'
import { sendReplyCreateNotifications } from '@/jobs/notifications/send-reply-create-notifications'
import { InitiatedEntity } from '@/types/common'
import { InitiatedEntity, TempClientFilter } from '@/types/common'
import { CreateAttachmentRequestSchema } from '@/types/dto/attachments.dto'
import { CommentsPublicFilterType, CommentWithAttachments, CreateComment, UpdateComment } from '@/types/dto/comment.dto'
import { DISPATCHABLE_EVENT } from '@/types/webhook'
Expand Down Expand Up @@ -463,9 +463,10 @@ export class CommentService extends BaseService {
}
}

protected getClientOrCompanyAssigneeFilter(includeViewer: boolean = true): Prisma.TaskWhereInput {
protected getClientOrCompanyAssigneeFilter(includeAssociatedTask: boolean = true): Prisma.TaskWhereInput {
const clientId = z.string().uuid().parse(this.user.clientId)
const companyId = z.string().uuid().parse(this.user.companyId)
const isCuPortal = !this.user.internalUserId && (clientId || companyId)

const filters = []

Expand All @@ -476,29 +477,32 @@ export class CommentService extends BaseService {
// Get company tasks for the client's companyId
{ companyId, clientId: null },
)
if (includeViewer)
filters.push(
// Get tasks that includes the client as a viewer
{
viewers: {
hasSome: [{ clientId, companyId }, { companyId }],
},
if (includeAssociatedTask) {
const tempClientFilter: TempClientFilter = {
associations: {
hasSome: [{ clientId, companyId }, { companyId }],
},
)
}
if (isCuPortal) tempClientFilter.isShared = true
// Get tasks that includes the client as a association
filters.push(tempClientFilter)
}
} else if (companyId) {
filters.push(
// Get only company tasks for the client's companyId
{ clientId: null, companyId },
)
if (includeViewer)
filters.push(
// Get tasks that includes the company as a viewer
{
viewers: {
hasSome: [{ companyId }],
},

// Get tasks that includes the company as a association
if (includeAssociatedTask) {
const tempCompanyFilter: TempClientFilter = {
associations: {
hasSome: [{ companyId }],
},
)
}
if (isCuPortal) tempCompanyFilter.isShared = true
filters.push(tempCompanyFilter)
}
}
return filters.length > 0 ? { OR: filters } : {}
} //Repeated twice because taskSharedService is an abstract class.
Expand Down
36 changes: 17 additions & 19 deletions src/app/api/notification/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { AssigneeType, ClientNotification, Task } from '@prisma/client'
import Bottleneck from 'bottleneck'
import httpStatus from 'http-status'
import { z } from 'zod'
import { Viewers, ViewersSchema } from '@/types/dto/tasks.dto'
import { getTaskViewers } from '@/utils/assignee'
import { AssociationsSchema } from '@/types/dto/tasks.dto'
import { getTaskAssociations } from '@/utils/assignee'

export class NotificationService extends BaseService {
async create(
Expand Down Expand Up @@ -75,8 +75,6 @@ export class NotificationService extends BaseService {

console.info('NotificationService#create | Created single notification:', notification)

const taskViewers = ViewersSchema.parse(task.viewers)

// 3. Save notification to ClientNotification or InternalUserNotification table. Check for notification.recipientClientId too
if (task.assigneeType === AssigneeType.client && !!notification.recipientClientId && !opts.disableInProduct) {
await this.addToClientNotifications(task, NotificationCreatedResponseSchema.parse(notification))
Expand Down Expand Up @@ -280,14 +278,14 @@ export class NotificationService extends BaseService {
*/
markClientNotificationAsRead = async (task: Task) => {
try {
const taskViewer = getTaskViewers(task)
const taskAssociations = getTaskAssociations(task)

// Due to race conditions, we are forced to allow multiple client notifications for a single notification as well
const relatedNotifications = await this.db.clientNotification.findMany({
where: {
// Accomodate company task lookups where clientId is null
clientId: Uuid.nullable().parse(task.clientId) || taskViewer?.clientId,
companyId: Uuid.parse(task.companyId ?? taskViewer?.companyId),
clientId: Uuid.nullable().parse(task.clientId) || taskAssociations?.clientId,
companyId: Uuid.parse(task.companyId ?? taskAssociations?.companyId),
taskId: task.id,
},
})
Expand Down Expand Up @@ -381,18 +379,18 @@ export class NotificationService extends BaseService {
throw new APIError(httpStatus.NOT_FOUND, `Unknown assignee type: ${task.assigneeType}`)
}
}
const viewers = ViewersSchema.parse(task.viewers)
const associations = AssociationsSchema.parse(task.associations)

switch (action) {
case NotificationTaskActions.Shared:
senderId = task.createdById
recipientId = !!viewers?.length ? z.string().parse(viewers[0].clientId) : ''
recipientId = !!associations?.length ? z.string().parse(associations[0].clientId) : ''
actionTrigger = await this.copilot.getInternalUser(senderId)
break
case NotificationTaskActions.SharedToCompany:
senderId = task.createdById
recipientIds = !!viewers?.length
? (await this.copilot.getCompanyClients(z.string().parse(viewers[0].companyId))).map((client) => client.id)
recipientIds = !!associations?.length
? (await this.copilot.getCompanyClients(z.string().parse(associations[0].companyId))).map((client) => client.id)
: []
actionTrigger = await this.copilot.getInternalUser(senderId)
break
Expand Down Expand Up @@ -450,17 +448,17 @@ export class NotificationService extends BaseService {
console.info('fetched client Ids', clientIds)
recipientIds = clientIds
}
if (viewers?.length) {
const clientId = viewers[0].clientId
if (associations?.length) {
const clientId = associations[0].clientId
if (clientId) {
recipientIds = [clientId] //spread recipientIds if we allow viewers on client tasks.
recipientIds = [clientId] //spread recipientIds if we allow associations on client tasks.
} else {
const clientsInCompany = await this.copilot.getCompanyClients(viewers[0].companyId)
const clientsInCompany = await this.copilot.getCompanyClients(associations[0].companyId)
const clientIds = clientsInCompany.map((client) => client.id)
console.info('fetched client Ids', clientIds)
recipientIds = clientIds
}
} //viewers comment notifications
} //associations comment notifications
// this break is needed otherwise we will fallthrough to the IU case.
// This is honestly unhinged JS behavior, I would not expect the
// next case to run if the switch did not match it
Expand Down Expand Up @@ -550,14 +548,14 @@ export class NotificationService extends BaseService {
senderCompanyId?: string,
): NotificationRequestBody {
// Assume client notification then change details body if IU
const viewers = ViewersSchema.parse(task.viewers)
const viewer = viewers?.[0]
const associations = AssociationsSchema.parse(task.associations)
const association = associations?.[0]
const notificationDetails: NotificationRequestBody = {
senderId,
senderCompanyId,
senderType: this.user.role,
recipientClientId: recipientId ?? undefined,
recipientCompanyId: task.companyId ?? viewer?.companyId ?? undefined,
recipientCompanyId: task.companyId ?? association?.companyId ?? undefined,
// If any of the given action is not present in details obj, that type of notification is not sent
deliveryTargets: deliveryTargets || {},
}
Expand Down
Loading
Loading