Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
4 changes: 2 additions & 2 deletions src/app/api/comments/comment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ export class CommentService extends BaseService {
filters.push(
// Get tasks that includes the client as a viewer
{
viewers: {
associations: {
hasSome: [{ clientId, companyId }, { companyId }],
},
},
Expand All @@ -494,7 +494,7 @@ export class CommentService extends BaseService {
filters.push(
// Get tasks that includes the company as a viewer
{
viewers: {
associations: {
hasSome: [{ companyId }],
},
},
Expand Down
12 changes: 6 additions & 6 deletions src/app/api/notification/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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 { AssociationsSchema } from '@/types/dto/tasks.dto'
import { getTaskViewers } from '@/utils/assignee'

export class NotificationService extends BaseService {
Expand Down Expand Up @@ -75,7 +75,7 @@ export class NotificationService extends BaseService {

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

const taskViewers = ViewersSchema.parse(task.viewers)
const taskViewers = AssociationsSchema.parse(task.associations)

// 3. Save notification to ClientNotification or InternalUserNotification table. Check for notification.recipientClientId too
if (task.assigneeType === AssigneeType.client && !!notification.recipientClientId && !opts.disableInProduct) {
Expand Down Expand Up @@ -381,7 +381,7 @@ export class NotificationService extends BaseService {
throw new APIError(httpStatus.NOT_FOUND, `Unknown assignee type: ${task.assigneeType}`)
}
}
const viewers = ViewersSchema.parse(task.viewers)
const viewers = AssociationsSchema.parse(task.associations)

switch (action) {
case NotificationTaskActions.Shared:
Expand Down Expand Up @@ -550,14 +550,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
8 changes: 4 additions & 4 deletions src/app/api/tasks/public/public.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { RFC3339DateSchema } from '@/types/common'
import { CopilotAPI } from '@/utils/CopilotAPI'
import { AssigneeType } from '@prisma/client'
import { z } from 'zod'
import { validateUserIds, ViewersSchema } from '@/types/dto/tasks.dto'
import { validateUserIds, AssociationsSchema } from '@/types/dto/tasks.dto'
import { PublicAttachmentDtoSchema } from '@/app/api/attachments/public/public.dto'

export const TaskSourceSchema = z.enum(['web', 'api'])
Expand Down Expand Up @@ -41,7 +41,7 @@ export const PublicTaskDtoSchema = z.object({
internalUserId: z.string().uuid().nullable(),
clientId: z.string().uuid().nullable(),
companyId: z.string().uuid().nullable(),
viewers: ViewersSchema,
viewers: AssociationsSchema,
attachments: z.array(PublicAttachmentDtoSchema),
})
export type PublicTaskDto = z.infer<typeof PublicTaskDtoSchema>
Expand All @@ -59,7 +59,7 @@ export const publicTaskCreateDtoSchemaFactory = (token: string) => {
internalUserId: z.string().uuid().optional(),
clientId: z.string().uuid().optional(),
companyId: z.string().uuid().optional(),
viewers: ViewersSchema, //right now, we only need the feature to have max of 1 viewer per task
viewers: AssociationsSchema, //right now, we only need the feature to have max of 1 viewer per task
})
.superRefine(async (data, ctx) => {
const { name, templateId, internalUserId, clientId, status } = data
Expand Down Expand Up @@ -153,7 +153,7 @@ export const PublicTaskUpdateDtoSchema = z
internalUserId: z.string().uuid().nullish(),
clientId: z.string().uuid().nullish(),
companyId: z.string().uuid().nullish(),
viewers: ViewersSchema,
viewers: AssociationsSchema,
})
.superRefine(validateUserIds)

Expand Down
4 changes: 2 additions & 2 deletions src/app/api/tasks/public/public.serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
CreateTaskRequestSchema,
UpdateTaskRequest,
UpdateTaskRequestSchema,
ViewersSchema,
AssociationsSchema,
} from '@/types/dto/tasks.dto'
import { rfc3339ToDateString, toRFC3339 } from '@/utils/dateHelper'
import { sanitizeHtml } from '@/utils/santizeContents'
Expand Down Expand Up @@ -63,7 +63,7 @@ export class PublicTaskSerializer {
internalUserId: task.internalUserId,
clientId: task.clientId,
companyId: task.companyId,
viewers: ViewersSchema.parse(task.viewers),
viewers: AssociationsSchema.parse(task.associations),
attachments: await PublicAttachmentSerializer.serializeAttachments({
attachments: task.attachments,
uploadedByUserType: 'internalUser', // task creator is always IU
Expand Down
49 changes: 30 additions & 19 deletions src/app/api/tasks/public/public.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users'
import { deleteTaskNotifications, sendTaskCreateNotifications, sendTaskUpdateNotifications } from '@/jobs/notifications'
import { TaskWithWorkflowState } from '@/types/db'
import { CreateTaskRequest, CreateTaskRequestSchema, UpdateTaskRequest, Viewers, ViewersSchema } from '@/types/dto/tasks.dto'
import { CreateTaskRequest, UpdateTaskRequest, Associations, AssociationsSchema } from '@/types/dto/tasks.dto'
import { DISPATCHABLE_EVENT } from '@/types/webhook'
import { UserIdsType } from '@/utils/assignee'
import { isPastDateString } from '@/utils/dateHelper'
Expand Down Expand Up @@ -170,13 +169,13 @@ export class PublicTasksService extends TasksSharedService {
console.info('TasksService#createTask | createdById overridden for public API:', createdById)
}

let viewers: Viewers = []
if (data.viewers?.length) {
let viewers: Associations = []
if (data.associations?.length) {
if (!validatedIds.internalUserId) {
throw new APIError(httpStatus.BAD_REQUEST, `Task cannot be created with viewers if its not assigned to an IU.`)
}
viewers = await this.validateViewers(data.viewers)
console.info('PublicTasksService#createTask | Viewers validated for task:', viewers)
viewers = await this.validateViewers(data.associations)
console.info('PublicTasksService#createTask | Associations validated for task:', viewers)
}

// Create a new task associated with current workspaceId. Also inject current request user as the creator.
Expand All @@ -191,7 +190,7 @@ export class PublicTasksService extends TasksSharedService {
source: Source.api,
assigneeId,
assigneeType,
viewers: viewers,
associations: viewers,
...validatedIds,
...(opts?.manualTimestamp && { createdAt: opts.manualTimestamp }),
...(await getTaskTimestamps('create', this.user, data, undefined, workflowStateStatus)),
Expand Down Expand Up @@ -279,6 +278,25 @@ export class PublicTasksService extends TasksSharedService {
return newTask
}

private async getValidatedAssociations({
prevAssociations,
associationsResetCondition,
}: {
prevAssociations: Prisma.JsonValue[]
associationsResetCondition: boolean
}) {
let associations: Associations = AssociationsSchema.parse(prevAssociations)
if (associations) {
// only update of associations attribute is available. No associations in payload attribute means the data remains as it is in DB.
if (associationsResetCondition || !associations?.length) {
associations = [] // reset associations to [] if task is not reassigned to IU.
} else if (associations?.length) {
associations = await this.validateViewers(associations)
}
}
return associations
}

async updateTask(id: string, data: UpdateTaskRequest) {
const policyGate = new PoliciesService(this.user)
policyGate.authorize(UserAction.Update, Resource.Tasks)
Expand Down Expand Up @@ -314,17 +332,10 @@ export class PublicTasksService extends TasksSharedService {
companyId: validatedIds?.companyId ?? null,
})

let viewers: Viewers = ViewersSchema.parse(prevTask.viewers)

const viewersResetCondition = shouldUpdateUserIds ? !!clientId || !!companyId : !prevTask.internalUserId
if (data.viewers) {
// only update of viewers attribute is available. No viewers in payload attribute means the data remains as it is in DB.
if (viewersResetCondition || !data.viewers?.length) {
viewers = [] // reset viewers to [] if task is not reassigned to IU.
} else if (data.viewers?.length) {
viewers = await this.validateViewers(data.viewers)
}
}
const associations: Associations = await this.getValidatedAssociations({
prevAssociations: prevTask.associations,
associationsResetCondition: shouldUpdateUserIds ? !!clientId || !!companyId : !prevTask.internalUserId,
})

const userAssignmentFields = shouldUpdateUserIds
? {
Expand Down Expand Up @@ -371,7 +382,7 @@ export class PublicTasksService extends TasksSharedService {
archivedBy,
completedBy,
completedByUserType,
viewers,
associations,
...userAssignmentFields,
...(await getTaskTimestamps('update', this.user, data, prevTask)),
},
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/tasks/subtasks.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users'
import { ViewersSchema, ViewerType } from '@/types/dto/tasks.dto'
import { AssociationsSchema, ViewerType } from '@/types/dto/tasks.dto'
import { CopilotAPI } from '@/utils/CopilotAPI'
import { buildLtreeNodeString } from '@/utils/ltree'
import APIError from '@api/core/exceptions/api'
Expand All @@ -13,7 +13,7 @@ interface Assignable {
internalUserId: string | null
clientId: string | null
companyId: string | null
viewers: JsonValue[]
associations: JsonValue[]
}

export class SubtaskService extends BaseService {
Expand Down Expand Up @@ -127,8 +127,8 @@ export class SubtaskService extends BaseService {
latestAccessibleTaskIndex = tasks.findLastIndex((task) => {
let viewer: ViewerType | undefined
// check if viewer exists and parse and assign viewer
if (Array.isArray(task.viewers) && !!task.viewers.length) {
viewer = ViewersSchema.parse(task.viewers)?.[0]
if (Array.isArray(task.associations) && !!task.associations.length) {
viewer = AssociationsSchema.parse(task.associations)?.[0]
}

return !(
Expand Down
12 changes: 6 additions & 6 deletions src/app/api/tasks/task-notifications.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Uuid } from '@/types/common'
import { TaskWithWorkflowState } from '@/types/db'
import { TaskResponseSchema, Viewers, ViewersSchema, ViewerType } from '@/types/dto/tasks.dto'
import { TaskResponseSchema, Associations, AssociationsSchema, ViewerType } from '@/types/dto/tasks.dto'
import { getTaskViewers } from '@/utils/assignee'
import { CopilotAPI } from '@/utils/CopilotAPI'
import User from '@api/core/models/User.model'
Expand Down Expand Up @@ -37,7 +37,7 @@ export class TaskNotificationsService extends BaseService {
private async checkParentAccessible(task: TaskWithWorkflowState): Promise<boolean> {
if (!task.assigneeId || !task.parentId) return false

const viewers = ViewersSchema.parse(task.viewers)
const viewers = AssociationsSchema.parse(task.associations)
const checkParentViewers = (
clientId: string | null,
companyIds?: string[],
Expand All @@ -60,11 +60,11 @@ export class TaskNotificationsService extends BaseService {
if (task.assigneeType === AssigneeType.client || task.assigneeType === AssigneeType.company || !!viewers?.length) {
const parentTask = await this.db.task.findFirst({
where: { id: task.parentId, workspaceId: this.user.workspaceId },
select: { assigneeId: true, assigneeType: true, viewers: true },
select: { assigneeId: true, assigneeType: true, associations: true },
})
if (!parentTask) return false

const parentViewer = getTaskViewers(TaskResponseSchema.pick({ viewers: true }).parse(parentTask))
const parentViewer = getTaskViewers(TaskResponseSchema.pick({ associations: true }).parse(parentTask))

if (task.assigneeType === AssigneeType.client) {
const client = await this.copilot.getClient(task.assigneeId)
Expand Down Expand Up @@ -121,7 +121,7 @@ export class TaskNotificationsService extends BaseService {
// If task is a subtask for a client/company and isn't visible on task board (is disjoint)
if (await this.checkParentAccessible(task)) return

const viewers = ViewersSchema.parse(task.viewers)
const viewers = AssociationsSchema.parse(task.associations)
if (viewers?.length && !isReassigned) {
const clientId = viewers[0].clientId
const sendViewersNotifications = clientId
Expand Down Expand Up @@ -354,7 +354,7 @@ export class TaskNotificationsService extends BaseService {
}
}

private sendUserTaskSharedNotification = async (task: Task, viewers: Viewers) => {
private sendUserTaskSharedNotification = async (task: Task, viewers: Associations) => {
if (!viewers?.length) return

const notificationType = NotificationTaskActions.Shared
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/tasks/tasks.logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import User from '@api/core/models/User.model'
import { BaseService } from '@api/core/services/base.service'
import { ActivityType, AssigneeType, Task, WorkflowState } from '@prisma/client'
import { ViewerAddedSchema, ViewerRemovedSchema } from '@api/activity-logs/schemas/ViewerSchema'
import { ViewersSchema } from '@/types/dto/tasks.dto'
import { AssociationsSchema } from '@/types/dto/tasks.dto'

/**
* Wrapper over ActivityLogger to implement a clean abstraction for task creation / update events
Expand Down Expand Up @@ -55,9 +55,9 @@ export class TasksActivityLogger extends BaseService {
}
}

if (Array.isArray(this.task.viewers) && Array.isArray(prevTask.viewers)) {
const currentViewers = ViewersSchema.parse(this.task.viewers) || []
const prevViewers = ViewersSchema.parse(prevTask.viewers) || []
if (Array.isArray(this.task.associations) && Array.isArray(prevTask.associations)) {
const currentViewers = AssociationsSchema.parse(this.task.associations) || []
const prevViewers = AssociationsSchema.parse(prevTask.associations) || []

if (
(!!currentViewers.length || !!prevViewers.length) &&
Expand Down
Loading
Loading